AAR 构建全解析:从解包到 APK 的完整生命周期
lxx
目次
依赖里有一堆 AAR,最终 APK 里却不见它们的影子。
不是它们消失了,而是它们一进构建系统就被解包,残片分别流向:
- dex
- manifest
- resources.arsc
- assets
- so
下面按真实的 AGP 源码线把整个事件复盘一遍。
AAR 被下载后,首先交给一个类处理:
源码位置:
1
2
| tools/base/build-system/gradle-core/src/main/java/
com/android/build/gradle/internal/dependency/ExtractAarTransform.kt
|
源码链接: ExtractAarTransform.kt
它的使命只有一个: 把一个 AAR 解压成若干个部分,每个部分给后续的构建步骤使用。
注: 本文基于 AGP 7.x-8.x 版本分析。在 AGP 8.0+ 中,部分实现使用了新的 Artifact Transform API,但整体流程保持一致。
核心代码长这样:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
| // ExtractAarTransform.kt (简化版)
override fun transform(
outputs: TransformOutputs,
input: File,
inputLibrary: LibraryIdentity
) {
ZipFile(input).use { zip ->
zip.entries().asSequence().forEach { entry ->
if (entry.isDirectory) return@forEach
val path = entry.name
val outputFile = when {
// classes.jar: 库的主要字节码
path == SdkConstants.FN_CLASSES_JAR -> {
outputs.file(CLASSES_JAR)
}
// libs/*.jar: AAR 打包时包含的第三方依赖 jar
path.startsWith(LIBS_PREFIX) && path.endsWith(SdkConstants.DOT_JAR) -> {
outputs.file(LIBS_DIR, path.substring(LIBS_PREFIX.length))
}
// AndroidManifest.xml: 库自己的 Manifest
path == SdkConstants.FN_ANDROID_MANIFEST_XML -> {
outputs.file(MANIFEST)
}
// res/: 资源文件 (layout、drawable、values 等)
path.startsWith(SdkConstants.FD_RES + '/') -> {
outputs.file(RES_DIR, path.substring(4))
}
// assets/: 原始资源文件
path.startsWith(SdkConstants.FD_ASSETS + '/') -> {
outputs.file(ASSETS_DIR, path.substring(7))
}
// jni/: native 库 (so 文件)
path.startsWith(SdkConstants.FD_JNI + '/') -> {
outputs.file(JNI_DIR, path.substring(4))
}
// R.txt: 资源符号表
path == SdkConstants.FN_RESOURCE_TEXT -> {
outputs.file(R_TXT)
}
// proguard.txt: 混淆规则
path == SdkConstants.FN_PROGUARD_TXT -> {
outputs.file(PROGUARD_RULES)
}
// lint.jar: Lint 检查规则
path == LINT_JAR_PATH -> {
outputs.file(LINT_JAR)
}
else -> null
}
// 将文件从 zip 中拷贝到输出目录
outputFile?.let { file ->
zip.getInputStream(entry).use { input ->
file.outputStream().use { output ->
input.copyTo(output)
}
}
}
}
}
}
|
AAR 在这里完成它的历史使命——解包成构建系统真正需要的零件。
剩下的流程,就是看这些碎片分别去哪。
1. classes.jar + libs/*.jar → 代码转换 → dex #
classes.jar 和 libs/ 目录下的 jar 文件拆出来之后,被交给代码处理链。
源码位置:
1
2
3
| tools/base/build-system/gradle-core/src/main/java/
com/android/build/gradle/internal/tasks/R8Task.kt # Release 构建
com/android/build/gradle/internal/tasks/DexArchiveBuilderTask.kt # Debug 构建
|
处理流程分两种情况:
- R8: 代码压缩 (ProGuard) + 优化 + 混淆 + dex 转换 (Release 模式)
- D8: 单纯的 class → dex 转换 (Debug 模式或禁用 R8 时)
关键入口大致是这样:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| // R8 模式 (Release 构建) - 伪代码
// AGP 7.x-8.0
R8.run(R8Command.builder()
.addProgramFiles(classesJar) // 主程序 classes
.addLibraryFiles(androidJar) // Android SDK
.setMinApiLevel(minSdk) // 最低 API
.addProguardConfigurationFiles(...) // 混淆规则 (包括 AAR 的 proguard.txt)
.setOutput(outputDex, OutputMode.DexIndexed)
.build()
);
// D8 模式 (Debug 构建) - 伪代码
D8.run(D8Command.builder()
.addProgramFiles(classesJar)
.setMinApiLevel(minSdk)
.setOutput(outputDex, OutputMode.DexIndexed)
.build()
);
|
在这个过程中发生了什么:
- 合并: 所有 AAR 的 classes.jar + libs/*.jar + 主工程的 class 文件一起处理
- 依赖解析: 处理 AAR 之间的传递依赖关系,确保所有需要的类都被包含
- 混淆规则收集: 所有 AAR 的 proguard.txt 被收集并应用到 R8/ProGuard
- 转换: Java 字节码 (.class) → Dalvik 字节码 (.dex)
- 分包: 如果方法数超过 65536,自动拆成多个 dex
最终输出:
1
2
3
4
| classes.dex
classes2.dex
classes3.dex
...
|
此时库的字节码已经和主工程代码一起,统一转换成了 dex 格式。
2. AndroidManifest.xml → 合并 → 最终 manifest #
库里的 manifest 文件会被送进合并器。
源码路径:
1
| tools/base/build-system/manifest-merger/
|
源码链接: manifest-merger
核心入口是:
1
2
3
4
5
6
7
8
9
10
11
12
13
| // ManifestMerger2.java (简化逻辑)
MergingReport report = ManifestMerger2.newMerger(
mainManifest, // 主模块的 manifest
logger,
MergeType.APPLICATION // 应用类型 (还有 LIBRARY 类型)
)
.addFlavorAndBuildTypeManifests(...) // variant 的 manifest
.addLibraryManifests(libraryManifests) // AAR 的 manifest 在这里加入
.withFeatures(...) // 合并特性配置
.merge(); // 执行合并
// 获取合并后的 manifest
XmlDocument mergedDocument = report.getMergedDocument();
|
合并的核心逻辑:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| // 简化版的节点合并策略 (伪代码)
private XmlDocument merge(
XmlDocument lowerPriorityDocument, // 优先级低的 (库)
XmlDocument higherPriorityDocument // 优先级高的 (主模块)
) {
// 遍历低优先级文档的所有可合并元素
for (XmlElement element : lowerPriorityDocument.getRootNode().getMergeableElements()) {
// 在高优先级文档中查找同类型、同 key 的节点
Optional<XmlElement> higherPriorityNode =
higherPriorityDocument.getNodeByTypeAndKey(element);
if (higherPriorityNode.isPresent()) {
// 存在冲突,按策略合并属性
// 默认: 高优先级覆盖低优先级
mergeAttributes(element, higherPriorityNode.get());
} else {
// 不冲突,直接添加到结果中
addNodeToMergedDocument(element);
}
}
return mergedDocument;
}
|
你看到的"主题被覆盖"“权限凭空出现"等现象都来源于这里。
合并优先级 (从高到低):
- 主模块 manifest
- buildType manifest (如 debug/release)
- flavor manifest (如 paid/free)
- 依赖库 manifest (AAR)
依赖冲突处理: 当 AAR1 依赖 AAR2,且都声明了相同的组件时:
- 按依赖解析顺序确定优先级
- 直接依赖优先于传递依赖
- 可使用
tools:replace、tools:merge、tools:remove 等标记手动控制合并策略
合并完成后,AAR 自己的 manifest 文件即完成使命。
3. res/ → 编译 → 合并 → 资源表 (resources.arsc) #
资源的处理是整个构建流程中最复杂的部分,分三个阶段。
源码位置:
1
2
| tools/base/sdk-common/src/main/java/com/android/ide/common/resources/...
tools/aapt2/
|
第一阶段: 编译 (compile) #
每个资源文件单独编译成中间格式。
源码入口:
Task 类: CompileLibraryResourcesTask (AGP 7.x-8.0)
处理流程:
1
2
3
4
5
6
7
8
9
10
11
12
13
| // aapt2 compile (简化逻辑)
int CompileCommand::Run(const std::vector<std::string>& args) {
for (const std::string& filePath : inputFiles) {
// 读取资源文件
std::unique_ptr<ResourceFile> resource = LoadResourceFile(filePath);
// 编译成 flat 格式
// flat 是 aapt2 的中间产物格式——每个资源文件编译后生成一个 .flat 文件
CompiledFileOutputStream out(outputPath);
CompileResource(resource.get(), &out);
}
return 0;
}
|
例如:
1
2
3
| res/layout/activity_main.xml → activity_main.xml.flat
res/drawable/icon.png → icon.png.flat
res/values/strings.xml → values_strings.arsc.flat
|
每个 AAR 的资源文件、主模块的资源文件,都会各自生成一堆 .flat 文件。
第二阶段: 链接 (link) #
所有 flat 文件被合并,生成最终的资源表和资源 ID。
源码入口:
Task 类: LinkApplicationAndroidResourcesTask (AGP 7.x-8.0)
处理流程:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
| // aapt2/link/Link.cpp (简化逻辑)
int LinkCommand::Run(const std::vector<std::string>& args) {
// 1. 加载所有 compiled resources (flat 文件)
std::vector<ResourceTable> tables;
for (const std::string& input : inputFiles) {
auto table = LoadCompiledFile(input);
tables.push_back(std::move(table));
}
// 2. 加载 AAR 的 R.txt 符号表
// R.txt 记录了库中资源的原始 ID,但这些 ID 会被重新分配
for (const std::string& rtxtPath : libraryRTxtFiles) {
ResourceTable table = ParseRTxt(rtxtPath);
tables.push_back(std::move(table));
}
// 3. 合并资源表
// 所有 AAR 的资源 + 主模块的资源都在这里合并
ResourceTable finalTable;
for (const ResourceTable& table : tables) {
// 检查资源冲突,执行合并策略
if (!finalTable.Merge(table, context)) {
// 冲突处理: 主模块优先,AAR 资源被覆盖
ReportConflict();
}
}
// 4. 重新分配资源 ID
// 每个资源分配一个唯一的 ID (0xPPTTEEEE 格式)
// PP (0x7f) = 应用资源包 ID (系统资源是 0x01)
// TT = 资源类型 ID (如 0x02=drawable, 0x0d=layout, 0x12=string)
// EEEE = 该类型下的资源条目 ID (Entry ID)
// AAR 原有的资源 ID 会被完全重新分配
IdAssigner assigner(&finalTable);
assigner.AssignIds();
// 5. 生成 resources.arsc (资源索引表)
TableFlattener flattener(options, &finalTable);
std::ofstream arscFile(outputDir + "/resources.arsc", std::ios::binary);
flattener.Flatten(&arscFile);
// 6. 生成 R.java (或针对每个包生成独立的 R.java)
// 包含所有资源的 ID 常量
JavaClassGenerator generator(context, &finalTable);
generator.Generate(packageName, outputDir + "/R.java");
// 7. 打包非编译资源 (如 raw、图片原文件等)
PackageResources(finalTable, outputDir);
return 0;
}
|
第三阶段: 编译 R.java → R.jar #
aapt2 link 生成的是 R.java 源文件,还需要编译:
1
2
3
4
5
| // 简化的编译流程
// AGP 7.x-8.0: 独立的编译任务
// AGP 8.0+: 整合到主编译流程中
javac -d output/ R.java
jar cf R.jar -C output/ .
|
最终产物:
- resources.arsc: 资源索引表 (记录所有资源的 ID 和位置)
- R.jar: 包含 R.java 编译后的 class
- res/… : 优化后的资源文件 (layout、drawable 等)
库的资源从此融入主工程,获得重新分配的统一资源 ID。
4. assets/ → 原样打包进入 APK #
这部分处理最简单,直接复制。
源码位置:
1
2
| tools/base/build-system/gradle-core/src/main/java/
com/android/build/gradle/internal/tasks/MergeSourceSetFolders.java
|
核心逻辑:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| // MergeSourceSetFolders.java (简化逻辑)
@TaskAction
public void merge() throws IOException {
// 收集所有 assets 目录 (主模块 + 所有 AAR)
List<File> assetsSets = getAssetsSets();
// 直接复制到合并目录
for (File assetsDir : assetsSets) {
if (assetsDir.exists()) {
// 简单的文件树复制
FileUtils.copyDirectory(assetsDir, outputDir);
}
}
}
|
如果多个 AAR 或主模块有同名的 assets 文件,合并优先级:
- 主模块 assets
- buildType assets
- flavor assets
- 依赖库 assets (按依赖解析顺序,后解析的覆盖先解析的)
最终结果: 所有 assets 文件原样躺在 APK 的 assets/ 目录下。
5. jni/ (so) → ABI 过滤 → 打包进入 APK #
so 文件的处理在 native 库合并任务里完成。
源码路径:
1
2
| tools/base/build-system/gradle-core/src/main/java/
com/android/build/gradle/internal/tasks/MergeNativeLibsTask.kt
|
注: Task 类名在不同 AGP 版本可能略有差异,但逻辑一致。
核心逻辑:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| // MergeNativeLibsTask.kt (简化逻辑)
@TaskAction
fun taskAction() {
// 收集所有 so 文件 (主模块 + AAR)
val nativeLibs = collectNativeLibs()
// 获取 ABI 过滤配置
val abiFilters = variantData.ndkConfig.abiFilters
nativeLibs.forEach { (abi, soFiles) ->
// 只处理配置中允许的 ABI
if (abiFilters.isEmpty() || abiFilters.contains(abi)) {
soFiles.forEach { soFile ->
// 复制到对应的 ABI 目录
val destFile = outputDir.resolve("lib/$abi/${soFile.name}")
soFile.copyTo(destFile, overwrite = true)
}
}
// 不在过滤列表中的 ABI 直接丢弃
}
}
|
例如,如果你在 build.gradle 中配置:
1
2
3
4
5
6
7
| android {
defaultConfig {
ndk {
abiFilters 'arm64-v8a', 'armeabi-v7a'
}
}
}
|
那么 x86、x86_64 的 so 文件会被直接丢弃。
最终输出结构:
1
2
3
4
5
6
7
| lib/
├── arm64-v8a/
│ ├── libxxx.so
│ └── libyyy.so
└── armeabi-v7a/
├── libxxx.so
└── libyyy.so
|
6. R.txt → 参与资源链接 #
AAR 中的 R.txt 记录了库内所有资源的名称、类型和在该库中的资源 ID。
格式示例:
1
2
3
| int string app_name 0x7f120001
int layout activity_main 0x7f0d0025
int drawable icon 0x7f080003
|
资源 ID 格式说明 (0xPPTTEEEE):
资源 ID 是 32 位整数,由三部分组成:
例如 0x7f0d0025:
0x7f = 应用资源包0x0d = layout 类型0x0025 = 该 layout 类型下的第 37 号资源
重要: 这些 ID 只在库自己的构建中有效,当库被集成到主工程时,所有 ID 会被 aapt2 link 重新分配。
这个文件在 aapt2 link 阶段被使用:
处理位置:
参与生成:
- R.jar: 最终的 R class,包含所有资源 ID
- resources.arsc: 资源索引表中的符号信息
基本流程:
1
2
3
4
5
6
7
8
9
10
11
12
| // aapt2 link 中读取 R.txt (简化逻辑)
std::vector<ResourceTable> librarySymbols;
for (const std::string& rtxtPath : libraryRTxtFiles) {
// 解析 R.txt,提取资源名称和类型信息
// 原有的 ID 值会被忽略,稍后重新分配
ResourceTable table = ParseRTxt(rtxtPath);
librarySymbols.push_back(std::move(table));
}
// 合并所有符号,分配统一的新 ID
MergeSymbols(librarySymbols, &finalTable);
AssignNewIds(&finalTable);
|
7. lint.jar → Lint 检查 #
AAR 中的 lint.jar 包含自定义的 Lint 检查规则。
使用场景:
库作者可以在 AAR 中打包自己的 Lint 规则,确保使用者正确使用该库的 API。
处理位置:
Task 类: AndroidLintTask
这些 lint.jar 会在代码检查阶段被加载:
1
2
3
4
5
6
| // Lint 检查时加载 AAR 的自定义规则 (简化逻辑)
List<File> lintJars = collectLintJarsFromAars();
LintClient client = new LintClient();
for (File lintJar : lintJars) {
client.addLintRules(lintJar);
}
|
实际案例:
例如,Jetpack Compose 的 AAR 中就包含 lint.jar,用于检查:
- 不正确的 Composable 函数命名 (必须以大写字母开头)
- 状态管理最佳实践 (remember、rememberSaveable 的使用)
- 性能相关的建议 (避免在 Composition 中进行耗时操作)
全链路流程图 (源码版) #

为什么 AAR 必须被解包? #
你可能会问: 为什么不直接把 AAR 塞进 APK?
因为 APK 的结构是固定的:
- dex: 所有代码必须合并成统一的字节码
- resources.arsc: 所有资源必须有统一的 ID 映射表
- AndroidManifest.xml: 只能有一个最终的 manifest
而 AAR 是"源码级别"的打包格式,它的存在是为了:
- 方便分发 (不用发源码)
- 包含完整信息 (代码 + 资源 + manifest + so + lint 规则)
- 保持模块化 (每个库独立构建)
但构建 APK 时,这些信息必须"扁平化"成 APK 的标准结构。
所以 AAR 必须被解包,没有其他选择。
实际案例: RecyclerView AAR 的解包 #
以 androidx.recyclerview:recyclerview:1.3.2 为例:
AAR 大小: 约 350 KB
解包后内容:
1
2
3
4
5
6
7
8
9
| classes.jar → 280 KB (包含所有 RecyclerView 相关类)
AndroidManifest.xml → 1 KB (声明最低 API 等基本信息)
R.txt → 5 KB (约 100+ 个资源符号)
res/
├── values/
│ ├── attrs.xml (自定义属性定义)
│ └── public.xml (公开的资源 ID)
└── layout/ (无,RecyclerView 无内置 layout)
proguard.txt → 2 KB (保持规则,防止混淆关键类)
|
构建后流向:
classes.jar → 经过 R8 混淆优化 → 合并到 classes.dexAndroidManifest.xml → 合并到主 manifest (贡献了 minSdkVersion 约束)res/values/attrs.xml → 编译到 resources.arscR.txt → 参与资源链接,生成新的资源 IDproguard.txt → 确保 RecyclerView.LayoutManager 等关键类不被混淆
最终 APK 中:
- 代码大小: 约 180 KB (混淆优化后)
- 资源增量: 约 3 KB (attrs 定义)
关键技术细节总结 #
资源 ID 的重新分配 #
很多人误以为 AAR 的资源 ID 会保留,实际上:
- AAR 中的 R.txt 记录的 ID (如
0x7f120001) 只在该库构建时有效 - 集成到主工程时,aapt2 link 会完全重新分配所有 ID
- 最终每个资源得到一个全局唯一的新 ID
- 这就是为什么你不能硬编码资源 ID 的根本原因
示例:
1
2
3
4
5
6
| // 错误做法 - 永远不要这样写!
int errorId = 0x7f120001; // 这个 ID 在不同构建中会变化
textView.setText(errorId);
// 正确做法
textView.setText(R.string.app_name); // 编译时会替换为实际 ID
|
混淆规则的收集 #
所有 AAR 的 proguard.txt 会被收集并应用:
1
2
3
4
5
6
| 主模块 proguard-rules.pro
+ AAR1 proguard.txt
+ AAR2 proguard.txt
+ ...
→ 合并后的完整混淆规则
→ 传递给 R8
|
这确保了库的 API 不会被错误混淆。
示例 (RecyclerView 的 proguard.txt):
1
2
3
4
5
6
7
8
9
| # 保持 LayoutManager 及其子类不被混淆
-keep public class * extends androidx.recyclerview.widget.RecyclerView$LayoutManager {
public <init>(...);
}
# 保持 ViewHolder 不被混淆
-keep public class * extends androidx.recyclerview.widget.RecyclerView$ViewHolder {
public <init>(...);
}
|
Manifest 合并的冲突解决 #
当多个 manifest 声明冲突时:
- 优先级高的覆盖低的 (主模块 > buildType > flavor > 依赖)
- 可以使用
tools:replace、tools:merge、tools:remove 等标记控制策略 - 构建时会生成详细的合并报告 (位于
build/outputs/logs/manifest-merger-*.txt)
常见冲突示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| <!-- 主模块 manifest -->
<application
android:theme="@style/AppTheme"
tools:replace="android:theme"> <!-- 强制覆盖库的 theme -->
...
</application>
<!-- AAR manifest -->
<application
android:theme="@style/LibraryTheme">
...
</application>
<!-- 最终结果: 使用 AppTheme -->
|
传递依赖的处理 #
当依赖关系为 App → AAR1 → AAR2 时:
- 依赖解析: Gradle 首先解析整个依赖图,确定所有需要的 AAR
- 版本冲突: 如果 AAR1 依赖 AAR2:1.0,主模块直接依赖 AAR2:2.0,默认选择 2.0 (最近优先)
- 资源合并: 所有 AAR 的资源一起参与 aapt2 link,按优先级合并
- 代码合并: 所有 AAR 的 classes.jar 一起传递给 R8/D8
依赖冲突解决策略:
1
2
3
4
5
6
7
8
9
10
| dependencies {
implementation 'com.example:aar1:1.0' // 内部依赖 aar2:1.0
implementation 'com.example:aar2:2.0' // 直接依赖,优先级更高
// 如果要强制使用特定版本
implementation('com.example:aar1:1.0') {
exclude group: 'com.example', module: 'aar2' // 排除传递依赖
}
implementation 'com.example:aar2:1.5' // 手动指定版本
}
|
总结: AAR 的命运只有一个——被解包 #
构建系统不需要"一个 AAR 文件”,它需要的只是:
- 里面的 class → 转成 dex
- manifest → 合并到主 manifest
- res → 编译后合并到 resources.arsc,资源 ID 重新分配
- assets → 直接复制
- so → 按 ABI 过滤后复制
- R.txt → 参与资源链接,生成最终 R class
- proguard.txt → 参与代码混淆
- lint.jar → 提供自定义检查规则
所以 AAR 一进入系统就被解包,碎片分别走向不同的处理链,最后散落在:
- classes.dex (代码)
- resources.arsc (资源索引)
- AndroidManifest.xml (配置)
- assets/ (原始文件)
- lib/ (native 库)
AAR 自己不可能出现在 APK 内部。
这,就是它的全部生命周期。
附录: AGP 版本差异说明 #
本文基于 AGP 7.x-8.x 源码分析,不同版本的主要差异:
| 特性 | AGP 7.x | AGP 8.0+ |
|---|
| Transform API | 旧版 Transform API | 新的 Artifact Transform API |
| R.java 编译 | 独立的 CompileRFilesTask | 整合到主编译流程 |
| 资源处理 | 基于 AAPT2 | 同样基于 AAPT2,优化了增量编译 |
| 依赖解析 | Configuration API | 同样,但增加了类型安全的 API |
| Task 类名 | MergeNativeLibs | MergeNativeLibsTask (名称统一化) |
核心流程保持一致,主要是 API 和性能优化上的改进。
源码版本说明:
本文基于 Android Gradle Plugin (AGP) 7.x-8.x 源码分析,路径均来自:
1
| https://android.googlesource.com/platform/tools/base/
|
不同 AGP 版本细节可能略有差异 (如 Task 类名、API 调用方式),但整体流程保持一致。AGP 8.0+ 引入了新的 Artifact Transform API,但 AAR 解包的本质流程未变。