Android编译的小知识

背景

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下 f8867195f676c45dba29c04203b09c08.png Gradle版本为7.3.3 当我们执行assembleDebug/assembleRelease编译命令的时候,Gradle就会开始进行编译构建流程。 当然,在此之前,我们得先了解下Gradle的生命周期

1.2 Gradle生命周期

96a11b190c3e3faa97c17241f93c0225.png 初始化阶段 执行项目根目录下的settings.gradle脚本,用于判断哪些项目需要被构建,并且为对应项目创建Project对象。 Configuration配置阶段 配置阶段的任务是执行各module下的build.gradle脚本,从而完成Project的配置,并且构造Task任务依赖关系图以便在执行阶段按照依赖关系执行Task。这个阶段Gradle会拉取remote repo的依赖(如果本地之前没有下载过依赖的话) 76bddbad38072332e5ffeec4076beb89.png gradle cache一般是放在.gradle/caches/modules-2/ 目录下 bd3d3cbd0b1b5b05db3be0c8d3f82405.png Execution执行阶段 获得 Task 的有向无环图之后,执行阶段就是根据依赖关系依次执行 Task 动作。 执行阶段的log如下(一般以Task + module名+task名称) 946155484866187b9381a037f6086784.png

2. 认识AGP

简介

AGP即Android Gradle Plugin,主要用于管理Android编译相关的Gradle插件集合,包括javac,kotlinc,aapt打包资源,D8/R8等都是在AGP中的。 AGP的版本是在根目录的build.gradle中引入的 f0867be0d151d003a29c09ed2aa5229a.png 如图所示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 15f31613a7a631d18c1b53fe1022e042.png 当前AGP版本7.2.2,Gradle版本7.3.3,是符合这个标准的。 ps:既然Android编译是通过AGP实现的,AGP就是Gradle插件,那么这个插件是什么时候被apply的呢?因为一个插件如果没有apply的话,那么压根不会执行的。 ee79dbe8212dd014590ef5ab906eb78b.png 这就是AGP被apply的地方,也是区分一个module究竟是被打包成app还是一个library

3. AGP和Gradle的一些使用trick

生成Gradle编译报告

编译的时候通过加上--scan,可以生成在线报告。 例如./gradlew assembleDebug --scan 1)基于这个报告,我们可以分析编译耗时的task 6774fa07fb489a8e63c6a22e9bf9db7f.png 2)分析依赖情况(当然本地也可以) 824fbf7e92d9b0ec438abbade2b22bfe.png 可以知道具体被打包进apk的aar版本究竟是哪个 3)分析引入的依赖对应的maven地址(可以删除废弃的maven,或者确定maven的优先级引入顺序,让编译提速) ed79290f30363483d86906330610c410.png 例如kotlin插件就是放在远端仓库: https://repo.maven.apache.org/maven2/ 4)结合AGP源码分析每个阶段执行的具体task 7e64fc3fb8e96108cdf7ad9fdeb152e5.png dexBuilderTESTDevDebug是在AGP的DexArchiveBuilderTask这个阶段执行的

AGP源码查看与调试

源码查看

可以通过在项目中加上compileOnly "com.android.tools.build:gradle:7.2.2" 即可查看AGP7.2.2的源码。 例如如果要查看dexbuilder阶段的源码,通过上述图片中的task名称“DexArchiveBuilderTask”直接全局搜索即可 188b2f82b53d746665cd04ec8a44bf2d.png 这样我们就能知道Android究竟是如何一步步进行编译的。

AGP断点调试

当然,光知道源码在哪是不够的,想清楚知道AGP的每个执行细节,需要有能够调试的手段,所以AGP的调试手段就很有必要了。后续AGP都以7.2.2为准 步骤

  1. Menu → Run → Edit Configurations → Add New Configuration → Remote 157f3f274460d07108c8024da13cb7e2.png 点击apply即可

  2. 选择要调试的位置,例如我这里调试dexbuilder,打上断点 12c8171f15f08f3390993ad0c6771268.png

  3. terminal中输入 ./gradlew assembleDebug -Dorg.gradle.daemon=false -Dorg.gradle.debug=true

  4. 此时编译会卡住,切换到刚刚创建的remote,点击调试按钮即可 d3df108af09d9d4fd3bf63264d2b8bda.png

  5. 等待编译一段时间后,执行到dexbuilder阶段,此时断点会触发,如下 68c59712958fa9901bf51bd2af404ffd.png 后续的话即可一步步调试每个执行逻辑了,对于熟悉AGP源码很有帮助。 ps:以此类推,想调试三方或者自定义Gradle插件也是类似的步骤

4. Android编译流程

fe60f8362b3c75ed97517483ed2df579.png

资源文件编译

通过aapt2编译工程中的资源文件,包括2部分:

  1. 编译:将res目录下的所有文件,AndroidManifest.xml编译成二进制文件

  2. 链接:合并所有已经编译的文件,生成R.java和resource.arsc

AIDL文件编译

将项目中aidl文件编译为java文件

Java与Kotlin文件编译

通过Javac和Kotlinc将项目中的java代码,kotlin代码编译生成.class字节码文件 这里有个问题:

  1. 当java,kotlin混编的时候,谁会先编译成class字节码,这个顺序是随机的吗? 回复:当java,kotlin混编的时候,先执行kotlinc将kotlin文件编译成class字节码,再执行javac将java文件编译成class字节码。

  2. 为什么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替代工具,用于代码压缩和混淆,包括以下: b4e9c275f6f5c7156355c62a2f2b357b.png

  1. shrink:摇树优化,去除无用的类、方法、域等代码

  2. optimize:对字节码的优化,如删除未使用的参数,内联一些方法等

  3. obfuscate:对类、方法的名字进行混淆,使用更短更无规律的字符替代名字

  4. preverify:对字节码进行校验,是 ProGuard 对前面所有优化的一个正确性校验

题外话

从这一步可以看到三方库的二进制文件是不会参与javac/kotlinc的编译打包流程的。 这就会引入另一个问题:编译没问题可以正常执行打包成apk,运行时却出现crash了,报这个class/method/field找不到的问题,例如线上常见的“NoClassDefFoundError/NoSuchMethodError/NoSuchFieldError” 9a4d94220831a8c80c7ebd4f459e8e46.png 简单描述下这类问题的本质,以NoSuchMethodError为例 2800990b0590afd72aa174b29cbabb8c.png 目前有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中就有问题了 aa7113fa29b03a7e59b2c858248fbcca.png 这个时候,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 93499e92b82582049a23576f05843995.png

5. 修改编译结果的几种方式

熟悉了编译流程后,我们可以基于AGP,做一些自定义操作,用于修改编译后最终的产物。 中间产物一般在app模块下的build/intermediates下 43d3eb0201b514cb98392d23bb4071c2.png

一、Transform修改字节码

简介 Transform API 是 AGP 1.5 就引入的特性,主要用于在 Android 构建过程中,在 Class→Dex 这个节点修改 Class 字节码。利用 Transform API,我们可以拿到所有参与构建的 Class 字节码文件,借助 Javassist 或 ASM 等字节码编辑框架进行修改,插入自定义逻辑。 3c80b8f4e5c3a1c4226d49fabb31b675.png 还是以Demo为例,引入字节的btrace插件 e70752f82a505cb213967bf2b8c98011.png 查看开启bTrace后,反编译的apk产物 45a9acaeae284f1b454b2f4f518d8c85.png 他会在每个方法的开始和末尾插入一段代码,用于记录方法节点,以用于运行时trace采集 实际的源码是肯定没有这些代码的 918097984aa867e1d1a33049d3bf08f1.png 这也让开发面临了一个不得不接受的事实:你写的代码可能并不是apk最终会执行的代码,有可能你的代码,会被某个优化插件给删除或者“魔改” 当排查线上问题的时候,分析堆栈,查看源码并不是唯一手段,有的时候可能需要借助编译产物来具体分析。

ASM

说到Transform,就不得不提字节码增强处理框架ASM(此处不展开Javassit知识点)。 ASM是一个通用的Java字节码操作和分析框架,它可用于修改现有类或直接以二进制形式动态生成类。 ASM提供了非常多的回调,用于处理Class字节码的每一行代码。 d737d6d5f9d28493fbf86c8789aa2796.png 很多Transform插件都是基于ASM实现的,例如刚刚举例子的bTrace 如果对ASM感兴趣,可以参考下ASM中文指南 https://blog.csdn.net/wanxiaoderen/article/details/106898567 或者原版guide https://asm.ow2.io/asm4-guide.pdf 建议搭配插件工具ASM ByteCode Viewer 08fb35d663ebaa838a73722691cb3798.png 这样能对ASM更快上手,当然也需要对Java字节码有比较深入的了解 例如一段简单的代码,在ASM框架下,可能就是这样的 eb310fed43caa24c1c46aa865ea751d2.png

二、 Gradle Task修改

可以基于Gradle Task,新增自定义task,修改中间产物以达到最终目的 来看一个例子 3e8271f64057329d2600e18545f2d101.png 这里就是基于gradle注册了一个新的task,在dexbuilder阶段将输出“register suceess”日志 9af3694c4fd9a5730d2af32642c99075.png

三、 “修改”AGP源码

这里并不是真的修改AGP源码,而是基于类加载机制,如果出现同名的文件,那么会优先加载使用。基于这个原理,我们可以在 classpath "com.android.tools.build:gradle:${agp_version}" 声明的上方引入我们自定义的同名AGP文件jar,这样当实际运行的时候会优先执行我们自定义的逻辑。 demo演示: 以AGP的processDebugManifestForPackage流程为准 c75c455bb686476ba7e62c83b8811b5d.png 创建AGP中同名的Task文件:ProcessPackagedManifestTask.kt,代码也一并copy 32e519105a195987e50c2881a807e08a.png 然后在这个文件基础上修改,例如我这里是在对应的task中加了一行日志代码 0eabefe97f30df2d0bc79942492e6cc6.png 发布jar,然后在build:gradle之前引入path 4713ef16f66df2bc68785eac0da4ff44.png 编译app,查看编译日志,发现“替换“成功。 851f86b1ab22382e2b5ef5743b6d5e25.png 基于此,我们对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直招,秒级反馈,全程跟进,经验分享

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值