在上篇文章 《组件管理之多仓组件化编译的一些问题》中介绍,一些原本可在编译期间报错的问题被带到了运转时,咱们需求开发一款查看插件,把 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 文件,作用如下:

{
  "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、生成的组件引证联系图的一部分:

(三)组件管理之编译期查看

5、一些小插曲:

AbstractMethodError 异常主要是检测没有完成父类的笼统办法,起初认为这个查看挺简略的,但在一路思考之后发现,并没有那么简略,画个树状图我们就能看理解了:

(三)组件管理之编译期查看

完成类的父类可能是笼统类,并且笼统类的父类可能也是笼统类,并且还带有接口,所以,就需求早年往后查找父类是否为笼统类,查到之后必须从后往前遍历,由于笼统类有可能把父类或是接口的笼统办法给完成,这样的话,子类就无需完成了,这种状况是不会产生 AbstractMethodError 异常的,这儿还需求需求注意一下接口的 default 办法,接口里边完成父类接口时,假如用 defeault 完成笼统办法的话,这种状况子类也是无需完成的,并且,default 办法的 accessFlag 也没有 ACC_ABSTRACT 标识:

(三)组件管理之编译期查看

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

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 分支完成中。