ThinkChat🤖让你学习和工作更高效,注册即送10W Token,即刻开启你的AI之旅 广告
[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)