最近的项目比较忙,能腾出的业余时间不多。周内,“机缘巧合” 之下,与国内的某知名手机厂商的架构师们,一起聊了聊如何进行 Android 的架构治理,而其中的出发点是:如何从依赖治理的角度来进行 Android 的架构治理?
作为一个非常熟悉 Android 和 Harmony OS 依赖分析的、非专业移动应用开发者,我大抵还算是有一定的经验。先从结论来说,Android 应用与一般的 Web 应用存在诸多的差异,在分析方式上也存在比较大的区别。也因此,而如果没有足够的体量或者是数量,那么并不需要花费大量的时间在治理的。
先看一个 TL;DR 版本,围绕于 Android 依赖分析的一个核心概念图:
从图上可以看到,多样化制品、生命周期、依赖类型,是我们在这里关注的几个重点。
接着,让我们看一些明显的差异点:
.class
=> .dex
,而如果在过程中使用 Proguard、R8 等混淆工具,那么又会产生一些额外的中间表示。src/main/java
形式,其中的 main、java 都可以配置成不同的形式,如 src/demoDebug/kotlin
。也因此 ,Android 也与普通的 Web 应用差异较大,除了可以使用多种语言,如 Kotlin、Java 之外,Android 变体的存在,也使得针对于源码分析,会变得异常的复杂。在上述的几种因素的结合之下,不论是分析源码,还是针对于构建后的中间表示进行分析,都会变得相当的复杂。所以,在继续进一步展开之前,你需要考虑一下性价比。
为了让没有 Android 经验的读者能理解一下上述的差异,我们先简单了解一下:变体 —— 可以根据API 级别或其他设备变化因素,为应用构建以不同设备为目标的不同版本。 如下图所示是一个变体的示例:一个 Android 项目中,可以根据 uildType、DeviceType、ProductFlavor 组合构建出应用:
如果我们有 debug、release 两种 BuildType,还有 phone、car、tv 三种 DeviceType,那么我们至少会构建出 6 个应用,即 BuildType * DevcieType 会产生 debugPhone、releasePhone 等 6 种结合。而假设我们配置了两种 Product Flavors,那么会构建出 12 个应用。
而这种复杂度会使得我们在分析源码的时候出现困难,因为源码(SourceSet)也可以根据变体进行配置,因此在源码上也会出现 12 种可能性。而这种复杂度,难以像 Web 一样,可以通过手动的方式来配置,需要根据 Gradle 的 API 来获取变体相关的配置。
在 Web 应用中,我们可以使用 ASM 字节码框架来分析生成的 jar 包。但是在 Android 应用中,最后的产出是一个 APK。而 “众所周知”:”.apk” != “.jar”
,在不加壳的情况下,apk 解压完后,我们会得到的一个 classes.dex
的文件。所以,在这个时候,我们会有两种做法:
.dex
转为 .smali
再进行分析。如下图所示:
而再 “众所周知” 一下,如果我们在过程中使用摇树优化的话,无用的代码就直接 byebyte 了。所以,要获得最有用的结果,那必须是过程中通过 Gradle 构建出来 的 .class
文件,在它之上进行分析。
所以,为了得到准确的分析结果,我们需要了解一下 Gradle 应用的构建过程。
从治理的角度来看,依旧包含大量的不确定性,所以在这里只是初步的探索。这里的不确定性包含:
而每个问题都足够的大 —— 它们都处于不同的架构模式和风格之下,需要进一步的讨论。不过,从分析的模式上来说,它们都比较的统一。
在 ArchGuard ( https://archguard.org/ )中,我们定义的架构治理的三个时期是:设计态、开发态、运行态;在 Android 中,经过上面的分析,我们根据它的生命周期分析的三个时态是:编译前、编译时、编译后。
每个阶段对应于不同的分析模式:
对应于不同的模式,有各自的分析场景和优劣势:
静态代码分析 | 基于构建工具分析 | 中间表示分析 | |
---|---|---|---|
适用场景 | 代码分析、架构分析、重构工具等 | 模块间依赖 | 代码依赖分析、编译优化 |
精确度 | 中。诸如注解需要定制 | 高。编译过程依赖于依赖解析 | 高。 |
开发难度(相对难度) | 中。已有的资源比较多 | 中。不同语言需要重新学习 | 高。相关学习资料少 |
方式 | 源码分析 | 过程产出物和编译时 API | 过程和结果产出物 |
工具示例 | Sonarqube、Findbugs | Android Studio、Harmony DT | Proguard/R8、Baksmali |
主要问题 | 分析结果的准确性依赖于框架的支持、语言特性分析等,类似于 IDE。想实现 100% 的准确性不太可能,适用度高,成本相对低。 | 依赖于 Gradle 的版本,需要考虑版本兼容性问题。官方文档较少,需要结合 ADT 中的 Gradle 源码。 | 由于过程和结果产出物,已经是优化的结果,想要 100% 复原是不可能的。 |
也因此,根据不同的情况下,我们可以划分不同的分析方式也治理手段,诸如于:
在上述的三种分析模式里,只有基于构建工具分析是在架构治理这一系列文章新出现的。主要是 Android 应用的架构与 Gradle 这一类构建工具的绑定过深,也因此在分析时候,我们需要结合 Gradle 才能完成。而在 Android 的 ADT 的设计中,我们需要借助于 ToolingModeBuilderRegistry 和 DefaultGradleConnector 才能从 build.gradle
中解析出 builder-model 中的 AndroidProject 相关的一系列模型:
在有了 AndroidProject 这个模型之后,我们就能构建出依赖分析时所需要的一系列信息,如 Library、SourceSet、Variant 等。
从现有的工具来说,Android 官方在 ADT 中提供的 Android Lint 就提供了一个非常好的参考案例和代码,详细可以看官方的文档。在 Android Lint 中,还提供了 Android Lint Universal AST 作为一个 AST (抽象语法树)的抽象层,可以适配不同的语言如 Kotlin、Java 等。如下是 Android Lint 中的模型,可以看出其中的 Detector 就是核心所在。在 Detector 中,定义了一系列相关的 Scanner 接口,用于进行 Lint,如下图所示:
而其中的是 JavaScanner 则是在编译后,借助于 ASM 进行分析。毕竟,从结果上来说,从 apk 分析不靠谱,远不如直接在构建的过程中,通过对于中间表示的分析方便。当然,这种方便,也意味着,我们需要对于 Android 构建工具也非常深入的了解。
从 Android Lint 对于 Android 的规范化,我们可以看出:将工具内建到开发流程中,才能使得我们的架构约束更有效果。然而,内建的工具往往可以非常容易被注释掉。这也是另外一个有意思的地方:当人们过分考虑短期的利益时,所有的长期性治理,就需要说明他的价值。
围观我的Github Idea墙, 也许,你会遇到心仪的项目