
APK
的诞生
上述以前在其余文章里面也常见的图,而这张图讲述一个
APK
的诞生流程,能够分为如下的几个流程html
-
aapt
工具将资源文件转化出对应的R
文件和编译好的文件, 可是这类资源文件中不包含assets
目录下的文件。 -
aidl
工具将aidl
文件转化为Java
代码。 -
Java Compiler
工具将上述二者以及咱们书写好的源代码进行整合生成咱们所熟悉的Class
文件。 -
dex
工具将会将第三方库和Class
文件转化二进制dex
文件。 -
apkbuilder
工具将编译好的资源文件、源码的二进制文件以及assets
下的资源文件内联最后生成咱们见到的apk
文件。 -
signer
工具用于签名使用(由于签名工具并不局限于Jarsigner
) -
zipalign
工具帮助优化资源索引。
aapt
/ 资源编译阶段
aapt
工具位于Android
目录下的build-tools
中java具体使用方法请参考AAPT2[1]android
AAPT2
支持编译res
文件目录下的资源。调用AAPT2
进行编译时,每次调用都应传递一个资源文件做为输入。而后,AAPT2
会解析该文件并生成一个扩展名为.flat
的中间二进制文件。而这个二进制文件就对应着图中的Compiled Resources
。git
aapt2 compile project_root/module_root/src/main/res/values-en/strings.xml -o compiled/
上面的我的感受比较坑的地方,
compile
后面跟着的路径必须是完整的,但而-o
后面的又用的相对路径。最后的结果就是如上面所说会生成一个以.flat
为后缀的二进制文件。github
下面给出各类不一样文件类型下将会获得的输出:web
输入 | 输出 |
---|---|
XML 资源文件(如 String 和 Style),它们位于 res/values/ 目录下。 | 以 *.arsc.flat 做为扩展名的资源表。 |
其余全部资源文件 | 除 res/values/ 目录下的文件之外的其余全部文件都将转换为扩展名为 *.flat 的二进制 XML 文件。此外,默认状况下,全部 PNG 文件都会被压缩,并采用 *.png.flat 扩展名。若是选择不压缩 PNG,您能够在编译期间使用 --no-crunch 选项。 |
另外aapt
工具的link
连接功能还会生成咱们一个R
的文件用于资源的惟一标示。算法
aapt2 link path-to-input-files [options] -o outputdirectory/outputfilename.apk
--manifest AndroidManifest.xml
经过定向的连接的能够实现增量连接的效果。spring
Android Studio
自带工具,点开APK
就可以直接解析。后端
咱们能够将整个
int
数值分为4个字节:缓存
-
第一位字节 0x7f
表示packageID
,用来限定资源的来源。应用资源是0x7f
,系统资源是0x01
-
次一位字节 01
表示typeID
,用来表示资源类型,如drawable
、layout
、menu
等,下一个资源的typeID
则会是02
-
后2字节 0000
指的是每个资源在对应的typeID
中出现的顺序。(给出的存储空间范围比较大)
可是咱们在APK
解析的文件中会找到这样的一个文件resources.arsc
,这个文件的生成一样伴随aapt
的连接而来。
抛去纯数值的文件不讲,着重看一下layout
文件可以发现v17
、watch-v20
。。其实为布局显示时留出了不一样版本选择空间,若是你再看一下mipmap
或者drawable
还会为不一样的屏幕尺寸留出了选择的余地。
Q1:R.java和resources.arsc文件做用是什么?
A1:resources.arsc
为应用程序在运行时同时支持不一样大小、密度的屏幕以及不一样语言等提供可能。R
文件为资源设置了惟一标示,从而可让应用程序可以根据设备的当前配置信息来快速索引到匹配资源。
Java Compile + Dex
/ 代码编译
项目中其实咱们更多时候已经用Android Studio
提供的Build
功能完成了,而这同样的能力提供方就包括Gradle
。
Gradle
是干吗用的?
在 关于Python的小小分享[2]曾分享过这样一张图。其实
Gradle
的其中一项能力就是为咱们提供不一样三方库之间的依赖关系,而基础就是Java
,因此在Build
的这样过程当中咱们常常会看到相似这样的一个Task
。
在正式接触Gradle
的打包流程以前有必要了解一下什么是Gradle
,先看下面的一段xml
文件内容。
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</dependency>
</dependencies>
若是你曾经作事后端开发,那么开发中确定是常常接触到这样的xml
文件编写,但这个文件格式书写辨识能力来自于与他具备相同能力的同伴maven
。
dependencies {
compile('org.springframework:spring-context:2.5.6')
testCompile('junit:junit:4.7')
}
而像Gradle
是基于本身定义的语法来完成依赖解析,呈现方式上更是一目了然。那说到这里我仍是没去介绍Gradle
这个工具他的做用究竟是什么?往简单了说,就是一个项目自动构建工具呗。可是这样的一个工具在咱们的开发过程当中到底占到怎么一个不可或缺的位置呢?咱们来纵观一下一个apk
的打包流程就能够直到他干了什么事情了。在写代码的时候咱们关注点是什么?一般会有如下几类:
-
源代码文件:包括 Kotlin
、Java
、C
、AIDL
等等文件。 -
资源文件:图片、视频、布局等等文件。 -
R
文件,各种资源的惟一标识。
完成以上内容的编写,咱们可能结束了代码编写,而后用了一下Android Studio
中提供的各项能力。若是不出所料,你的项目就飞快的在你的测试机上开始开心的运行了。
可能到这里你尚未感受,但若是看了这张图呢?是否能切实的感觉到
Gradle
所提供的强大能力了,由于对咱们咱们开发者而言其实只干了一件运行按钮的触发操做,可是背后Gradle
给咱们所带来的收益是无穷无尽的。
在这里咱们知道他颇有用了,可是为何还要提一下他的兄弟Maven
呢?主要是为了让你转化手头的构建工具,根据官网的构建速度对比。
具体请参考文档Gradle vs Maven:性能比较[3]

由于公司里通常的项目都是组件化的,并且接入方会不少不少不少,因此咱们拿一个大型构建的时间对比可能更服人心。对于干净的构建,Gradle的速度提升了2-3倍,对于增量更改,Gradle的速度提升了约7倍,而对Gradle任务输出进行缓存时,Gradle的速度提升了3倍。 如此之高的构建效率提高对咱们开发者而言确定也是有利有“弊”的,好比说我做为一个抖音开发者,本来抖音的构建工具使用的是Maven
他的增量编译构建速度本来20分钟完成一次,那说明我如今有20分钟的摸鱼时间了,可是若是我一天要编译10次20次呢?整体这样折算下来一天的工做效率能够说骨折式缩短,可能由于编译效率太低,致使你没法按时完成需求年终奖一无全部。可是用了Gradle
之后,效率翻倍,每次增量编译只用10分钟就完成了,虽然摸鱼时间短了,可是效率上来了,老板说你表现优异又给你加了3个月的奖金。
回归这个主题的内容,Gradle
是怎么为咱们提供能力的?
Proguard
+ Dex
Dex
工具就是将Class
文件转换成二进制这里就不作介绍
在关于proguard
的内容上,对于8成的开发者阮大概最熟悉的内容就是混淆了。
Q1:混淆带给咱们的好处有什么?
A1: 为何咱们要混淆?很简单,不想让第三者轻易得到咱们开发的app
源码,那他的第一个优点就出来了,让代码失去直观的语义,让一部分想窃取公司机密的外部业余黑客望而却步。其实这个工具还给咱们带来了第二个优点,就是代码内容缩短,在总体的包体积缩小起到了相当重要的做用。
那Proguard
只有这么点做用吗??显然并非这样的。从图中能够得知,
Proguard
针对的部分是抛去系统库的,因此在混淆的图中可以发现android.support
的库仍是清晰的显示着,我的考虑是由于若是加上系统库进行混淆的话,可能引来奇怪的Bug
。
咱们将总体分为4个部分:
-
shrink
—— 代码删减 -
optimize
—— 指令优化 -
obfuscate
—— 代码混淆 -
preverify
—— 代码校验
Shrink
做为代码删减确定是有删减的入口的。ProGuard
会根据Configuration Roots
开始标记, 同时根据Roots
为入口开始发散。标记完成之后, 删除未被标记的类或成员。最终获得的是精简的ClassPool
。
Q1:那这些Roots
的来源是什么呢?
A1:Roots
包括类,方法字段,方法指令, 来源主要有2种。
-
经过 keep
同时allowshrinking
不为true
。计算class_specification
中类限定和限定成员 -
经过 keepclasseswithmembers
关键字allowshrinking
不为true
。若是类限定和成员限定都存在。计算class_specification
中类限定和成员限定。
Q2:删除的是那些代码?
A2: 其实删除的内容就是在全局范围内并无调用点而且没有用keep
去保留的方法或者类。
Optimize
Optimize
会在该阶段经过对 代码指令、 堆栈, 局部变量以及数据流分析。来模拟程序运行中尽量出现的状况来优化和简化代码. 为了数据流分析的须要Optimize
会屡次遍历全部字节码ProGuard
会开启多线程来加快速度。
具体的优化策略详见于ProGuard 初探的 Optimize 部分[4]
Obfuscate
代码混淆想来是咱们最为常见的部分了。混淆部分一共会带来两部分的收益:
-
代码失去直观的语义(由于咱们的方法或者函数命名时都会有必定的规则) -
代码内容缩短,缩小总体的包体积
Preverify
对代码进行预校验。 主要校验StackMap / StackMapTable
属性。android
虚拟机字节码校验不基于StackMap /StackMapTable
。
具体内容详见于 ProGuard 初探[5]
D8
是
Dex
的替代产品
这一解析器的引入很是重要的目的是为了适应
Java 8
上新概念Lambda
。Java
底层是经过invokedynamic
指令来实现,因为Dalvik/ART
并无支持invokedynamic
指令或者对应的替代功能。简单的来讲,就是Android
的dex
编译器不支持invokedynamic
指令,致使Android
不能直接支持Java 8
。
因此Android
作的事情就是间接支持,将Lambda
变化为能够解析的语法而后执行。将代码编译之后,咱们可以发现生成的代码中会同时生成以
Lambda
来标识的类,这就是说明了他的解析方案,而代码的实现方式就是咱们在Java 7
中常见的方案了。
不过你以为新产品的提高会止步于此吗?🤫
-
编译速度的提高 -
编译产生的 dex
文件体积缩小
R8
是
Proguard
+Dex
的替代产品
R8
中包含了D8
+R8
R8
做为Proguard
的替代产品,继承了原有的功能而且作出了拓展。那在
R8
这个工具上,开发者又作出了什么样的突破呢?从图中可以比较直观地看到,R8做为集成物,将
ProGuard
+Dex
的能力集成,不只在编译效率上提高,而且包大小的体积也有必定的收益
apkbuilder
的话就是一个集成工具了不作讲解了
签名
为何Android
的程序须要签名呢?是否常常遇到这样的状况,同一个项目两个台机器上运行到同一部手机中,咱们常常会碰到关于签名不一样的报错。而后咱们的作法可能就是删除,而后从新安装,这样就能解决问题了,但其实致使这个问题的缘由是签名,若是两台机器使用了一样的签名,这个问题就自动解除了。
签名为咱们带来了什么样的好处呢?
-
使用特殊的key签名能够获取到一些不一样的权限 -
验证数据保证不被篡改,防止应用被恶意的第三方覆盖
经过
Android Studio
的Generate Signed Bundle or APK
方法能够看到上述的两种签名的方法:Jar Signature
和Full APK Signature
,那这两种签名方式又有什么区别呢?
Jar Signature
/ v1
签名经过
Jar Signature
在APK
的表现形式又是怎么样的呢?v1签名过程很简单,一共分为了三个部分:
-
对非目录文件以及过滤文件进行摘要,存储在 MANIFEST.MF
文件中。 -
对 MANIFEST.MF
文件的进行摘要以及对MANIFEST.MF
文件的每一个条目内容进行摘要,存储在CERT.SF
文件中。 -
使用指定的私钥对 CERT.SF
文件计算签名,而后将签名以及包含公钥信息的数字证书写入CERT.RSA
。
从这个实现流程上其实可以明显感受出来这个签名模式确定是存在问题的,由于最后的签名数据至关于说向外暴露了。只要稍微注意一下数据就可以把一个APK
反编译改完之后再编译回来。
Full APK Signature
/ v2
咱们知道了Jar Signature
的签名方式,那如今这个新的签名方式又是如何实现的呢?
APK
签名方案v2
是一种全文件签名方案,该方案可以发现对APK
的受保护部分进行的全部更改,从而有助于加快验证速度并加强完整性保证。
使用APK
签名方案v2
进行签名时,会在APK
文件中插入一个APK
签名分块,该分块位于“ZIP
中央目录”部分以前并紧邻该部分。在“APK
签名分块”内,v2
签名和签名者身份信息会存储在APK
签名方案v2
分块中。

APK 签名方案 v2 验证

-
找到“APK 签名分块”并验证如下内容: -
“APK 签名分块”的两个大小字段包含相同的值。 -
“ZIP 中央目录结尾”紧跟在“ZIP 中央目录”记录后面。 -
“ZIP 中央目录结尾”以后没有任何数据。 -
找到“APK 签名分块”中的第一个“APK 签名方案 v2 分块”。若是 v2 分块存在,则继续执行第 3 步。不然,回退至使用 v1 方案验证 APK。 -
对“APK 签名方案 v2 分块”中的每一个 signer 执行如下操做: -
从 signatures 中选择安全系数最高的受支持 signature algorithm ID。安全系数排序取决于各个实现/平台版本。 -
使用 public key 并对照 signed data 验证 signatures 中对应的 signature。(如今能够安全地解析 signed data 了。) -
验证 digests 和 signatures 中的签名算法 ID 列表(有序列表)是否相同。(这是为了防止删除/添加签名。) -
使用签名算法所用的同一种摘要算法计算 APK 内容的摘要。 -
验证计算出的摘要是否与 digests 中对应的 digest 一致。 -
验证 certificates 中第一个 certificate 的 SubjectPublicKeyInfo 是否与 public key 相同。 -
若是找到了至少一个 signer,而且对于每一个找到的 signer,第 3 步都取得了成功,APK 验证将会成功。
那问题来了,这个这个v2
的整块数据是如何计算出来的呢?
v2的详细计算过程请见于 APK 签名方案 v2 分块[6]

-
每一个部分都会被拆分红多个大小为 1MB 的连续块。每一个部分的最后一个块可能会短一些。 -
每一个块的摘要均经过字节 0xa5 + 块的长度 + 块的内容进行计算。 -
顶级摘要经过字节 0x5a + 块数 + 块的摘要的链接进行计算。
摘要以分块方式计算,以便经过并行处理来加快计算速度。
v3(Android 9 及更高版本)
v3新版本签名中加入了证书的旋转校验,便可以在一次的升级安装中使用新的证书,新的私钥来签名APK。固然这个新的证书是须要老证书来保证的,相似一个证书链。
详细内容见于:Android P v3签名新特性[7]
v4(Android 11)
此方案会在单独的文件 (apk-name.apk.idsig) 中生成一种新的签名,但在其余方面与 v2 和 v3 相似。没有对 APK 进行任何更改。此方案支持 ADB 增量 APK 安装。设备上安装大型(2GB 以上)APK 可能须要很长的时间,ADB(Android 调试桥)增量 APK 安装能够安装足够的 APK 以启动应用,同时在后台流式传输剩余数据,从而加快 APK 安装速度。
zipalign
zipalign
是一种归档对齐工具,可对 Android 应用 (APK) 文件提供重要的优化。 其目的是要确保全部未压缩数据的开头均相对于文件开头部分执行特定的对齐。具体来讲,它会使 APK 中的全部未压缩数据(例如图片或原始文件)在 4 字节边界上对齐。
使用时间点
必须在应用构建过程当中的两个特定时间点之一使用 zipalign,具体在哪一个时间点使用,取决于所使用的应用签名工具:
-
若是使用的是 jarsigner,则只能在为 APK 文件签名以后执行 zipalign。 -
若是使用的是 apksigner,则只能在为 APK 文件签名以前执行 zipalign。若是您在使用 apksigner 为 APK 签名以后对 APK 作出了进一步更改,签名便会失效。

自此,一个能够运行的APK
就诞生了。
APK
运行在Android
手机上
既然咱们要开始在手机上运行了,那基本还要用上adb
的工具了,这里温习一个安装的命令adb install <dir>/XXXX.apk
在
Android
里咱们须要了解的的就是Dalvik
和ART
两个虚拟机了。
可是咱们得先了解一下为何当年在有JVM
的状况下,还要本身造出一个DVM
来知足需求呢?先思考一个问题,为何
Android
程序明明是用Java
写的,可以直接在JVM
上运行,还要本身再写一个DVM
呢??
可能不少文章都这样说,由于经过JVM
来运行,虽然可以一份代码处处跑,可是显然从性能上跟不上直接经过寄存器来完成全部的数据操做的。可是我以前据说过一个故事,是谷歌被Oracle
限制了JVM
的使用😵 , 因此才造了一个DVM
。而后效果又比用JVM
好,就开始流行起来了。
那为何JVM
会比DVM
运行起来慢呢?
JVM | DVM |
---|---|
基于栈开发 | 基于寄存器开发 |
java文件 | dex文件 |
按需加载 | 一次性加载 |
在没有引入multiDex
以前的DVM
是一次性加载,可能加载速度上会比JVM
慢,可是加载完毕之后,总体效率高,这基于的是几个方面:
-
按需加载,致使加载不够实时。 -
基于栈开发,对应的二进制指令更加复杂。
既然Davlik
听起来已经这么好了,为何还要再开发一套ART
的虚拟机呢?
其实他的优化角度有这几个层面:
-
采用AOT(Ahead-Of-Time,预编译)编译技术,它能将Java字节码直接转换成目标机器的机器码。 -
更为高效和细粒度的垃圾回收机制(GC)。
AOT(Ahead-Of-Time,预编译)编译技术

JIT(Just in Time)
运行时进行字节码到本地机器码的编译缺点:
-
每次启动应用都须要从新编译 -
运行时比较耗电(由于常常有编译的过程)
AOT(Ahead of Time)
在应用安装时就将字节码编译成本地机器码缺点:
-
应用安装和系统升级以后的应用优化比较耗时(从新编译,把程序代码转换成机器语言) -
优化后的文件会占用额外的存储空间(缓存转换结果)
JIT + AOT

为何要出现这样的方案呢?其实咱们看无论是单纯的JIT
或是AOT
方案都有本身的优缺点,为何这么说呢。
这是一个流量的时代,而一个安装包的体积大小、安装时间常常就会成为用户安装时的软肋,缘由见于 App竞品技术分析 (3)减少安装包的体积[8]。这就体现了JIT
方案的优点,由于安装时没有了编译的过程,安装速度相比较而言就更快。可是运行后呢?JIT
的优点就断崖式降低了,这个时候有AOT
的话,可以再下一次启动时来加速咱们的程序执行效率,可是AOT
的触发条件是什么?
当手机长期处于空闲或者充电状态的时候,系统才会进行执行 AOT 过程进行编译,生成的机器码缓存为文件,因此说这个AOT
在无人干预的状况下是一个很是不可控的过程。
更为高效和细粒度的垃圾回收机制(GC)
关于GC
又能够分为这样的几个层面:
-
内存分配器 -
垃圾回收算法 -
超大对象存储空间的支持 -
Moving GC策略 -
GC调度策略的多样性
这里咱们只对GC
垃圾回收算法作一个讲解。首先咱们先作一个回顾,在关于JVM,你必须知道的那些玩意儿[9] 中我曾经提到过关于JVM
内的三种垃圾回收算法,复制收集、标记清理、标记整理三种算法,但对于JVM
而言是有将堆区经过本身的规则总体成一个生命周期。而后他与会有不少不少的垃圾回收器,好比说Serial收集器、ParNew收集器、G1回收器。。。。
但那是对于JVM
而言的,而DVM
的出场姿式又是什么样的呢?
对于DVM
而言,很简单的处理方式就是和最开始的 JVM 垃圾收集器同样Stop The World
,而后套上本身的清理算法,先标记使用中的数据,再把无用数据清理掉。这也就致使了用户体验到了难以用语言描述的卡顿感。
而ART
是如何在保持着Stop The World
的观念的同时又提升了性能的呢?ART
须要垃圾收集器作的工做,拆分给应用程序自己完成,这一项任务其实就是标记了。这里作一个盲猜,ART
的实现应该是经过添加了相似于使用标记位的东西,经过不断更新这个值,等须要进行清理时,数据的标识其实已经处于一个完备的状态了,可能麻烦的问题就在于这个标记位的设定了。对于清理过程的减负,Google
又引入了一项名叫packard pre-cleaning
预清理的技术来减轻须要GC
的数量来提升效率。
详细见于 Android 5.0 ART GC 对比 Android 4.x Dalvik GC[10]
参考资料
-
Android 兼容 Java 8 语法特性的原理分析 [11] -
缩减、混淆处理和优化应用 [12] -
也谈Android签名机制 [13] -
APK 签名方案 v2 [14] -
Android P v3签名新特性 [15]
参考资料
AAPT2: https://developer.android.com/studio/command-line/aapt2
[2]关于Python的小小分享: https://juejin.im/post/6854573208750948359
[3]Gradle vs Maven:性能比较: https://gradle.org/gradle-vs-maven-performance/
[4]ProGuard 初探的 Optimize 部分: https://www.jianshu.com/p/4278862ef7e7
[5]ProGuard 初探: https://www.jianshu.com/p/4278862ef7e7
[6]APK 签名方案 v2 分块: https://source.android.com/security/apksigning/v2#apk-signature-scheme-v2-block
[7]Android P v3签名新特性: https://xuanxuanblingbling.github.io/ctf/android/2018/12/30/signature/
[8]App竞品技术分析 (3)减少安装包的体积: https://blog.csdn.net/jspandasp/article/details/49339403
[9]关于JVM,你必须知道的那些玩意儿: https://juejin.im/post/6844904183909318664#heading-18
[10]Android 5.0 ART GC 对比 Android 4.x Dalvik GC: https://blog.csdn.net/hello2mao/article/details/42361755
[11]Android 兼容 Java 8 语法特性的原理分析: https://tech.meituan.com/2019/10/17/android-java-8.html
[12]缩减、混淆处理和优化应用: https://developer.android.com/studio/build/shrink-code?hl=zh-cn
[13]也谈Android签名机制: https://www.jianshu.com/p/a5af970ca1db
[14]APK 签名方案 v2: https://source.android.com/security/apksigning/v2#integrity-protected-contents
[15]Android P v3签名新特性: https://xuanxuanblingbling.github.io/ctf/android/2018/12/30/signature/
本文分享自微信公众号 - 告物(ClericYi_Android)。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。