前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Android编译的小知识

Android编译的小知识

作者头像
用户1907613
发布2023-09-22 16:49:40
4610
发布2023-09-22 16:49:40
举报
文章被收录于专栏:Android群英传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下

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为准 步骤

  1. Menu → Run → Edit Configurations → Add New Configuration → Remote

点击apply即可

  1. 选择要调试的位置,例如我这里调试dexbuilder,打上断点
  1. terminal中输入 ./gradlew assembleDebug -Dorg.gradle.daemon=false -Dorg.gradle.debug=true
  2. 此时编译会卡住,切换到刚刚创建的remote,点击调试按钮即可
  1. 等待编译一段时间后,执行到dexbuilder阶段,此时断点会触发,如下

后续的话即可一步步调试每个执行逻辑了,对于熟悉AGP源码很有帮助。 ps:以此类推,想调试三方或者自定义Gradle插件也是类似的步骤

4. Android编译流程

资源文件编译

通过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替代工具,用于代码压缩和混淆,包括以下:

  1. shrink:摇树优化,去除无用的类、方法、域等代码
  2. optimize:对字节码的优化,如删除未使用的参数,内联一些方法等
  3. obfuscate:对类、方法的名字进行混淆,使用更短更无规律的字符替代名字
  4. 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”行为的方式 **缺点:**需要兼容每个版本,不够灵活,对开发完全黑盒,容易产生潜在的问题。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2023-09-19 08:30,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 群英传 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 背景
  • 1. 认识Gradle
    • 1.1 Gradle简介
      • 1.2 Gradle生命周期
      • 2. 认识AGP
        • 简介
          • AGP与Gradle的区别与关联
          • 3. AGP和Gradle的一些使用trick
            • 生成Gradle编译报告
              • AGP源码查看与调试
                • 源码查看
                • AGP断点调试
            • 4. Android编译流程
              • 资源文件编译
                • AIDL文件编译
                  • Java与Kotlin文件编译
                    • Class文件打包成Dex
                      • 题外话
                    • 生成APK文件
                      • 签名&对齐
                      • 5. 修改编译结果的几种方式
                        • 一、Transform修改字节码
                          • ASM
                        • 二、 Gradle Task修改
                          • 三、 “修改”AGP源码
                            • 总结
                            领券
                            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档