opoojkk

AAR 构建全解析:从解包到 APK 的完整生命周期

lxx
目次

依赖里有一堆 AAR,最终 APK 里却不见它们的影子。

不是它们消失了,而是它们一进构建系统就被解包,残片分别流向:

下面按真实的 AGP 源码线把整个事件复盘一遍。

AAR 生命周期的起点: ExtractAarTransform #

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.jarlibs/ 目录下的 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 构建

处理流程分两种情况:

关键入口大致是这样:

 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()
);

在这个过程中发生了什么:

  1. 合并: 所有 AAR 的 classes.jar + libs/*.jar + 主工程的 class 文件一起处理
  2. 依赖解析: 处理 AAR 之间的传递依赖关系,确保所有需要的类都被包含
  3. 混淆规则收集: 所有 AAR 的 proguard.txt 被收集并应用到 R8/ProGuard
  4. 转换: Java 字节码 (.class) → Dalvik 字节码 (.dex)
  5. 分包: 如果方法数超过 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;
}

你看到的"主题被覆盖"“权限凭空出现"等现象都来源于这里。

合并优先级 (从高到低):

  1. 主模块 manifest
  2. buildType manifest (如 debug/release)
  3. flavor manifest (如 paid/free)
  4. 依赖库 manifest (AAR)

依赖冲突处理: 当 AAR1 依赖 AAR2,且都声明了相同的组件时:

合并完成后,AAR 自己的 manifest 文件即完成使命。

3. res/ → 编译 → 合并 → 资源表 (resources.arsc) #

资源的处理是整个构建流程中最复杂的部分,分三个阶段。

源码位置:

1
2
tools/base/sdk-common/src/main/java/com/android/ide/common/resources/...
tools/aapt2/

第一阶段: 编译 (compile) #

每个资源文件单独编译成中间格式。

源码入口:

1
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 文件。

所有 flat 文件被合并,生成最终的资源表和资源 ID。

源码入口:

1
tools/aapt2/link/

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/ .

最终产物:

库的资源从此融入主工程,获得重新分配的统一资源 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 文件,合并优先级:

  1. 主模块 assets
  2. buildType assets
  3. flavor assets
  4. 依赖库 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'
        }
    }
}

那么 x86x86_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:

重要: 这些 ID 只在库自己的构建中有效,当库被集成到主工程时,所有 ID 会被 aapt2 link 重新分配。

这个文件在 aapt2 link 阶段被使用:

处理位置:

1
tools/aapt2/link/

参与生成:

  1. R.jar: 最终的 R class,包含所有资源 ID
  2. 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。

处理位置:

1
tools/base/lint/

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,用于检查:

全链路流程图 (源码版) #

为什么 AAR 必须被解包? #

你可能会问: 为什么不直接把 AAR 塞进 APK?

因为 APK 的结构是固定的:

而 AAR 是"源码级别"的打包格式,它的存在是为了:

  1. 方便分发 (不用发源码)
  2. 包含完整信息 (代码 + 资源 + manifest + so + lint 规则)
  3. 保持模块化 (每个库独立构建)

但构建 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   (保持规则,防止混淆关键类)

构建后流向:

最终 APK 中:

关键技术细节总结 #

资源 ID 的重新分配 #

很多人误以为 AAR 的资源 ID 会保留,实际上:

  1. AAR 中的 R.txt 记录的 ID (如 0x7f120001) 只在该库构建时有效
  2. 集成到主工程时,aapt2 link 会完全重新分配所有 ID
  3. 最终每个资源得到一个全局唯一的新 ID
  4. 这就是为什么你不能硬编码资源 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 声明冲突时:

常见冲突示例:

 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 时:

  1. 依赖解析: Gradle 首先解析整个依赖图,确定所有需要的 AAR
  2. 版本冲突: 如果 AAR1 依赖 AAR2:1.0,主模块直接依赖 AAR2:2.0,默认选择 2.0 (最近优先)
  3. 资源合并: 所有 AAR 的资源一起参与 aapt2 link,按优先级合并
  4. 代码合并: 所有 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 文件”,它需要的只是:

所以 AAR 一进入系统就被解包,碎片分别走向不同的处理链,最后散落在:

AAR 自己不可能出现在 APK 内部。

这,就是它的全部生命周期。

附录: AGP 版本差异说明 #

本文基于 AGP 7.x-8.x 源码分析,不同版本的主要差异:

特性AGP 7.xAGP 8.0+
Transform API旧版 Transform API新的 Artifact Transform API
R.java 编译独立的 CompileRFilesTask整合到主编译流程
资源处理基于 AAPT2同样基于 AAPT2,优化了增量编译
依赖解析Configuration API同样,但增加了类型安全的 API
Task 类名MergeNativeLibsMergeNativeLibsTask (名称统一化)

核心流程保持一致,主要是 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 解包的本质流程未变。

标签:
Categories: