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

2023-11-20 13:22:42 浏览数 (1)

在上篇文章 《组件治理之多仓组件化编译的一些问题》中介绍,一些原本可在编译期间报错的问题被带到了运行时,我们需要开发一款检查插件,把 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

0 人点赞