[TOC]
## 编译流程
![](https://img.kancloud.cn/c8/ec/c8ec9deb9994f101e7b707ce8a6e06ef_950x1068.png)
1. 通过 aapt 打包 res 资源文件,生成 R.java、resources.arsc 和 res 文件(二进制 & 非二进制如 res/raw 和 pic 保持原样);
2. 处理 .aidl 文件,生成对应的 Java 接口文件;
3. 通过 Java Compiler 编译 R.java、Java 接口文件、Java 源文件,生成 .class 文件;
4. 通过 dex 命令,将 .class 文件和第三方库中的 .class 文件处理生成 classes.dex;
5. 通过 apkbuilder 工具,将 aapt 生成的 resources.arsc 和 res 文件、assets 文件和 classes.dex 一起打包生成 apk;
6. 通过 Jarsigner 工具,对上面的 apk 进行 debug 或 release 签名;
7. 通过 zipalign 工具,将签名后的 apk 进行对齐处理。
看起来我们貌似已经回答出了这个问题的答案,但是今天是来屠龙的,所以我们不能就这么简单的放过这个题目。
## 从gradle Task看编译流程
先贴一段gradle打印task耗时的代码
1. 项目根目录build.gradle打开
2. 加入下面代码
~~~
import java.util.concurrent.TimeUnit
// Log timings per task.
class TimingsListener implements TaskExecutionListener, BuildListener {
private long startTime
private timings = []
@Override
void beforeExecute(Task task) {
startTime = System.nanoTime()
}
@Override
void afterExecute(Task task, TaskState taskState) {
def ms = TimeUnit.MILLISECONDS.convert(System.nanoTime() - startTime, TimeUnit.NANOSECONDS);
timings.add([ms, task.path])
task.project.logger.warn "${task.path} took ${ms}ms"
}
@Override
void buildFinished(BuildResult result) {
println "Task timings:"
for (timing in timings) {
if (timing[0] >= 50) {
printf "%7sms %s\n", timing
}
}
}
@Override
void buildStarted(Gradle gradle) {}
@Override
void projectsEvaluated(Gradle gradle) {}
@Override
void projectsLoaded(Gradle gradle) {}
@Override
void settingsEvaluated(Settings settings) {}
}
gradle.addListener new TimingsListener()
复制代码
~~~
当项目运行完之后会输出类似如下的日志,表示一个run执行之后gradle所执行的task的时间以及任务名。
~~~
1543ms :compiler:kaptGenerateStubsKotlin
144ms :RouterLib:packageDebugResources
1166ms :compiler:kaptKotlin
816ms :compiler:compileKotlin
401ms :compiler:compileJava
65ms :compiler:jar
122ms :app:mergeDebugResources
56ms :EmptyLoader:compileJava
170ms :app:processDebugManifest
171ms :RouterLib:parseDebugLocalResources
60ms :app:checkDebugDuplicateClasses
2416ms :RouterLib:compileDebugKotlin
122ms :RouterLib:compileDebugJavaWithJavac
124ms :secondmoudle:mergeDebugNativeLibs
1185ms :app:processDebugResources
70ms :secondmoudle:kaptGenerateStubsDebugKotlin
202ms :RouterLib:mergeDebugNativeLibs
350ms :secondmoudle:kaptDebugKotlin
158ms :secondmoudle:compileDebugJavaWithJavac
1108ms :app:kaptGenerateStubsDebugKotlin
91ms :secondmoudle:bundleLibRuntimeToJarDebug
129ms :app:mergeDebugNativeLibs
430ms :app:kaptDebugKotlin
1008ms :app:compileDebugKotlin
120ms :app:compileDebugJavaWithJavac
265ms :app:mergeDebugJavaResource
181ms :app:transformClassesAndResourcesWithAuto_registerForDebug
7262ms :app:dexBuilderDebug
1308ms :app:mergeProjectDexDebug
344ms :app:packageDebug
复制代码
~~~
从上述Task列表中可以看出,其实最上面这张图所说的编译流程其实并不完整。
## kapt和apt
我上篇文章说了,javaCompiler执行之前会先执行apt,生成java代码,其任务名就是kaptGenerateStubsDebugKotlin。
[聊聊AbstractProcessor和Java编译流程](https://juejin.cn/post/6844904197775687694 "https://juejin.cn/post/6844904197775687694")
## compiler 混入了奇怪的东西
kotlin已经被引入了很多版本了,但是kotlin的compiler其实和java compiler是不一样的。
如果按照标准答案去回答这个问题吧,总感觉还是有所欠缺的,所以我们需要补充的一个点就是**compileDebugKotlin**。
## 当然少不了transform
当我们使用字节码插桩之后其实就增加了个transform的流程,也就是这个**transformClassesAndResourcesWithAuto\_registerForDebug**。
## 那么是不是还有什么可以补充的呢?
AGP在不同版本的差异还是比较大的。特别是在3.2版本之上的版本被引入了D8编译器之后。
低版本先使用DX编译器将class转化为dex。
而高版本采用**d8**编译器将class转化为dex。
![](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/7/2/1730dfb360e912a5~tplv-t2oaga2asx-watermark.awebp)
### desugar是干嘛的?
Android Studio 为使用部分 Java 8 语言功能及利用这些功能的第三方库提供内置支持。默认工具链对 javac 编译器的输出执行字节码转换(称为 desugar),从而实现新语言功能。
**语法糖香归香,但是最后.dex可是不认识你的。**
### 那么D8的优势是什么呢???
话不多,直接上图。
![](https://img.kancloud.cn/30/4e/304ec052a6949300074db3201572428d_1200x742.png)
![](https://img.kancloud.cn/b1/d1/b1d158e23afa1f921c5c3d6d93a7de72_1200x744.png)
可以看到D8在编译速度以及编译出来的文件体积上有了明显的提升。
## 那么混淆呢??
看看最一开始的图,有没有发现少了混淆的流程呢!!!
在AGP3.4版本上引入了R8,也就是混淆升级版本。而且在高版本上,整体流程也其实发生了微妙的变更,将原先的流程进行了合并。
1. R8开启前的编译流程
![](https://img.kancloud.cn/a1/40/a140d293fa35baeed094237cf3149315_1710x347.png)
2. R8开启后的编译流程
![](https://img.kancloud.cn/d1/61/d16176d80b23d385b36591c29efe091c_1712x448.png)
说句题外话,但是R8更吃内存,机器辣鸡的老哥慎重点。
## 关于签名
之前写的东西有点遗漏啊,谷歌官方有说明,下面是引用啊
> 注意:您必须在应用构建过程中的两个特定时间点之一使用 zipalign,具体在哪个时间点使用,取决于您所使用的应用签名工具:
> 如果您使用的是 apksigner,则只能在为 APK 文件签名之前执行 zipalign。如果您在使用 apksigner 为 APK 签名之后对 APK 做出了进一步更>改,签名便会失效。
> 如果您使用的是 jarsigner,则只能在为 APK 文件签名之后执行 zipalign。
[链接地址](https://link.juejin.cn?target=https%3A%2F%2Fdeveloper.android.google.cn%2Fstudio%2Fcommand-line%2Fzipalign.html "https://developer.android.google.cn/studio/command-line/zipalign.html")
那么当使用V1签名时,编译流程顺序还是6-7
而当使用的是V2的签名时,则编译流程顺序是7-6
## Gradle 生命周期
Gradle的构建过程可以分为三部分:**初始化阶段**、**配置阶段**和**执行阶段**。
简单的说下就是buildSrc先编译,之后是根目录的settings.gradle, 根build.gradle,最后才是module build
![](https://img.kancloud.cn/90/91/9091d64636269616285b57e5f3e5c128_633x525.png)
## apt是编译中哪个阶段
APT解析的是java 抽象语法树(AST),属于javac的一部分流程。大概流程:.java -> AST -> .class
> [聊聊AbstractProcessor和Java编译流程](https://juejin.cn/post/6844904197775687694 "https://juejin.cn/post/6844904197775687694")
## Dex和class有什么区别
[链接传送门](https://link.juejin.cn?target=https%3A%2F%2Fwww.dazhuanlan.com%2Fharmless%2Ftopics%2F1137050 "https://www.dazhuanlan.com/harmless/topics/1137050")
Class与dex的区别
1)虚拟机: class用jvm执行,dex用dvm执行
2)文档: class中冗余信息多,dex会去除冗余信息,包含所有类,查找方便,适合手机端
JVM与DVM
1)JVM基于栈(使用栈帧,内存),DVM基于寄存器,速度更快,适合手机端
2)JVM执行Class字节码,DVM执行DEX
3)JVM只能有一个实例,一个应用启动运行在一个DVM
DVM与ART
1)DVM:每次运行应用都需要一次编译,效率降低。JIT
2)ART:Android5.0以上默认为ART,系统会在进程安装后进行一次预编译,将代码转为机器语言存在本地,这样在每次运行时不用再进行编译,提高启动效率;。 AOP & JIT
## Transform是如何被执行的
Transform 在编译过程中会被封装成Task 依赖其他编译流程的Task执行。
![image.png](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/3fc7dbb584af49aabf7692132a6ee414~tplv-k3u1fbpfcp-watermark.awebp?)
## Transform和其他系统Transform执行的顺序
其实这个题目已经是个过期了,后面对这些都合并整合了,而且最新版的api也做了替换,要不然考虑下回怼下面试官?
[Transform和Task之间有关?](https://juejin.cn/post/6875141808825991181 "https://juejin.cn/post/6875141808825991181")
## 如何监控编译速度变慢问题
~~~
./gradlew xxxxx -- scan
复制代码
~~~
之后会生成一个gradle的网页,填写下你的邮箱就好了。
另外一个相对来说比较简单了。通过gradle原生提供的listener进行就行了。
~~~
// 耗时统计kt化
class TimingsListener : TaskExecutionListener, BuildListener {
private var startTime: Long = 0L
private var timings = linkedMapOf<String, Long>()
override fun beforeExecute(task: Task) {
startTime = System.nanoTime()
}
override fun afterExecute(task: Task, state: TaskState) {
val ms = TimeUnit.MILLISECONDS.convert(System.nanoTime() - startTime, TimeUnit.NANOSECONDS)
task.path
timings[task.path] = ms
project.logger.warn("${task.path} took ${ms}ms")
}
override fun buildFinished(result: BuildResult) {
project.logger.warn("Task timings:")
timings.forEach {
if (it.value >= 50) {
project.logger.warn("${it.key} cos ms ${it.value}\n")
}
}
}
override fun buildStarted(gradle: Gradle) {
}
override fun settingsEvaluated(settings: Settings) {
}
override fun projectsLoaded(gradle: Gradle) {
}
override fun projectsEvaluated(gradle: Gradle) {
}
}
gradle.addListener(TimingsListener())
复制代码
~~~
## Gradle中如何给一个Task前后插入别的任务
最简单的可以考虑直接获取到Task实例,之后在after和before插入一些你所需要的代码。
另外一个就是通过`dependOn`前置和`finalizedBy`挂载一个任务 mustAfter
[Gradle 使用指南 -- Gradle Task](https://link.juejin.cn?target=https%3A%2F%2Fwww.heqiangfly.com%2F2016%2F03%2F13%2Fdevelopment-tool-gradle-task%2F "https://www.heqiangfly.com/2016/03/13/development-tool-gradle-task/")
9. ksp APT Transform的区别
ksp 是kotlin专门独立的ast语法树
apt 是java 的ast语法树
transform是 agp 专门修改字节码的一个方法。
反杀时刻`AsmClassVisitorFactory`,可以看看我之前写的那篇文章。
## Transform上的编译优化能做哪些?
虽然是个即将过期的api,但是大家对他的改动还是都比较多的。
首先肯定是需要完成增量编译的,具体的可以参考我的demo工程。记住,所有的transfrom都要全量。
另外可以考虑多线程优化,将转化操作移动到子线程内,建议使用gradle内部的共享线程。
参考agp最新做法,抽象出一个新的interface,之后通过spi串联,之后将asm链式调用。我的文章也介绍过,具体的点在哪里自己盘算。
[现在准备好告别Transform了吗](https://juejin.cn/post/7016147287889936397 "https://juejin.cn/post/7016147287889936397")
## aar 源码切换插件原理
这个前几天刚介绍过,原理和方案业内都差不多,`mulite-repo`应该都需要这个东西的。我的版本也比较简陋,大厂内部肯定都会有些魔改的。
相对来说功能肯定会更丰富,更全面一点。
> [aar和源码切换插件Plus](https://juejin.cn/post/7028599249675747341 "https://juejin.cn/post/7028599249675747341")
## 你们有哪些保证代码质量的手段
最简单的方式还是通过静态扫描+pipline 处理,之后在合并mr之前进行一次拦截。
静态扫描方式比较多,下面给大家简单的介绍下
阿里的sonar 但是对kt的支持很糟糕,因为阿里使用,所以有很多现成的规则可以使用,但是如果从0-1接入,你可能会直接放弃。
原生的lint,可以基于原生提供的lint api,对其进行开发,支持种类也多,基本上算是一个非常优秀的方案了,但是由于文档资料较少,对于开发的要求可能会较高。
> [AndroidLint](https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2FLeifzhang%2FAndroidLint "https://github.com/Leifzhang/AndroidLint")
13. 如何对第三方的依赖做静态检查?
魔高一尺道高一丈。lint还是能解决这个问题的。
[Tree Api+ClassScanner = 识别三方隐私权限调用](https://juejin.cn/post/7009210297340657701 "https://juejin.cn/post/7009210297340657701")
14. R.java code too large 解决方案
又是一个过期的问题,尽早升级agp版本,让R8帮你解决这个问题,R文件完全可以内联的。
或者用别的AGP插件的R inline也可以解决这个问题。
15. R inline 你需要注意些什么?
预扫描,先收集调用的信息,之后在进行替换。还有javac 的时候可能就因为文件过大,直接挂掉了。
16. 一个类替换父类 比如所有activity实现类替换baseactivity
`class node` 直接替换 `superName` ,想起了之前另外一个问题,感觉主要是要对构造函数进行修改,否则也会出异常。
17. R8 D8 以及混淆相关的,还有R8除了混淆还能干些什么? 混淆规则有没有碰到什么奇怪的问题?
`D8`和`Dx`的区别,主要涉及到编译速度以及编译产物的体积,包体积大概小11%。
`R8` 则是变更了整个编译流程的,其中我觉得最微妙的就是`java8 lambda`相关的,脱糖前后的差别还是比较大的。同时R8也少了很多之前的Transform。
R8的混淆部分,混淆除了能增加代码阅读难度意外,更多的是对于代码优化方面的。 比如无效代码优化, 同时也删除代码等等都可以做。
18. 编译的时候有没有碰到javac的常量优化
javac会将静态常量直接优化成具体的数值。但是尤其是多模块场景下尤其容易出现异常,看起来是个实际的常量引用,但是产物上却是一个具体的常量值了。
。
## 参考资料
[聊聊Android编译流程](https://juejin.cn/post/6845166890759749645)
[Android 基础架构组面试题 | 面试](https://juejin.cn/post/7032625978023084062)
- Android
- 四大组件
- Activity
- Fragment
- Service
- 序列化
- Handler
- Hander介绍
- MessageQueue详细
- 启动流程
- 系统启动流程
- 应用启动流程
- Activity启动流程
- View
- view绘制
- view事件传递
- choreographer
- LayoutInflater
- UI渲染概念
- Binder
- Binder原理
- Binder最大数据
- Binder小结
- Android组件
- ListView原理
- RecyclerView原理
- SharePreferences
- AsyncTask
- Sqlite
- SQLCipher加密
- 迁移与修复
- Sqlite内核
- Sqlite优化v2
- sqlite索引
- sqlite之wal
- sqlite之锁机制
- 网络
- 基础
- TCP
- HTTP
- HTTP1.1
- HTTP2.0
- HTTPS
- HTTP3.0
- HTTP进化图
- HTTP小结
- 实践
- 网络优化
- Json
- ProtoBuffer
- 断点续传
- 性能
- 卡顿
- 卡顿监控
- ANR
- ANR监控
- 内存
- 内存问题与优化
- 图片内存优化
- 线下内存监控
- 线上内存监控
- 启动优化
- 死锁监控
- 崩溃监控
- 包体积优化
- UI渲染优化
- UI常规优化
- I/O监控
- 电量监控
- 第三方框架
- 网络框架
- Volley
- Okhttp
- 网络框架n问
- OkHttp原理N问
- 设计模式
- EventBus
- Rxjava
- 图片
- ImageWoker
- Gilde的优化
- APT
- 依赖注入
- APT
- ARouter
- ButterKnife
- MMKV
- Jetpack
- 协程
- MVI
- Startup
- DataBinder
- 黑科技
- hook
- 运行期Java-hook技术
- 编译期hook
- ASM
- Transform增量编译
- 运行期Native-hook技术
- 热修复
- 插件化
- AAB
- Shadow
- 虚拟机
- 其他
- UI自动化
- JavaParser
- Android Line
- 编译
- 疑难杂症
- Android11滑动异常
- 方案
- 工业化
- 模块化
- 隐私合规
- 动态化
- 项目管理
- 业务启动优化
- 业务架构设计
- 性能优化case
- 性能优化-排查思路
- 性能优化-现有方案
- 登录
- 搜索
- C++
- NDK入门
- 跨平台
- H5
- Flutter
- Flutter 性能优化
- 数据跨平台