移动应用架构治理初探:从依赖分析与 Android 应用的生命周期说起

2022-08-25 20:00:20 浏览数 (1)

最近的项目比较忙,能腾出的业余时间不多。周内,“机缘巧合” 之下,与国内的某知名手机厂商的架构师们,一起聊了聊如何进行 Android 的架构治理,而其中的出发点是:如何从依赖治理的角度来进行 Android 的架构治理?

作为一个非常熟悉 Android 和 Harmony OS 依赖分析的、非专业移动应用开发者,我大抵还算是有一定的经验。先从结论来说,Android 应用与一般的 Web 应用存在诸多的差异,在分析方式上也存在比较大的区别。也因此,而如果没有足够的体量或者是数量,那么并不需要花费大量的时间在治理的。

先看一个 TL;DR 版本,围绕于 Android 依赖分析的一个核心概念图:

从图上可以看到,多样化制品、生命周期、依赖类型,是我们在这里关注的几个重点。

Web 后端 vs Android:微服务 vs 微内核架构

接着,让我们看一些明显的差异点:

  • 架构风格差异。与 Web 应用不同的是,在 Android 这一类资源受限的嵌入式设备中,性能是一个非常其中的因素。除此还有兼容性等,对于嵌入式设备而言,一旦应用发布分发之后,想要全面更新非常的困难。也因此,从性能的角度来说,任何的运行时分析的成本都是非常之高的;另外一方面,从架构风格上来说,移动应用也以单体 微内核/插件式架构为主。也因此,在 Android 应用中更多的是模块间依赖,还非微服务这种项目间的依赖。
  • 单一制品 vs 组合式的多制品。Android 应用算得上是多制品单体,即一个 Android 工程可以构建出不同的应用,以发布于不同的渠道、应用市场等。也因此,与 Web 应用的侧重点存在比较多的差异,诸如于:Android 需要重视构建阶段的分析、多变体情况下优先考虑中间表示进行分析等。
  • 多阶段中间表示。Android 在编译的过程中,会产生多种中间表示,如 Kotlin、Java、AIDL => .class => .dex,而如果在过程中使用 Proguard、R8 等混淆工具,那么又会产生一些额外的中间表示。
  • 非单一代码源。在 Gradle/Maven 工程中,源码是以 src/main/java 形式,其中的 main、java 都可以配置成不同的形式,如 src/demoDebug/kotlin。也因此 ,Android 也与普通的 Web 应用差异较大,除了可以使用多种语言,如 Kotlin、Java 之外,Android 变体的存在,也使得针对于源码分析,会变得异常的复杂。

在上述的几种因素的结合之下,不论是分析源码,还是针对于构建后的中间表示进行分析,都会变得相当的复杂。所以,在继续进一步展开之前,你需要考虑一下性价比。

变体:单一制品 vs 组合式的多制品

为了让没有 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 转为 .class,再通过 asm 分析。
  • 通过 baksmali 将 .dex 转为 .smali 再进行分析。

如下图所示:

而再 “众所周知” 一下,如果我们在过程中使用摇树优化的话,无用的代码就直接 byebyte 了。所以,要获得最有用的结果,那必须是过程中通过 Gradle 构建出来 的 .class 文件,在它之上进行分析。

所以,为了得到准确的分析结果,我们需要了解一下 Gradle 应用的构建过程。

Android 生命周期的分析与治理

从治理的角度来看,依旧包含大量的不确定性,所以在这里只是初步的探索。这里的不确定性包含:

  • 以小程序为核心的超级应用的痛点?不稳定?
  • 组件化/模块化架构的问题。是各个模块间的 API,需要确保符合接口?还是?
  • 常规应用是否需要架构治理?
  • 面向 OS 构建的应用的问题是?调用私有接口?
  • ……

而每个问题都足够的大 —— 它们都处于不同的架构模式和风格之下,需要进一步的讨论。不过,从分析的模式上来说,它们都比较的统一。

Android 应用架构治理分析模式

在 ArchGuard ( https://archguard.org/ )中,我们定义的架构治理的三个时期是:设计态、开发态、运行态;在 Android 中,经过上面的分析,我们根据它的生命周期分析的三个时态是:编译前、编译时、编译后。

  • 编译前。对源码进行语法、控制流行等的分析,从而实现对代码的依赖分析、静态检查、自动化重构等。
  • 编译时。通过编写 Gradle 插件/IDE 插件、执行特定的 task,分析各个模块间的依赖关系等。
  • 编译后。对编译过程或者编译后产生的中间表示(IR)分析,如字节码(bytecode)、smali 等。

每个阶段对应于不同的分析模式:

对应于不同的模式,有各自的分析场景和优劣势:

静态代码分析

基于构建工具分析

中间表示分析

适用场景

代码分析、架构分析、重构工具等

模块间依赖

代码依赖分析、编译优化

精确度

中。诸如注解需要定制

高。编译过程依赖于依赖解析

高。

开发难度(相对难度)

中。已有的资源比较多

中。不同语言需要重新学习

高。相关学习资料少

方式

源码分析

过程产出物和编译时 API

过程和结果产出物

工具示例

Sonarqube、Findbugs

Android Studio、Harmony DT

Proguard/R8、Baksmali

主要问题

分析结果的准确性依赖于框架的支持、语言特性分析等,类似于 IDE。想实现 100% 的准确性不太可能,适用度高,成本相对低。

依赖于 Gradle 的版本,需要考虑版本兼容性问题。官方文档较少,需要结合 ADT 中的 Gradle 源码。

由于过程和结果产出物,已经是优化的结果,想要 100% 复原是不可能的。

也因此,根据不同的情况下,我们可以划分不同的分析方式也治理手段,诸如于:

  • 变体少或者变体的变更少。通过静态代码分析就可以完成,再结合 Android Lint,而需要注意的是 Android 的代码有 Kotlin、Java、C 、AIDL 等,而像 Harmony OS 的应用,则还会有 JavaScript 和 Harmony IDL 的存在。
  • 变体多的情况。考虑结合中间表示 构建工具来完成对于依赖的架构治理等。

基于构建工具分析

在上述的三种分析模式里,只有基于构建工具分析是在架构治理这一系列文章新出现的。主要是 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 的规范化,我们可以看出:将工具内建到开发流程中,才能使得我们的架构约束更有效果。然而,内建的工具往往可以非常容易被注释掉。这也是另外一个有意思的地方:当人们过分考虑短期的利益时,所有的长期性治理,就需要说明他的价值。

0 人点赞