背景
Android是如何进行编译的? 项目中的源代码是如何一步步被执行为可以安装到手机上的apk的? 文章会一一给大家介绍,尽量以代码为例,好让大家快速理解。
文末有福利~
1. 认识Gradle
1.1 Gradle简介
官方文档:https://docs.gradle.org/7.3.3/userguide/what_is_gradle.html 官方解释:Gradle是一个开源的自动化构建工具。 现在Android项目构建编译都是通过Gradle进行的,Gradle的版本在gradle/wrapper/gradle-wrapper.properties下 Gradle版本为7.3.3 当我们执行assembleDebug/assembleRelease编译命令的时候,Gradle就会开始进行编译构建流程。 当然,在此之前,我们得先了解下Gradle的生命周期
1.2 Gradle生命周期
初始化阶段 执行项目根目录下的settings.gradle脚本,用于判断哪些项目需要被构建,并且为对应项目创建Project对象。 Configuration配置阶段 配置阶段的任务是执行各module下的build.gradle脚本,从而完成Project的配置,并且构造Task任务依赖关系图以便在执行阶段按照依赖关系执行Task。这个阶段Gradle会拉取remote repo的依赖(如果本地之前没有下载过依赖的话) gradle cache一般是放在.gradle/caches/modules-2/ 目录下 Execution执行阶段 获得 Task 的有向无环图之后,执行阶段就是根据依赖关系依次执行 Task 动作。 执行阶段的log如下(一般以Task + module名+task名称)
2. 认识AGP
简介
AGP即Android Gradle Plugin,主要用于管理Android编译相关的Gradle插件集合,包括javac,kotlinc,aapt打包资源,D8/R8等都是在AGP中的。 AGP的版本是在根目录的build.gradle中引入的 如图所示AGP版本为7.2.2
AGP与Gradle的区别与关联
首先需要声明的是,AGP与Gradle不能直接划“等号”,二者不是一个维度的,Gradle是构建工具,而AGP是管理Android编译的插件,是一群java程序的集合。 可以理解为AGP是Gradle构建流程中重要的一环。 虽然AGP与Gradle不是一个维度的事情,但是二者也在一定程度上有所关联 :二者的版本号必须匹配上 https://developer.android.com/studio/releases/gradle-plugin?hl=zh-cn#updating-gradle 当前AGP版本7.2.2,Gradle版本7.3.3,是符合这个标准的。 ps:既然Android编译是通过AGP实现的,AGP就是Gradle插件,那么这个插件是什么时候被apply的呢?因为一个插件如果没有apply的话,那么压根不会执行的。 这就是AGP被apply的地方,也是区分一个module究竟是被打包成app还是一个library
3. AGP和Gradle的一些使用trick
生成Gradle编译报告
编译的时候通过加上--scan,可以生成在线报告。 例如./gradlew assembleDebug --scan 1)基于这个报告,我们可以分析编译耗时的task 2)分析依赖情况(当然本地也可以) 可以知道具体被打包进apk的aar版本究竟是哪个 3)分析引入的依赖对应的maven地址(可以删除废弃的maven,或者确定maven的优先级引入顺序,让编译提速) 例如kotlin插件就是放在远端仓库: https://repo.maven.apache.org/maven2/ 4)结合AGP源码分析每个阶段执行的具体task dexBuilderTESTDevDebug是在AGP的DexArchiveBuilderTask这个阶段执行的
AGP源码查看与调试
源码查看
可以通过在项目中加上compileOnly "com.android.tools.build:gradle:7.2.2" 即可查看AGP7.2.2的源码。 例如如果要查看dexbuilder阶段的源码,通过上述图片中的task名称“DexArchiveBuilderTask”直接全局搜索即可 这样我们就能知道Android究竟是如何一步步进行编译的。
AGP断点调试
当然,光知道源码在哪是不够的,想清楚知道AGP的每个执行细节,需要有能够调试的手段,所以AGP的调试手段就很有必要了。后续AGP都以7.2.2为准 步骤
Menu → Run → Edit Configurations → Add New Configuration → Remote 点击apply即可
选择要调试的位置,例如我这里调试dexbuilder,打上断点
terminal中输入 ./gradlew assembleDebug -Dorg.gradle.daemon=false -Dorg.gradle.debug=true
此时编译会卡住,切换到刚刚创建的remote,点击调试按钮即可
等待编译一段时间后,执行到dexbuilder阶段,此时断点会触发,如下 后续的话即可一步步调试每个执行逻辑了,对于熟悉AGP源码很有帮助。 ps:以此类推,想调试三方或者自定义Gradle插件也是类似的步骤
4. Android编译流程
资源文件编译
通过aapt2编译工程中的资源文件,包括2部分:
编译:将res目录下的所有文件,AndroidManifest.xml编译成二进制文件
链接:合并所有已经编译的文件,生成R.java和resource.arsc
AIDL文件编译
将项目中aidl文件编译为java文件
Java与Kotlin文件编译
通过Javac和Kotlinc将项目中的java代码,kotlin代码编译生成.class字节码文件 这里有个问题:
当java,kotlin混编的时候,谁会先编译成class字节码,这个顺序是随机的吗? 回复:当java,kotlin混编的时候,先执行kotlinc将kotlin文件编译成class字节码,再执行javac将java文件编译成class字节码。
为什么kc比javac先执行? 回复:kotlin是jetBrains开发的,后续才被确认为Android的官方语言之一。kotlin语言解码器是会兼容java语法的,但是在此之前Java是不认识Kotlin这个语言的,Java唯一认准的是字节码格式,即class文件。所以Kotlin必须先被编译成Java能够识别的class文件,这样Javac才能顺利执行。
Class文件打包成Dex
这一步是将生成的class文件和三方库中的aar/jar一并打包成dex 在AGP3.0.1之前,是通过dx将class文件打包成dex 在AGP3.0.1之后,d8替代dx将class文件打包成dex 在AGP3.0.4之后,新增R8(7. 0 及之后版本的 AGP 强制开启 R8),整合了desugaring、shrinking、obfuscating、optimizing 和 dexing,从而将class文件打包成dex ps:R8是Proguard替代工具,用于代码压缩和混淆,包括以下:
shrink:摇树优化,去除无用的类、方法、域等代码
optimize:对字节码的优化,如删除未使用的参数,内联一些方法等
obfuscate:对类、方法的名字进行混淆,使用更短更无规律的字符替代名字
preverify:对字节码进行校验,是 ProGuard 对前面所有优化的一个正确性校验
题外话
从这一步可以看到三方库的二进制文件是不会参与javac/kotlinc的编译打包流程的。 这就会引入另一个问题:编译没问题可以正常执行打包成apk,运行时却出现crash了,报这个class/method/field找不到的问题,例如线上常见的“NoClassDefFoundError/NoSuchMethodError/NoSuchFieldError” 简单描述下这类问题的本质,以NoSuchMethodError为例 目前有4个包,分别是:A, B, C:0.0.1, C:0.0.2 其中A依赖C:0.0.1, 01版本C中有funX,funY 2个接口方法 B依赖C:0.0.2, 02版本C中仅有funY 1个接口方法 A,B单独编译都没问题,但是如果A,B被引入到app module中就有问题了 这个时候,A,B,C都是二进制形式,不会参与javac/kotlinc编译,而AGP解决依赖冲突默认以高版本为准。所以最终打包进apk的是:A,B,C:0.0.2 这三个库。 当运行时,如果逻辑刚好走到A库中,刚好要调用C中的funX方法,那么是肯定找不到的,最终会导致NoClassDefFoundError/NoSuchMethodError/NoSuchFieldError 这类错误。
生成APK文件
在资源文件与代码文件都编译完成后,将manifest文件、resources文件、dex文件、assets文件等等打包成一个压缩包,也就是apk文件。在AGP3.6.0之后,使用zipflinger作为默认打包工具来构建APK,以提高构建速度。
签名&对齐
签名:生成apk文件后需要对其签名,否则无法安装 对齐:zipalign会对apk中未压缩的数据进行4字节对齐,对齐的主要过程是将APK包中所有的资源文件距离文件起始偏移为4字节整数倍,对齐后就可以使用mmap函数读取文件,可以像读取内存一样对普通文件进行操作。 注意:如果是用Android的apksinger进行签名,尤其是以V2之后的签名方式,一定是先进行签名,再进行对齐。不过现在基本已经将签名和对齐整合到一起了 原因:V2之后,会往apk中插入签名块,这也是为什么对齐操作只能在签名之后 https://source.android.com/docs/security/features/apksigning/v2?hl=zh-cn
5. 修改编译结果的几种方式
熟悉了编译流程后,我们可以基于AGP,做一些自定义操作,用于修改编译后最终的产物。 中间产物一般在app模块下的build/intermediates下
一、Transform修改字节码
简介 Transform API 是 AGP 1.5 就引入的特性,主要用于在 Android 构建过程中,在 Class→Dex 这个节点修改 Class 字节码。利用 Transform API,我们可以拿到所有参与构建的 Class 字节码文件,借助 Javassist 或 ASM 等字节码编辑框架进行修改,插入自定义逻辑。 还是以Demo为例,引入字节的btrace插件 查看开启bTrace后,反编译的apk产物 他会在每个方法的开始和末尾插入一段代码,用于记录方法节点,以用于运行时trace采集 实际的源码是肯定没有这些代码的 这也让开发面临了一个不得不接受的事实:你写的代码可能并不是apk最终会执行的代码,有可能你的代码,会被某个优化插件给删除或者“魔改” 当排查线上问题的时候,分析堆栈,查看源码并不是唯一手段,有的时候可能需要借助编译产物来具体分析。
ASM
说到Transform,就不得不提字节码增强处理框架ASM(此处不展开Javassit知识点)。 ASM是一个通用的Java字节码操作和分析框架,它可用于修改现有类或直接以二进制形式动态生成类。 ASM提供了非常多的回调,用于处理Class字节码的每一行代码。 很多Transform插件都是基于ASM实现的,例如刚刚举例子的bTrace 如果对ASM感兴趣,可以参考下ASM中文指南 https://blog.csdn.net/wanxiaoderen/article/details/106898567 或者原版guide https://asm.ow2.io/asm4-guide.pdf 建议搭配插件工具ASM ByteCode Viewer 这样能对ASM更快上手,当然也需要对Java字节码有比较深入的了解 例如一段简单的代码,在ASM框架下,可能就是这样的
二、 Gradle Task修改
可以基于Gradle Task,新增自定义task,修改中间产物以达到最终目的 来看一个例子 这里就是基于gradle注册了一个新的task,在dexbuilder阶段将输出“register suceess”日志
三、 “修改”AGP源码
这里并不是真的修改AGP源码,而是基于类加载机制,如果出现同名的文件,那么会优先加载使用。基于这个原理,我们可以在 classpath "com.android.tools.build:gradle:${agp_version}" 声明的上方引入我们自定义的同名AGP文件jar,这样当实际运行的时候会优先执行我们自定义的逻辑。 demo演示: 以AGP的processDebugManifestForPackage流程为准 创建AGP中同名的Task文件:ProcessPackagedManifestTask.kt,代码也一并copy 然后在这个文件基础上修改,例如我这里是在对应的task中加了一行日志代码 发布jar,然后在build:gradle之前引入path 编译app,查看编译日志,发现“替换“成功。 基于此,我们对AGP的“替换/修改”的方案已实现。 有了这个实现依据,AGP再也不是Gradle的AGP,而是可以私人定制的,想对AGP的任意task流程做修改都是可以的!
总结
以上三种修改编译结果的方式,适用的场景和优缺点还是不同的 **Transform:**适用于会修改class字节码和处理少量资源的场景。 **优点:**灵活,对字节码的修改没有限制,适用于静态检测,字节码插桩,编译优化,包体优化等相关场景。 **缺点:**学习成本高,需要对ASM(Javassist),class文件结构,字节码处理有一定了解;大部分transform会对编译耗时产生影响;AGP8.0废弃了Transform api接口,适配成本巨大。 **Gradle Task:**适用于对编译产物资源进行简单的修改 **优点:**轻便,完全基于Gradle,例如对AndroidManifest修改,收集中间产物上报等。 **缺点:**无法修改字节码,处理场景并不灵活 **“修改”AGP:**适用于解决AGP版本之间不兼容的问题 **优点:**可以达到直接修改“AGP”行为的方式 **缺点:**需要兼容每个版本,不够灵活,对开发完全黑盒,容易产生潜在的问题。
内推招聘帖
[上海/北京] 小红书 - 社区客户端团队 - 基础体验技术方向 - iOS/Android
岗位及团队介绍
小红书社区客户端-基础体验技术团队负责小红书社区主站核心业务的研发工作,包括首页主框架、全场景搜索业务、图文笔记业务、视频消费等核心场景的业务探索、性能体验优化、用户体验与架构优化等工作,你可以充分参与到业务的讨论和落地,也可以发挥主观能动性为小红书的发展助力,我们希望你积极主动,热爱移动端产品的研发,愿意深入钻研,提倡提效,反对内卷,做正确、艰难而有价值的事。
岗位要求
Android 开发工程师
大学本科或以上学历,计算机相关专业,3年以上 Android 相关经验 对移动研发充满热情,有较强的学习能力,好奇心和积极向上的心态 熟悉 Java/Kotlin 语言,熟悉 Android 系统 API,RxJava,Dagger2,以及 App 打包,测试,开发流程 代码基本功扎实,对数据结构及算法有一定程度的理解,良好的面向对象化编程思想,熟练运用常见设计模式 抗压能力强,具备良好的沟通表达能力和团队合作精神 有大型业务架构设计经验者优先,有跨端、动态化经验者优先
iOS 开发工程师
大学本科或以上学历,计算机相关专业,3年以上 iOS 相关经验 对移动研发充满热情,有较强的学习能力,好奇心和积极向上的心态 熟悉 Objective-C/Swift,熟悉 Cocoa 设计模式,深入理解 MVC MVVM 代码基本功扎实,对于常见的第三方库的使用和原理有一定的理解。对数据结构及算法有一定程度的理解,良好的面向对象化编程思想,熟练运用常见设计模式 抗压能力强,具备良好的沟通表达能力和团队合作精神 有大型业务架构设计经验者优先,有跨端、动态化经验者优先
联系方式 邮箱:[dkong@xiaohongshu.com] 联系人:扶摇 微信:bridge_k(加微信备注下公司+岗位+名字+工作经验)秒级通过 优势:Leader直招,秒级反馈,全程跟进,经验分享