阅读 7611

深入探索编译插桩技术(一、编译基础)

前言

成为一名优秀的Android开发,需要一份完备的知识体系,在这里,让我们一起成长为自己所想的那样~。

现如今,Gradle + 编译插桩 的应用场景越来越多,无论是 各种性能优化中的插件工具制作,还是用来支持 插件化、热修复的各种插件,都会使用到这个组合,因此,掌握 Gradle + 编译插桩 技术能够大大提升我们的技术竞争力。从本篇开始,笔者将会与大家一起深入探索编译插桩技术,关于整个编译插桩技术研究系列的大纲如下所示:

思维导图大纲

通常来说,整个《深入探索编译插桩技术系列》到第四篇 ASM 也就结束了,但是 ReDex 的功能实在是太强大了,以至于我不得不多花两篇的篇幅来进行深入讲解。需要注意的是,Dex 字节码与 ReDex 的实现基本是以 C/C++ 语言为主,而且其实现较为复杂,所以我会在全部更新完 Awesome-Android-NDK 的 一至四 部分之后,才会让大家和我一起去深入研究 ReDex 的实现机制

众所周知,编译 主要分为 词法分析、语法分析 、语义检查和代码优化 等步骤。额。。等等,别慌,这篇文章并不是要讲编译原理,对于绝大多数的 Android 开发来说,我们能将 App 的编译和打包流程理解清楚就 OK 了。因此,我们这篇文章主要讲的是 App 的编译过程,本篇包含的主要内容如下所示:

  • 1、App的编译和打包流程
  • 2、编译提速
  • 3、广义的编译-CI
  • 4、总结

本篇文章可能是从现在到今年年底 最简单的一篇文章 了。放轻松,好好享受,后面可能就。。。

好了,下面,我们就先回顾下 App 的编译和打包过程。

一、App 的编译和打包流程

1、APK 的组成

我们都知道,APK 其实是一个 zip 类型的压缩包,而一个典型的 APK 通常都会包含了以下七部分的内容

  • 1、AndroidManifest.xml:如果 App 是一本书,那么这个文件就是它的 “封面” 和 “目录” 。它记载了 App 的名称、权限声明、所包含的组件等一系列信息
  • 2、classes.dex:它是由项目源码生成的 .class 文件经过进一步地转换而生成的 Android 系统可识别的 Dalvik Byte Code。并且,由于 Android 系统中的字节码和标准 JVM 中的字节码是有区别的,所以如果 App 中引用了第三方 jar 包的话,那么通常情况下它也会被包含在 classes.dex 中
  • 3、resources.arsc:资源索引表,包含编译后的二进制资源文件。每当在 res 文件夹下放一个文件时,aapt 就会自动生成对应的 id 并保存在 .R 文件中,但 .R 文件仅仅只是保证编译程序不会报错,实际上在应用运行时,系统会根据 ID 寻找对应的资源路径,而 resources.arsc 文件就是用来记录这些 ID 和 资源文件位置对应关系 的文件
  • 4、res 目录:未编译的资源文件
  • 5、asserts:额外建立的资源文件夹resassets 的不同在于 res 目录下的文件会在 .R 文件中生成对应的资源 ID,而 assets 不会自动生成对应的 ID,而是通过 AssetManager 类的接口来获取。
  • 6、libs 目录:如果存在的话,存放的是 ndk 编出来的 so 库
  • 7、META-INF 目录:用于保存 App 的签名和校验信息,以保证程序的完整性。当生成 APK 包时,系统会对包中的所有内容做一次校验,然后将结果保存在这里。而手机在安装这一 App 时还会对内容再做一次校验,并和 META-INF 中的值进行比较,以避免 APK 被恶意篡改。其中包含如下 三个文件,如下所示:
    • 1)、MANIFEST.MF:其中每一个资源文件都有一个对应的 SHA-256-Digest(SHA1) 签名,MANIFEST.MF 文件的 SHA256(SHA1) 经过 base64 编码的结果即为 CERT.SF 中的 SHA256(SHA1)-Digest-Manifest 值。
    • 2)、CERT.SF:除了开头处定义的 SHA256(SHA1)-Digest-Manifest 值,后面几项的值是对 MANIFEST.MF 文件中的每项再次 SHA256(SHA1) 经过 base64 编码后的值。
    • 3)、CERT.RSA:其中包含了公钥、加密算法等信息。首先,对前一步生成的 CERT.SF 使用了 SHA256(SHA1)生成了数字摘要并使用了 RSA 加密,接着,利用了开发者私钥进行签名。然后,在安装时使用公钥解密。最后,将其与未加密的摘要信息(MANIFEST.MF文件)进行对比,如果相符,则表明内容没有被修改。

接下来,我们来看看 App 的编译和打包过程。

2、APK 的编译打包流程

早在 深入探索 Android 包体积优化(匠心制作) 一文中我们就探讨过打包的部分流程,这里我们需要更加全面地了解下。Android 官方的编译打包流程图如下所示:

为了 了解更多打包过程中的细节,我们需要查看更加详细的旧版 APK 打包流程图 ,如下图所示:

打包流程可简述为如下 八个步骤

  • 1、首先,.aidl(Android Interface Description Language)文件需要通过 aidl 工具转换成编译器能够处理的 Java 接口文件
  • 2、同时,资源文件(包括 AndroidManifest.xml、布局文件、各种 xml 资源等等)将被 AAPT(Asset Packaging Tool)(Android Gradle Plugin 3.0.0 及之后使用 AAPT2 替代了 AAPT)处理为最终的 resources.arsc,并生成 R.java 文件以保证源码编写时可以方便地访问到这些资源
  • 3、然后,通过 Java Compiler 编译 R.java、Java 接口文件、Java 源文件,最终它们会统一被编译成 .class 文件
  • 4、因为 .class 并不是 Android 系统所能识别的格式,所以还需要通过 dex 工具将它们转化为相应的 Dalvik 字节码(包含压缩常量池以及清除冗余信息等工作)。这个过程中还会加入应用所依赖的所有 “第三方库”
  • 5、下一步,通过 ApkBuilder 工具将资源文件、DEX 文件打包生成 APK 文件
  • 6、接着,系统将上面生成的 DEX、资源包以及其它资源通过 apkbuilder 生成初始的 APK 文件包
  • 7、然后,通过签名工具 Jarsigner 或者其它签名工具对 APK 进行签名得到签名后的 APK。如果是在 Debug 模式下,签名所用的 keystore 是系统自带的默认值,否则我们需要提供自己的私钥以完成签名过程
  • 8、最后,如果是正式版的 APK,还会利用 ZipAlign 工具进行对齐处理,以提高程序的加载和运行速度。而对齐的过程就是将 APK 文件中所有的资源文件距离文件的起始位置都偏移4字节的整数倍,这样通过 mmap 访问 APK 文件的速度会更快,并且会减少其在设备上运行时的内存占用

至此,我们已经了解了整个 APK 编译和打包的流程。

那么,为什么 XML 资源文件要从文本格式编译成二进制格式?

主要基于以下 两点原因

  • 1、空间占用更小因为所有 XML 元素的标签、属性名称、属性值和内容所涉及到的字符串都会被统一收集到一个字符串资源池中,并且会去重。有了这个字符串资源池,原来使用字符串的地方就会被替换成一个索引到字符串资源池的整数值,从而可以减少文件的大小

  • 2、解析效率更高二进制格式的 XML 文件解析速度更快。这是由于二进制格式的 XML 元素里面不再包含有字符串值,因此就避免了进行字符串解析,从而提高了解析效率

而 Android 资源管理框架又是如何快速定位到最匹配资源的?

主要基于两个文件,如下所示:

  • 1、资源 ID 文件 R.java赋予每一个非 assets 资源一个 ID 值,这些 ID 值以常量的形式定义在 R.java 文件中
  • 2、资源索引表 resources.arsc用来描述那些具有 ID 值的资源的配置信息

除此之外,APK 的签名也是至关重要的,那么,其签名算法的实现原理是怎样的呢?下面我们就来了解下 APK 签名算法的实现原理。

3、签名算法的原理

什么是签名?

在 Apk 中写入一个 “指纹”。指纹写入以后,Apk 中有任何修改,都会导致这个指纹无效,Android 系统在安装 Apk 进行签名校验时就会不通过,从而保证了安全性

那么,为什么要签名?

主要有 两点原因,如下所示:

  • 1、确保 Apk 来源的真实性
  • 2、确保 Apk 没有被第三方篡改

在了解 APK 签名的实现之前,我们还必须知道什么是数字摘要。

数字摘要

对一个任意长度的数据,通过一个 Hash 算法计算后,都可以得到一个固定长度的二进制数据,这个数据就称为 “摘要”

在签名和校验的流程之中,应用了许多密码学的知识,这里我们需要先大致了解一下。

Hash(散列算法)的基础原理

Hash 算法就是 将数据(如一段文字)运算变为另一固定长度值。它的特点主要有如下 三点

  • 1、唯一性
  • 2、固定长度比较常用的 Hash 算法有 MD5 和 SHA1,MD5 的长度是128位,SHA1 的长度是160位
  • 3、不可逆性

而常用的 Hash 算法有如下 三种

  • 1、SHA-1在密码学中,SHA-1(安全散列算法1)是一种加密散列函数,它接受输入并产生一个160 位(20 字节)散列值,称为消息摘要
  • 2、MD5MD5 消息摘要算法(英语:MD5 Message-Digest Algorithm),一种被广泛使用的密码散列函数,可以产生出一个128位(16字节)的散列值(hash value),用于确保信息传输完整一致
  • 3、SHA-2名称来自于安全散列算法2(Secure Hash Algorithm 2)的缩写,一种密码散列函数算法标准,其下又可再分为六个不同的算法标准,包括了:SHA-224、SHA-256、SHA-384、SHA-512、SHA-512/224、SHA-512/256

签名和校验的主要过程

签名就是 在摘要的基础上再进行一次加密,对摘要加密后的数据就可以当作数字签名。

签名过程:

签名过程可以细分为 三步,如下所示:

  • 1、计算摘要通过 Hash 算法提取出原始数据的摘要
  • 2、计算签名再通过基于密钥(私钥)的非对称加密算法对提取出的摘要进行加密,加密后的数据就是签名信息
  • 3、写入签名将签名信息写入原始数据的签名区块内

校验过程:

校验过程同样也可以分为 三步,如下:

  • 1、提取摘要首先用同样的 Hash 算法从接收到的数据中提取出摘要
  • 2、解密签名使用发送方的公钥对数字签名进行解密,解密出原始摘要
  • 3、比较摘要如果解密后的数据和提取的摘要一致,则校验通过;如果数据被第三方篡改过,解密后的数据和摘要将会不一致,则校验不通过

那么,我们该如何保证公钥的可靠性呢?答案是 数字证书

数字证书

数字证书是 身份认证机构(Certificate Authority)颁发的,主要包含了以下 六类信息

  • 1、证书颁发机构
  • 2、证书颁发机构签名
  • 3、证书绑定的服务器域名
  • 4、证书版本、有效期
  • 5、签名使用的加密算法(非对称算法,如 RSA)
  • 6、公钥等

接收方收到消息后,需要先向 CA 验证证书的合法性,再进行签名校验

需要注意的是,Apk 的证书通常是自签名的,也就是由开发者自己制作,没有向 CA 机构申请。Android 在安装 Apk 时并没有校验证书本身的合法性,只是从证书中提取公钥和加密算法,这也正是对第三方 Apk 重新签名后,还能够继续在没有安装这个 Apk 的系统中继续安装的原因。

keystore 和证书格式

keystore 文件中包含了 私钥、公钥和数字证书。根据编码不同,keystore 文件分为很多种,Android 使用的是 Java 标准 keystore 格式 JKS(Java Key Storage),所以通过 Android Studio 导出的 keystore 文件是以 .jks 结尾的。

keystore 使用的 证书标准是 X.509,X.509 标准也有多种 编码格式,常用的有两种:pem(Privacy Enhanced Mail)和 der(Distinguished Encoding Rules)jks 使用的是 der 格式,但是,Android 也支持直接使用 pem 格式的证书进行签名

下面,我们了解下两种证书编码格式的区别,如下所示:

  • DER(Distinguished Encoding Rules)二进制格式,所有类型的证书和私钥都可以存储为 der 格式
  • PEM(Privacy Enhanced Mail)base64 编码,内容以-----BEGIN xxx----- 开头,以-----END xxx----- 结尾

jarsigner 和 apksigner 的区别

Android 提供了 两种对 Apk 的签名方式,一种是基于 JAR 的签名方式,另一种是基于 Apk 的签名方式,它们的 主要区别在于使用的签名文件不一样:jarsigner 使用 keystore 文件进行签名;而 apksigner 除了支持使用 keystore 文件进行签名外,还支持直接指定 pem 证书文件和私钥进行签名

在我们签名时,除了要指定 keystore 文件和密码外,也要指定 alias 和 key 的密码,这是为什么呢?

keystore 是一个密钥库,也就是说它可以存储多对密钥和证书,keystore 的密码是用于保护 keystore 本身的,每一对密钥和证书是通过 alias 来区分的。所以 jarsigner 是支持使用多个证书对 Apk 进行签名的,apksigner 也同样支持。

Android Apk V1 验证签名的原理

Android Apk V1 验证签名的过程主要可以分为如下 四步

  • 1、解析出 CERT.RSA 文件中的证书、公钥,解密 CERT.RSA 中的加密数据
  • 2、解密结果和 CERT.SF 的指纹进行对比,保证 CERT.SF 没有被篡改
  • 3、接着,将 CERT.SF 中的内容再和 MANIFEST.MF 中的指纹对比,保证 MANIFEST.MF 文件没有被篡改
  • 4、MANIFEST.MF 中的内容和 APK 所有文件指纹逐一对比,保证 APK 没有被篡改

在整个 App 的编译打包过程中,Gradle 自动化构建工具发挥出了重要作用,而编译速度可是需要我们迫切解决的一大痛点。下面,我们就来看看如何对编译进行提速。

二、编译提速

1、了解 Android Studio 3.0 依赖类型的变化

Android Studio 3.0 之前 共有 六种 依赖方式,如下所示:

  • 1、Compile对所有的 build type 以及 falvors 编译并且打包到 APK
  • 2、Provided对所有的 build type 以及 falvors 只编译,不打包到 APK
  • 3、APK只会打包到 APK,不参与编译,比如引用 jar 中的类或者方法, 编译时就会报错
  • 4、Test compile仅对单元测试的代码和打包的测试 APK 有效,而对 debug 或者 release APK 包无效
  • 5、Debug compile仅对 debug 模式的编译和打包的 debug APK 有效,而对 test 或者 release APK 打包无效
  • 6、Release compile仅对 Release 模式的编译和打包的 Release APK 有效,而对 test 或者 debug APK 打包无效

而在 Android Studio 3.0 之后,新增了两种方式:api 和 implementation。其中 api 完全等同于 compile

api

等同于 compile, 用 api 指令编译,表示 三方库的依赖对 module 是可见的,即等同 app Module 可以使用此三方库依赖。

implementation

特点是 将该依赖隐藏在内部,而不对外部公开。比如在组件化项目中,有一个 app module 和一个 base module,app moudle 引入了 base module。其中 base module 使用 implementation 依赖了 Glide 库,因为 implementation 是内部依赖,所以是无法调用到 Glide 库的功能的。因此 implementation 可 以 对外隐藏不必要的接口,并且,使用它可以有效地 提高编译速度。比如,在组件化项目中一般含有多个 Moudle 模块,如 Module A => Module B => Moudle C, 比如 改动 Moudle C 接口的相关代码,如果使用的是 implementation,这时候编译只需要单独编译 Module B 模块就行,但是如果使用 api 或者旧版本的 compile,由于 Module A 也可以访问到 Moudle C,所以 Module A 部分也需要重新编译。所以,在使用无错的情况下,可以优先使用 implementation

2、现有编译方案

Gradle 的官方方案 Instant Run在 Android Plugin 2.3 之前,它使用了 Multidex 实现。在 Android Plugin 2.3 之后,它使用了 Android 5.0 新增的 Split APK 机制。如下图所示:

image

但是,如果你的应用较大,会有如下四个问题:

  • 1、多进程的限制如果应用存在多进程,热交换和温交换都不能生效。此时,Instant Run 的速度就会降低不少
  • 2、Split APK 安装耗时虽然 Split APK 的安装不会生成 Odex 文件,但是这里依然会进行签名校验和文件拷贝,这可能需要几秒到几十秒
  • 3、Annotation Processor 需全量 javac 的问题在 Gradle 4.6 及之前,如果项目中运用了 Annotation Processor,本次修改以及它依赖的模块都需要全量 javac,这可能会需要几十秒
  • 4、常量需全量 javac 的问题此时,常量池会直接把值编译到其他类中,Gradle 并不知道有哪些类使用了这个常量

阿里的 FreeLine 在大部分情况比 Instant Run 更快,但是,它 牺牲了正确性。因为,为了追求更快的速度,它直接忽略了 Annotation 和常量改变可能带来错误的编译产物。而 Instant Run 作为官方方案,它优先保证了 100% 的正确性

但是,在 Android Studio 3.5 之后,Android 8.0 以后的设备将会使用新的方案 Apply Changes 去代替 Instant Run。而 ApplyChange 采用了跟 InstantRun 不一样的原理来加快 AndroidStudio 部署安装 APK 的流程。下面,我们就来了解下他们之间的区别。

InstantRun

InstantRun 主要解决以下两个问题:

  • 1、减少构建和部署 app 到手机的时间
  • 2、热更新代码改动,无需重启 app 或者 activity

为了实现这两个目标,InstantRun 通过重写 apk 的构建流程往每个类里去注入 Hook(钩子) 来达到类的热替换。关于 InstantRun 详细的实现原理可以看看我之前写的深入探索Android启动速度优化一文。

对于小型的应用,InstantRun 确实很好用,能够节省构建和部署的时间,并且不会出错。但是,对于大型的复杂应用,它会导致更长的构建时间,同时由于 InstantRun 构建过程和正常的 app 构建存在冲突,常常出现让开发者意想不到的错误。AS 开发团队在连续几个大版本中都尝试去解决这些问题,但是效果不理想。

所以基于此,AS 开发者团队 重新设计了底层的架构,推出了 ApplyChangs。和 InstantRun 不同的是,它不会在构建过程中去修改 apk。取而代之,它使用了 Android 8.0(Oreo)上支持的 Runtime Instrumentation 以及更新的设备和模拟器在运行时重定义类

ApplyChanges

对于 运行在 Android 8.0 或者更新版本上的设备和虚拟机Android Studio 现在有 三个按钮 来控制应用程序重启的程度:

  • Run会部署所有的改动并重启应用程序
  • Apply Changes会尝试应用资源和代码的更改,并只重启 Activity, 而不是重启应用程序
  • Apply Code Changes会尝试应用代码的更改,而不重启任何东西

通常只有方法体内部的代码更改才会对 Apply Changes 具有兼容性。而 ApplyChanges 的 实现原理 就是 找出 AndroidStudio 构建出来的 apk 和已经安装到手机设备 apk 的差异。找出差异后,然后将差异发送到手机上执行差异合并。ApplyChanges 的 总体架构 如下图所示:

image

那么,理想的编译方案是怎么样的呢?

3、理想的编译方案

我们可以把安装的 Base APK 作为一个壳 APK,而真正的业务代码都放到 Assets 的 ClassesN.dex 中。该方案需要包含以下 三个优化点

  • 1、免安装可以参考 Tinker 热修复的实现原理,每次只把修改以及依赖的类插入到 pathclassloader 的最前方即可
  • 2、使用 ReDex 源码中的 Oatmeal首次安装运行时 ClassesN.dex 转换成 Odex 很耗时,我们可以使用 ReDex 优化模块下的 Oatmeal,通过它应用可以在 100 ms 内生成一个完全解释执行的 Odex 文件
  • 3、关闭 JIT 优化:使用把修改和依赖的类插入到 pathclassloader 最前方的这种方式在 Android N的混合编译 会遇到一些问题:无论是使用插入 pathlist 还是 parent classloader 的方式,若补丁修改的 class 已经存在于 app image(app image 的作用是记录已经编译好的 “热代码”,并且在启动时一次性把它们加载到缓存,而预先加载是为了代替用时查找以提升应用的性能),它们都是无法通过热补丁更新的。它们在启动 app 时已经加入到 PathClassloader 的 ClassTable 中,因此系统在查找类时会直接使用 dex 中的 class。这时我们需要关闭虚拟机的 JIT 优化,通过 在 AndroidManifest 指定 android:vmSafeMode=“true” 即可。

4、编译速度优化

除了将电脑更换为 Mac Pro 顶配版之外,还有以下方式可以提升编译速度:

  • 1、及时升级 Gradle 和 Build Tools 编译工具链,充分利用谷歌最新的优化成果
  • 2、可以将项目中基本不变的模块拆离出去,使用远端 Cache 的模式保留编译后的缓存,具体的搭建流程可参见 Caching for faster builds
  • 3、使用 Android Gradle Plugin 3.0.0 和更高的版本,因为此时在默认情况下启用 AAPT2,它替代了 AAPT 来编译资源,并实现了资源的增量编译,其中并将资源的编译拆分为了两个步骤:Compile 和 Link。Compile 负责将资源文件编译为二进制格式,Link 则会合并所有已编译的文件,并将它们打包到一个软件包中
  • 4、Android Studio 3.1 开始默认使用 D8 编译器(3.0推出),它取代了之前的 dx 工具,将 .class 文件转换为 Dex 文件,使用它能够提升编译速度和减少生成的 Dex 的大小。此外,在 Android Studio 3.1 开始,你可以在 gradle.properties 开启 R8,它的目标是取代混淆和 D8,对于 D8 和 R8 的详细介绍,具体可以看看我写的深入探索Android包体积优化一文
  • 5、此外,可以尝试将构建系统切换到 Buck 或使用 Flutter 混合开发,Flutter 中的 Hot Reload秒级编译功能比较强大,其堪称为开发者的神兵利器,它能够在快速修改 UI,增加功能,修复bug的情况下,而不需要去重新启动应用,即可看到改动效果
  • 6、最后,Gradle 官方有一个 build-scan 的功能,它可以生成构建期间的详细报告,其中有性能相关的统计,可以用于帮助分析一些耗时的 task

那么,Flutter 的 Hot Reload 的实现原理是什么呢?

在回答这个问题之前,我们必须先了解 Flutter 的编译模式

Flutter 的编译模式

编译模式大体可以分为 两种,如下所示:

  • AOT(Ahead Of Time)编译是在程序运行前就已经编译,因此,在运行时不需要进行分析、编译,因此执行速度更快
  • JIT(Just In Time)编译代码可以在程序执行时期编译,因为要在程序执行前进行分析、编译,JIT 编译可能会导致程序执行时间较慢

而 Flutter 使用了与众不同的编译模式,在开发阶段下,使用了 Kernel Snapshot 模式(对应 JIT 编译),将 dart 代码生成了标记化的源代码,而在运行时编译使用的是解释执行。在 release 阶段,iOS 使用 AOT 编译,编译器将 dart 代码生成汇编代码,最终生成 app.framwork,而 android 使用了 Core JIT 编译,将 dart 转化为二进制模式,并在 VM 启动前载入

因此,在开发阶段的 Kernel Snapshot 编译模式下,Hot Reload 会通过扫描项目文件,将有改动的 dart 文件转化为标记化源代码 kernel files,并发送到正在运行的 DartVM,等待 DartVM 替换资源,然后通知 Flutter Framework 重建、重新布局、重新绘制 WidgetsTree,即可看到改动效果

那么,flutter 又是如何触发 WidgetsTree 的重建呢?

Flutter framework 中 BindingBase 注册了名为 reassemble的Dart VM 服务,用于外部与正在运行的 Dart VM 通信,这样,便能够触发根节点树实现重建操作。当 Hot Reload 导致需重建 WidgetsTree时,reassemble 的 Dart VM 服务就会被触发,触发后,就会由根节点开始一步步实现widgets树重建,其重建流程如下所示:

    ext.flutter.reassemble => BindingBase.reassembleApplication => 
    WidgetsBinding.performReassemble => BuildOwner.reassemble => Element.reassemble
复制代码

三、广义的编译-CI

CI 即 持续集成,在大型开发团队中,CI 的建设是重中之重,CI 主要包括 打包构建、Code Review、代码工程管理、代码扫描 等一系列流程。它的 整套运转体系 可以简化为下图:

image

1、持续集成的原因

构建 CI 的目的主要是为了解决以下四个问题。

1)、项目依赖复杂

随着业务的发展,基础组件库的数量会持续上涨,这个时候组件间的关系就会变得错综复杂,这将会导致如下 两个问题

  • 1、如果某个开发同学需要修改代码,极有可能会影响到其它业务,牵一发而动全身
  • 2、人工维护组件间复杂的依赖关系非常困难

2)、琐碎的研发流程

在日常的功能开发中,我们一般都会经 代码开发、组件发版、组件集成、打包、测试这五个步骤。如果测试发现 Bug 需要进行修复,然后会再次经历代码修改、组件发版、组件集成、打包、测试,直到测试通过交付产品。传统的研发流程如下图所示:

image

可以看到,开发同学在整个开发流程中需要手动提交 MR、升级组件、触发打包以及去实时监控流程的状态,这样肯定会严重影响开发的专注度,降低研发的生产力。

3)、与 App 性能监控体系的融合

随着 App从 项目初期 => 成长期 => 成熟期,对性能的要求会越来越高,为了保障性能的足够稳定,我们需要制造出许多性能监控的工具,以实时监控我们应用的性能。而 App 性能监控体系必须和 CI 结合起来,以实现流程的自动化和平台化。

4)、项目的编译构建速度缓慢

随着 App 的体积变大,依赖变多,项目的编译构建速度会越来越慢,缓慢的编译速度会严重拖垮开发同学的研发效率。因此,提升 App 的编译构建速度刻不容缓。

2、持续集成的主要步骤

持续集成涉及的流程非常多,但是有 两个主要的步骤是非常重要 的,具体如下所示:

1)、代码检查

为了防止不符合规范的代码提交到远程仓库中,我们需要 自定义一套符合自身项目的编码规范,并使用专门的插件来检测。自定义代码检测可以通过完全自己实现或者扩展 Findbugs 插件,例如美团就利用 Findbugs 实现了 Android 漏洞扫描工具 Code Arbiter,其中 FindBugs 是一个静态分析工具,它一般用来检查类或者 JAR 文件,将字节码与一组缺陷模式进行对比来发现可能存在的问题,它可以以独立的 JAR 包形式运行,也可以作为集成开发工具的插件形式而存在。而 FindBugs 插件具有着极强的可扩展性,只需要将扩展的 JAR 包导入 FindBugs 插件,重启 AS,即可完成相关功能的扩展。

在 FindBugs 有一款专门对安全问题进行检测的扩展插件 Find Security Bugs,该插件主要用于对 Web 安全问题进行检测,也有极少对Android相关安全问题的检测规则。我们只需要 定制化自己的 Find Security Bugs,通过增加检测项来检测尽可能多的安全问题,通过优化检测规则来减少检测的误报 即可,这里我们可以直接使用 Android_Code_Arbiter 这个插件,它 去除了其中跟 Android 漏洞无关的漏洞,保留了与 Android 相关的,并增加了其它的一些检测项,以此形成了针对与于 Android 的源码审计工具

此外,我们也可以使用 第三方的代码检查工具,例如收费的 Coverity,以及 Facebook 开源的 Infer

然而,尽管将问题代码扫描出来了,可是还是会有不少开发同学不知道如何修改,对于这种情况,我们可以给在自定义代码扫描工具的时候,对于每一个问题检查项都给出对应的修改方针

最后,我们可以据此建立一个解决项目异常的流程:建立一个服务专门每天跑项目的 Lint 检查,跑完将警告汇总分配到对应的负责人身上,并邮件告知他,直到上线。

2)、Code Review

Code Review 非常重要,在每一次提交代码时,我们都需要自己进行一次 Code Review,然后再让别人去 Review,以建立自身良好的技术品牌。

有些同学可能会认为 CI 并不重要,它好像跟具体的技术并无关联。但是,我们需要知道,学会不仅仅是钻在开发角度看问题,跳脱出来,站在用户角度,站在产品角度,或许会有意外的收获

四、总结

到这里,关于 Android 编译相关的知识就介绍完了。下面,总结一下本篇文章涉及的 三大主题

  • 1、App 的编译和打包流程APK 的编译打包流程、签名算法的原理
  • 2、编译提速了解 Android Studio 3.0 依赖类型的变化、现有编译方案、理想的编译方案、编译速度优化
  • 3、广义的编译-CI持续集成的原因、持续集成的主要步骤

在本篇文章,我们即涉及到了 Android 编译的深度方面:App 的编译和打包流程、签名算法的原理,也涉及到了 Android 编译的广度方面:持续集成。因此,在我们学习的过程中,技术就像是一棵树,在顶部叶子上各个领域看似毫不相干,但是在一个领域越往下深入,各个领域相互交错到的知识或者设计方式就越多,所以技术深度和广度并不是对立面,对技术深度的探索不仅有利于你在特定领域有更深理解,更加可以帮助你轻松切换到另一个领域,特别是像前端的各细分领域的工作,很多领域的知识背后都殊途同归,而技术的广度也不是有的人说的那样不堪,在有技术深度的基础上,去拓展自己的技术广度,其实会让你对原有技术的理解变得更加地深入。

参考链接:


1、极客时间之Android开发高手课《关于编译,你需要了解什么?》

2、《深入理解Android内核设计思想》第20章 Android应用程序的编译和打包

3、Android应用程序资源的编译和打包过程分析

4、Caching for faster builds

5、Hot Reload秒级编译

6、build-scan

7、Code Arbiter

8、Find Security Bugs

9、Infer

10、MCI:移动持续集成在大众点评的实践

11、aapt2官方教程

12、FreeLine

13、Apply Changes

14、Android N的混合编译

很感谢您阅读这篇文章,希望您能将它分享给您的朋友或技术群,这对我意义重大。

希望我们能成为朋友,在 Github掘金上一起分享知识。