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

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

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 标识:

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

java 复制代码
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 分支实现中。

相关推荐
流氓也是种气质 _Cookie几秒前
uniapp 在线更新应用
android·uniapp
zhangphil2 小时前
Android ValueAnimator ImageView animate() rotation,Kotlin
android·kotlin
徊忆羽菲3 小时前
CentOS7使用源码安装PHP8教程整理
android
编程、小哥哥4 小时前
python操作mysql
android·python
Couvrir洪荒猛兽4 小时前
Android实训十 数据存储和访问
android
五味香6 小时前
Java学习,List 元素替换
android·java·开发语言·python·学习·golang·kotlin
十二测试录7 小时前
【自动化测试】—— Appium使用保姆教程
android·经验分享·测试工具·程序人生·adb·appium·自动化
Couvrir洪荒猛兽8 小时前
Android实训九 数据存储和访问
android
aloneboyooo9 小时前
Android Studio安装配置
android·ide·android studio
Jacob程序员9 小时前
leaflet绘制室内平面图
android·开发语言·javascript