前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >(三)组件治理之编译期检查

(三)组件治理之编译期检查

作者头像
codelang
发布2023-11-20 13:22:42
1840
发布2023-11-20 13:22:42
举报
文章被收录于专栏:codelang

在上篇文章 《组件治理之多仓组件化编译的一些问题》中介绍,一些原本可在编译期间报错的问题被带到了运行时,我们需要开发一款检查插件,把 NoClassDefFoundError、NoSuchMethodError、NoSuchFieldError 与 AbstractMethodError 等异常提前在编译期间卡住。

1、收集所有参与编译的 Class 文件

参与项目编译的模块有:

  • Android SDK 源码
  • Java 源码
  • 依赖组件

1、Android SDK 源码我们可以通过读 AppExtension 的 compileSdkVersion 拿到参与编译的版本,然后读取 local.properties 里的 sdk.dir 路径,由此即可拼接出 android.jar 的路径,以此拿到 Android SDK 源码,读取到的路径如下:

SDK_DIR/platforms/android-$compileSdkVersion/android.jar

2、Java 源码不是很好拿到,从 jdk9 开始,已经没有 rt.jar 了,具体可以查看 oracle 关于 Removed rt.jar and tools.jar 的部分,所以,这里只好退而求其次,使用 jdk8 的 rt.jar 参与编译。

3、运行时的依赖可以通过 RuntimeClasspath Configuration 来拿到所有参与编译的依赖 jar 文件

在拿到上面所有的 jar 文件后,我们就可以通过 ASM 来读取 jar 里面的 Class 文件,并收集出 Class 文件的字段、方法等信息,然后存到一个以 ClassName 为 key 的 map 集合中,方便后面在分析 Class 文件时可以直接判断引用的类是否存在,并且还可以拿到 Class 相关的信息。

2、检查 class 文件引用外部类的情况

一个类引用到其他类的几种情况:

  • 注解:类、字段、方法、参数使用注解去描述的情况
  • 字段:使用类去申明的字段,基础类型忽略
  • 方法:方法 Code 里涉及到的外部类字段、方法的调用
  • 接口
  • 父类

我们在遍历所有参与编译依赖的 Class 文件时(Android、java 源码不参与遍历),即可通过这些情况去分析引用情况。这里有一个细节点,在方法 Code 中的字段与方法调用,在 owner 找不到的情况还要继续从他的父类与接口继续查找,因为调用的字段与方法有可能在父类。

一些特殊情况的处理:有的模块可能就是会报 unsolved,例如 androidx.compose.ui:ui 依赖的 RenderNodeApi23 与 RenderNodeApi29 类中的 RenderNode,他们的包名在不同的 SDK 版本不一样,但他们在运行阶段会通过 SDK 版本来选择加载哪个类,所以,类似这类的 unsolved 是可以放过的,但前提是做好 review

3、检查 xml 中 class 文件的引用情况

在 layout 的布局 xml 中,对于自定义 view 的定义,也需要进行类扫描

4、插件介绍

1、插件能力

  • 分析模块之间的真实引用关系,并生成 plantUML 与 mermaid 文件
  • 组件依赖重复类检查
  • 未解决的引用检查

2、执行插件

./gradlew moduleRef

执行完成后会在 app/build 目录生成 moduleRef.json 文件,效果如下:

代码语言:javascript
复制
{
  "androidx.compose.ui:ui:1.3.0": {
    "dependencies": [
      "org.jetbrains.kotlin:kotlin-stdlib:1.7.20",
      "androidx.compose.ui:ui-unit:1.3.0",
      "androidx.compose.runtime:runtime:1.3.0",
      "androidx.compose.ui:ui-graphics:1.3.0",
      "//............."
    ],
    "unsolved": {
      "clazz": [
        "android.view.RenderNode",
        "android.view.DisplayListCanvas"
      ],
      "fields": [
        "androidx.compose.ui.platform.RenderNodeApi23_android.view.RenderNode"
      ],
      "methods": []
    }
  }
}
  • dependenciesandroidx.compose.ui:ui:1.3.0 所使用到的依赖
  • unsolvedandroidx.compose.ui:ui:1.3.0 依赖使用到的 类、字段和方法在整个依赖关系中都找不到

3、生成的组件引用关系图的一部分:

image.png

5、一些小插曲:

AbstractMethodError 异常主要是检测没有实现父类的抽象方法,起初以为这个检查挺简单的,但在一路思考之后发现,并没有那么简单,画个树状图大家就能看明白了:

实现类的父类可能是抽象类,并且抽象类的父类可能也是抽象类,并且还带有接口,所以,就需要从前往后查找父类是否为抽象类,查到之后必须从后往前遍历,因为抽象类有可能把父类或是接口的抽象方法给实现,这样的话,子类就无需实现了,这种情况是不会发生 AbstractMethodError 异常的,这里还需要需要注意一下接口的 default 方法,接口里面实现父类接口时,如果用 defeault 实现抽象方法的话,这种情况子类也是无需实现的,并且,default 方法的 accessFlag 也没有 ACC_ABSTRACT 标识:

在我吭哧吭哧开发之后又发现一些小问题,接口的多继承下,是允许方法重复的,例如:

代码语言:javascript
复制
public interface IAnimal {
    void run();
}

public interface Dog extends IAnimal{
    void run();
}

所以,在收集方法 accessFlag 为 ACC_ABSTRACT 时,需要做一下去重。我以为终于解决所有问题了,但在检查结果时发现,还是有一些情况没有检测到,这个问题就真的离了大谱了,Java 编译出来的 class 是没问题的,问题出现在了 Kotlin 上面。在 Kotlin 中,接口继承接口时,也是可以实现父类的抽象方法,效果看起来跟 Java 的 default 类似,示例如下:

Dog 接口实现了父类 IAnimal 接口的抽象 run 方法,代码上来看并没有问题,但检测结果却报了 AbstractMethodError 异常,说 run 方法没有实现,如果按 java 的 default 方法来看的话,Dog 这个类的 run 方法应该是一个非抽象方法,现在只能 Decompile 看下具体原因了:

Kotlin 接口实现方法居然是通过桥接类做到的,Dog 类的 run 方法仍然是抽象方法,在 Kotlin 的这种情况下,我没办法通过类遍历来检查抽象方法有无实现。按道理,应该可以继续遍历接口的 innerClass 内部类,检查是否有 DefaultImpls 类,然后检查 DefaultImpls 的方法是否与接口方法签名一致,是的话,也算是实现了接口方法,目前这个部分的代码还在 feature 分支实现中。

源码地址:https://github.com/MRwangqi/ModuleRef

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2023-11-18,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 扣浪 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1、收集所有参与编译的 Class 文件
  • 2、检查 class 文件引用外部类的情况
  • 3、检查 xml 中 class 文件的引用情况
  • 4、插件介绍
  • 5、一些小插曲:
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档