Java Compile + Dex
/ 代码编译
项目中其实我们更多时候已经用Android Studio
提供的Build
功能完成了,而这一样的能力提供方就包括Gradle
。
Gradle
是干嘛用的?
在 关于Python的小小分享曾分享过这样一张图。
其实Gradle
的其中一项能力就是为我们提供不同三方库之间的依赖关系,而基础就是Java
,所以在Build
的这样过程中我们经常会看到类似这样的一个Task
。
在正式接触Gradle
的打包流程之前有必要了解一下什么是Gradle
,先看下面的一段xml
文件内容。
如果你曾经做过后端开发,那么开发中肯定是经常接触到这样的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:性能比较
因为公司里一般的项目都是组件化的,而且接入方会很多很多很多,所以我们拿一个大型构建的时间对比可能更服人心。对于干净的构建,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 部分
Obfuscate
代码混淆想来是我们最为常见的部分了。
混淆部分一共会带来两部分的收益:
- 代码失去直观的语义(因为我们的方法或者函数命名时都会有一定的规则)
- 代码内容缩短,缩小整体的包体积
Preverify
对代码进行预校验。 主要校验StackMap / StackMapTable
属性。android
虚拟机字节码校验不基于StackMap /StackMapTable
。
具体内容详见于 ProGuard 初探
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
的表现形式又是怎么样的呢](https://upload-images.jianshu.io/upload_images/24244313-66bc0461ee974372.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
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 分块
[图片上传中…(image-4a28ef-1600953978232-9)]
- 每个部分都会被拆分成多个大小为 1MB 的连续块。每个部分的最后一个块可能会短一些。
- 每个块的摘要均通过字节 0xa5 + 块的长度 + 块的内容进行计算。
- 顶级摘要通过字节 0x5a + 块数 + 块的摘要的连接进行计算。
摘要以分块方式计算,以便通过并行处理来加快计算速度。
v3(Android 9 及更高版本)
v3新版本签名中加入了证书的旋转校验,即可以在一次的升级安装中使用新的证书,新的私钥来签名APK。当然这个新的证书是需要老证书来保证的,类似一个证书链。
详细内容见于:Android P v3签名新特性
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文件 |
学习宝典
对我们开发者来说,一定要打好基础,随时准备战斗。不论寒冬是否到来,都要把自己的技术做精做深。虽然目前移动端的招聘量确实变少了,但中高端的职位还是很多的,这说明行业只是变得成熟规范起来了。竞争越激烈,产品质量与留存就变得更加重要,我们进入了技术赋能业务的时代。
不论遇到什么困难,都不应该成为我们放弃的理由!
很多人在刚接触这个行业的时候或者是在遇到瓶颈期的时候,总会遇到一些问题,比如学了一段时间感觉没有方向感,不知道该从那里入手去学习,对此我针对Android程序员,我这边给大家整理了一套学习宝典!包括不限于高级UI、性能优化、移动架构师、NDK、混合式开发(ReactNative+Weex)微信小程序、Flutter等全方面的Android进阶实践技术;希望能帮助到大家,也节省大家在网上搜索资料的时间来学习,也可以分享动态给身边好友一起学习!
【Android核心高级技术PDF文档,BAT大厂面试真题解析】
【算法合集】
【延伸Android必备知识点】
【Android部分高级架构视频学习资源】
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门,即可获取!
【Android核心高级技术PDF文档,BAT大厂面试真题解析】
[外链图片转存中…(img-zhpgLfSg-1715287849087)]
【算法合集】
[外链图片转存中…(img-yL767zsC-1715287849088)]
【延伸Android必备知识点】
[外链图片转存中…(img-2wyNJb7V-1715287849090)]
【Android部分高级架构视频学习资源】
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门,即可获取!