背景是这边要求避免露出某些的标识。上周测试同学发现多了一个kotlin_module的文件,大约是这样的:
META-INF/xxxx_release.kotlin_module。
这个文件比较奇怪,demo中没有,但是我生成出来的正式包有。
检查原始代码
发现没有这个文件,但是反编译的代码中可以找到对应的代码
@Metadata(d1 = {"}, d2 = {"Lorg/yeshen/model/Code$Close;", "", "", "(Ljava/lang/String;I)V", "AA", "BB", "CC", "xxxx_release"}, k = 1, mv = {2, 1, 0}, xi = ConstraintLayout.LayoutParams.Table.LAYOUT_CONSTRAINT_VERTICAL_CHAINSTYLE)
-> Metadata,d2中有对应的字符信息"xxxx_release"
-> META-INF/xxxx_release.kotlin_module 文件中的几个字符串反编译后,都可以在代码中找到对应信息
尝试excludes文件失败
尝试了在 android.packagingOptions.exclude 、android.packaging.resources.excludes 中配置了这个文件,发现还是有。
-> exclude 配置是对的,配置了之后,META-INF下其他文件都会消失,但是这个文件不会消失
-> 猜测是动态生成的,所以excludes不了。
定位demo项目不会有这个文件,但是正式包有这个文件的原因
-> 尝试对齐版本,定位是什么什么版本引入的问题。
-> 发现测试包不会有这个文件,但是正式包有,怀疑是混淆问题
-> 问了下ai,ai给了几个可疑的混淆规则,一一尝试之后,定位到是 -keep class kotlin.Metadata { *; } 之后,就会生成这个文件
-> 使用whyareyoukeeping方法,定位到是af-android-sdk-6.17.3引入的规则,逆向查看确实如此
总结
- appsflyer指定了-keep class kotlin.Metadata,这个规则下+混淆, -keep class kotlin.Metadata { *; } 告诉 R8 保留所有带 @Metadata 注解的类。 R8 解析这些类时,发现注解引用了 kotlin_module 文件,R8 将 kotlin_module 标记为强制保留,AGP 资源合并时,R8 的标记优先级高于 exclude 配置,最终 kotlin_module 被打包到 APK 中。
- 这个信息在运行时反射调用的时候会使用到,所以保留其实是合理的行为。
解决方法
- 修改 appsflyer 的包,把-keep class kotlin.Metadata的混淆规则去掉;或者直接去掉这个依赖。
- 避免写文件级别的常量(即把@Metadata 这些东西都去掉就好)也可以解决问题。
- 是修改模块名,避免模块名直接暴露特殊的信息。
AI 总结
Kotlin Module 文件详解
文件来源
META-INF/xxx_release.kotlin_module 文件是由 Kotlin 编译器自动生成的元数据文件,主要在以下情况产生:
1. 生成时机
kotlin
// 当你的项目使用 Kotlin 编写代码时
// Kotlin 编译器会在编译过程中自动生成
- 编译阶段 :Kotlin 编译器(kotlinc)在将
.kt文件编译成.class文件时生成 - 打包阶段 :最终会被打包到 AAR/JAR 文件的
META-INF目录下 - 命名规则 :通常格式为
<模块名>_<构建类型>.kotlin_module
2. 生成位置
build/intermediates/compile_library_classes_jar/
└── release/
└── classes.jar
└── META-INF/
└── your_module_release.kotlin_module
文件作用
核心功能
- 存储 Kotlin 元数据
kotlin
// 记录模块中的 Kotlin 特性信息:
- 顶层函数/属性
- 类型别名
- 扩展函数
- 内联函数
- 模块名称
- 反射支持
kotlin
// 支持 Kotlin 反射获取模块信息
import kotlin.reflect.full.*
fun getModuleInfo() {
// 可以通过反射获取模块中的顶层声明
val kClass = MyClass::class
kClass.memberFunctions
}
- 编译器优化
- 帮助 Kotlin 编译器识别和优化跨模块调用
- 内联函数的跨模块内联
- 编译时类型检查
移除会有的问题
1. 反射功能受限
kotlin
// ❌ 可能失败:无法正确识别顶层声明
val functions = MyClass::class.memberFunctions
// ❌ 可能失败:获取不到模块级别的元数据
kotlin.reflect.jvm.kotlinClass
2. 跨模块调用问题
kotlin
// Module A (library)
// Utils.kt
inline fun debugLog(message: String) {
if (BuildConfig.DEBUG) {
Log.d("TAG", message)
}
}
// Module B (app) - 依赖 Module A
// ⚠️ 如果 kotlin_module 被移除,内联优化可能失效
debugLog("test") // 可能无法正确内联
3. IDE 支持问题
- 代码提示:IDE 可能无法正确提示顶层函数
- 导航功能:跳转到声明可能失效
- 重构支持:重命名等操作可能不准确
4. 多模块项目冲突
groovy
// 如果多个模块有同名的 kotlin_module 文件
// 打包时可能会出现冲突
// build.gradle (可能需要的解决方案)
android {
packagingOptions {
// ⚠️ 不推荐移除
exclude 'META-INF/*.kotlin_module'
}
}
常见场景和建议
1. 文件冲突处理
groovy
// build.gradle
android {
packagingOptions {
// 方案1:保留第一个(不推荐)
pickFirst 'META-INF/*.kotlin_module'
// 方案2:合并(推荐)
merge 'META-INF/*.kotlin_module'
}
}
2. ProGuard/R8 配置
proguard
# proguard-rules.pro
# 保留 kotlin_module 文件
-keep class kotlin.Metadata { *; }
-keepattributes RuntimeVisibleAnnotations
3. 最佳实践
kotlin
// ✅ 推荐:保留 kotlin_module 文件
// 它体积很小(通常几 KB),但提供重要功能
// ❌ 不推荐:移除 kotlin_module
// 除非你确定:
// 1. 不使用 Kotlin 反射
// 2. 没有跨模块的内联函数
// 3. 不需要 IDE 的完整支持
总结
| 方面 | 说明 |
|---|---|
| 生成方式 | Kotlin 编译器自动生成 |
| 文件大小 | 通常几 KB,对 APK 大小影响很小 |
| 建议 | 保留,除非有特殊需求 |
| 移除风险 | 反射失效、内联优化受影响、IDE 支持受限 |
建议:不要移除 这个文件,它对应用运行时影响很小,但对 Kotlin 功能的完整性很重要。如果遇到冲突,使用 merge 策略而不是 exclude。
Kotlin Module 与混淆的关系
1. 混淆确实是关键因素
典型配置差异
groovy
// app/build.gradle
android {
buildTypes {
debug {
minifyEnabled false // ❌ 不混淆
shrinkResources false // ❌ 不压缩资源
}
release {
minifyEnabled true // ✅ 启用混淆
shrinkResources true // ✅ 启用资源压缩
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt')
}
}
}
2. 为什么会有这种差异
R8/ProGuard 的处理逻辑
bash
Debug 构建流程:
Kotlin编译 → 生成kotlin_module → 直接打包 → APK
↓
可能被优化掉(空文件)
Release 构建流程:
Kotlin编译 → 生成kotlin_module → R8混淆 → 重新打包 → APK
↓
强制保留并合并
具体原因分析
groovy
// Debug 模式
- 编译器生成 kotlin_module
- 但因为是空模块或无特殊声明
- Gradle 打包时可能跳过空文件
- 最终 APK 中没有该文件 ✅
// Release 模式(启用混淆)
- R8/ProGuard 会扫描所有 META-INF 文件
- kotlin_module 被视为元数据必须保留
- 即使是空文件也会被强制打包 ⚠️
3. 验证混淆是否是原因
实验方法
groovy
// app/build.gradle
android {
buildTypes {
release {
// 临时禁用混淆测试
minifyEnabled false // ← 改为 false
shrinkResources false
}
}
}
bash
# 重新构建 Release 包
./gradlew clean
./gradlew assembleRelease
# 检查 APK 内容
unzip -l app/build/outputs/apk/release/app-release.apk | grep kotlin_module
如果禁用混淆后 kotlin_module 消失了 → 确认是混淆导致的
4. ProGuard 规则的影响
默认规则会保留 kotlin_module
proguard
# Android 默认的 proguard-android-optimize.txt 包含:
# 保留 Kotlin 元数据
-keep class kotlin.Metadata { *; }
# 保留注解
-keepattributes *Annotation*
# 这些规则会导致 R8 保留 kotlin_module 文件
自定义规则尝试排除
proguard
# proguard-rules.pro
# ❌ 这个无效(R8 会忽略)
-dontwarn META-INF/*.kotlin_module
# ❌ 这个也无效
-dontnote META-INF/*.kotlin_module
# ✅ 需要在 Gradle 中配置
5. R8 的资源压缩行为
shrinkResources 的影响
groovy
android {
buildTypes {
release {
minifyEnabled true
shrinkResources true // ← 这个配置很关键
}
}
}
工作原理
xml
<!-- R8 资源压缩流程 -->
1. 扫描代码引用的资源
2. 标记未使用的资源
3. 删除未引用的资源文件
<!-- kotlin_module 的特殊性 -->
- 不是标准资源文件
- 被视为"代码元数据"
- 默认保留(认为被代码引用)
6. 解决方案
方案 1: 在 R8 阶段排除(AGP 7.0+)
groovy
// app/build.gradle
android {
buildTypes {
release {
minifyEnabled true
// AGP 7.0+ 新语法
packaging {
resources {
excludes += ['META-INF/*.kotlin_module']
}
}
}
}
}
方案 2: 后处理删除
groovy
// app/build.gradle
android {
// ...
}
// 在打包后删除文件
tasks.whenTaskAdded { task ->
if (task.name == 'packageRelease') {
task.doLast {
def apkFile = file("$buildDir/outputs/apk/release/app-release.apk")
if (apkFile.exists()) {
// 使用 zip 命令删除
exec {
commandLine 'zip', '-d', apkFile.absolutePath,
'META-INF/*.kotlin_module'
}
}
}
}
}
方案 3: 自定义 R8 配置
groovy
// app/build.gradle
android {
buildTypes {
release {
minifyEnabled true
// 自定义 R8 配置文件
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'),
'proguard-rules.pro',
'r8-custom.txt' // 额外配置
}
}
}
proguard
# r8-custom.txt
# R8 特定配置(实验性,不保证有效)
-assumenosideeffects class kotlin.Metadata {
public *;
}
7. 为什么 Debug 没有此问题
Debug 模式的特殊性
groovy
// Debug 配置通常是这样:
debug {
minifyEnabled false
shrinkResources false
debuggable true
// 因为没有混淆,Gradle 的打包策略不同:
// - 只打包非空的有效文件
// - 空 kotlin_module 被自动跳过
}
实际生成差异
bash
# Debug 构建
build/intermediates/compile_library_classes_jar/debug/
└── classes.jar
└── META-INF/
└── xxxx_debug.kotlin_module # 可能存在但为空
# 打包到 APK 时
- Gradle 检测到文件为空或无有效内容
- 自动跳过该文件 ✅
# Release 构建
build/intermediates/compile_library_classes_jar/release/
└── classes.jar
└── META-INF/
└── xxxx_release.kotlin_module
# R8 混淆后
- R8 扫描到 META-INF 文件
- 作为元数据强制保留
- 最终打包到 APK ⚠️
8. 验证结论的完整测试
bash
# 1. 检查中间产物
./gradlew :app:compileReleaseKotlin
ls build/intermediates/compile_library_classes_jar/release/classes.jar
# 解压查看
unzip -l build/intermediates/.../classes.jar | grep kotlin_module
# 2. 对比 Debug 和 Release
./gradlew assembleDebug assembleRelease
# 3. 检查 APK 内容
unzip -l app/build/outputs/apk/debug/app-debug.apk | grep kotlin_module
unzip -l app/build/outputs/apk/release/app-release.apk | grep kotlin_module
# 4. 测试禁用混淆的 Release
# 修改 build.gradle: minifyEnabled false
./gradlew clean assembleRelease
unzip -l app-release.apk | grep kotlin_module
9. 总结
| 条件 | Debug | Release | 原因 |
|---|---|---|---|
| minifyEnabled = false | 无文件 | 无文件 | Gradle 跳过空文件 |
| minifyEnabled = true | - | 有文件 | R8 强制保留元数据 ⚠️ |
核心结论:
- ✅ 混淆是主要原因
- R8 将
kotlin_module视为必要的代码元数据 - 即使文件为空,混淆后也会被保留
- Debug 模式因为没有混淆,空文件被自动优化掉
建议:
groovy
// 如果确实要在 Release 中排除
android {
packaging {
resources {
excludes += ['META-INF/*.kotlin_module']
}
}
}
R8 强制保留 kotlin_module 的源码分析
1. 核心原理概览
kotlin_module 文件保留链路:
ProGuard规则 → R8解析 → 元数据扫描 → 资源合并 → 强制打包
↓ ↓ ↓ ↓ ↓
-keep Metadata 识别注解 关联文件 aapt2合并 APK输出
2. R8 源码分析
2.1 ProGuard 规则解析
java
// R8 源码位置:
// com/android/tools/r8/shaking/ProguardConfiguration.java
public class ProguardConfiguration {
// 解析 -keep 规则
public void parseKeepRule(String rule) {
if (rule.contains("kotlin.Metadata")) {
// 标记 Kotlin 元数据需要保留
this.keepKotlinMetadata = true;
}
}
}
2.2 Kotlin Metadata 注解检测
java
// R8 源码:
// com/android/tools/r8/kotlin/KotlinMetadataUtils.java
public class KotlinMetadataUtils {
// Kotlin.Metadata 注解的签名
private static final String KOTLIN_METADATA_DESC =
"Lkotlin/Metadata;";
/**
* 检查类是否包含 Kotlin 元数据注解
*/
public static boolean hasKotlinMetadata(DexProgramClass clazz) {
for (DexAnnotation annotation : clazz.annotations().annotations) {
if (annotation.getAnnotationType()
.toDescriptorString()
.equals(KOTLIN_METADATA_DESC)) {
return true; // ← 发现 @Metadata 注解
}
}
return false;
}
/**
* 解析 Kotlin 元数据内容
*/
public static KotlinMetadata parseMetadata(
DexAnnotation annotation
) {
// 读取 @Metadata 注解的字段
// k: kind (1=class, 2=file facade, 3=synthetic class)
// d1: 元数据二进制数据
// d2: 字符串数据
KotlinMetadataHeader header =
new KotlinMetadataHeader(annotation);
if (header.getKind() == 2) { // File facade
// 这种类型需要 kotlin_module 文件支持
return parseFileFacade(header);
}
return null;
}
}
2.3 关联 kotlin_module 文件的逻辑
java
// R8 源码:
// com/android/tools/r8/kotlin/KotlinModuleUtils.java
public class KotlinModuleUtils {
/**
* 扫描 META-INF 目录下的 kotlin_module 文件
*/
public static void processKotlinModules(
AppView<?> appView,
ExecutorService executorService
) {
// 1. 查找所有 kotlin_module 文件
List<Resource> kotlinModules =
findKotlinModuleResources(appView);
for (Resource resource : kotlinModules) {
// 2. 解析 kotlin_module 内容
KotlinModuleMetadata metadata =
parseKotlinModule(resource);
// 3. 检查是否有对应的类被保留
for (String className : metadata.getFileClasses()) {
DexType type =
appView.dexItemFactory().createType(className);
if (appView.appInfo().definitionFor(type) != null) {
// 4. 如果类被保留,标记 kotlin_module 也要保留
markKotlinModuleAsLive(resource);
break;
}
}
}
}
/**
* 标记 kotlin_module 为必须保留
*/
private static void markKotlinModuleAsLive(Resource resource) {
// 将文件添加到最终的资源列表中
resource.setKeep(true);
// 记录日志
Log.info("Keeping Kotlin module: " + resource.getName());
}
}
3. AGP 的资源合并逻辑
3.1 Gradle 任务流程
kotlin
// AGP 源码位置:
// com/android/build/gradle/internal/tasks/MergeJavaResourcesTask.kt
abstract class MergeJavaResourcesTask : NewIncrementalTask() {
override fun doTaskAction() {
// 合并所有 Java 资源(包括 META-INF)
mergeJavaResources(
inputs = inputs.get(),
output = outputFile.get(),
packagingOptions = packagingOptions.get()
)
}
private fun mergeJavaResources(...) {
val excludePatterns = packagingOptions.excludes
for (input in inputs) {
// 遍历所有资源文件
input.forEach { entry ->
val path = entry.name
// 检查是否匹配排除规则
if (shouldExclude(path, excludePatterns)) {
// ⚠️ 这里是关键:即使配置了 exclude
// 如果文件被 R8 标记为 keep,仍会保留
if (!entry.isMarkedAsKeep()) {
return@forEach // 跳过
}
}
// 添加到输出
output.addEntry(entry)
}
}
}
}
3.2 R8 标记传递机制
java
// AGP 源码:
// com/android/build/gradle/internal/dependency/R8ResourceShrinker.java
public class R8ResourceShrinker {
/**
* R8 shrinking 后的资源标记
*/
public void markResourcesFromR8(
File r8MappingFile,
Set<String> usedResources
) {
// 1. 读取 R8 的输出信息
R8Output output = parseR8Output(r8MappingFile);
// 2. 获取被保留的元数据文件
Set<String> keptMetadata = output.getKeptMetadataFiles();
// 3. 强制标记这些文件
for (String metadataFile : keptMetadata) {
if (metadataFile.endsWith(".kotlin_module")) {
// ← 关键:即使配置了 exclude,这里会覆盖
usedResources.add(metadataFile);
Log.info("R8 marked as keep: " + metadataFile);
}
}
}
}
4. 完整的执行流程
有 -keep kotlin.Metadata
是
有 exclude
已标记 keep
Kotlin 编译
生成 @Metadata 注解
生成 kotlin_module
R8 处理
检查 ProGuard 规则
扫描所有类
找到带 @Metadata 的类
解析注解内容
是否引用 kotlin_module?
标记 kotlin_module 为 keep
资源合并阶段
检查 exclude 规则
检查 R8 标记
忽略 exclude,强制保留
最终 APK 包含 kotlin_module
5. Kotlin Metadata 注解示例
5.1 生成的字节码
kotlin
// 源代码: Utils.kt
package com.example
fun topLevelFunction() {
println("test")
}
java
// 反编译后的 UtilsKt.class
@Metadata(
k = 2, // ← FileFacade 类型
d1 = {
"\u0000\b\n\u0000\n\u0002\u0010\u0002\n\u0000\u001a\u0006\u0010\u0000\u001a\u00020\u0001"
},
d2 = {
"topLevelFunction", "", "app_release"
} // ← 关联到 app_release.kotlin_module
)
public final class UtilsKt {
public static final void topLevelFunction() {
System.out.println("test");
}
}
5.2 kotlin_module 文件内容
protobuf
// META-INF/app_release.kotlin_module (二进制格式)
// 解析后的内容:
module {
package_parts {
package_fqname: "com.example"
file_facades {
name: "UtilsKt" // ← 对应上面的类
}
}
}
6. R8 的判断逻辑源码
java
// R8 核心判断逻辑
// com/android/tools/r8/shaking/Enqueuer.java
public class Enqueuer {
private void processKotlinMetadataAnnotation(
DexProgramClass clazz,
DexAnnotation annotation
) {
// 1. 解析 @Metadata 注解
KotlinMetadata metadata =
KotlinMetadataUtils.parseMetadata(annotation);
if (metadata == null) return;
// 2. 检查 ProGuard 配置
if (!proguardConfiguration.keepKotlinMetadata) {
return; // ← 如果没有 -keep kotlin.Metadata,直接返回
}
// 3. 如果是 FileFacade 类型
if (metadata.isFileFacade()) {
String moduleName = metadata.getModuleName();
String moduleFile = "META-INF/" + moduleName + ".kotlin_module";
// 4. 标记 kotlin_module 文件必须保留
registerResourceToKeep(moduleFile);
// 5. 记录原因(用于调试)
keepReasons.put(
moduleFile,
"Referenced by Kotlin Metadata in " + clazz.type
);
}
}
/**
* 注册需要保留的资源文件
*/
private void registerResourceToKeep(String resourcePath) {
// 添加到全局的 keep 集合
this.resourcesToKeep.add(resourcePath);
// 这个集合会传递给 AGP 的资源合并任务
// 覆盖用户的 exclude 配置
}
}
7. 为什么 exclude 配置无效
7.1 优先级机制
java
// AGP 资源处理优先级
// com/android/build/gradle/internal/tasks/PackageAndroidArtifact.kt
class PackageAndroidArtifact {
fun mergeResources() {
val priority = listOf(
ResourceSource.R8_KEEP, // ← 最高优先级
ResourceSource.AAPT2_COMPILED,
ResourceSource.USER_CONFIG // ← exclude 配置在这里
)
for (resource in allResources) {
// 按优先级处理
val action = determineAction(resource, priority)
when (action) {
Action.KEEP_BY_R8 -> {
// 即使配置了 exclude,也强制保留
output.add(resource)
Log.warn(
"Resource ${resource.name} kept by R8, " +
"ignoring exclude configuration"
)
}
Action.EXCLUDE -> {
// 仅当没有更高优先级标记时才排除
continue
}
}
}
}
}
7.2 调试方法
bash
# 开启 R8 详细日志
./gradlew assembleRelease \
-Pandroid.enableR8.fullMode=true \
-Pandroid.r8.verbose=true \
--info
# 查看 R8 保留的文件
grep "Keeping.*kotlin_module" build/outputs/logs/r8-*.log
示例输出:
[R8] Keeping resource: META-INF/app_release.kotlin_module
[R8] Reason: Referenced by @Metadata in com.example.UtilsKt
[R8] Keep rule: -keep class kotlin.Metadata { *; }
8. 解决方案
方案1:移除 Metadata 保留规则(推荐)
proguard
# proguard-rules.pro
# ❌ 注释掉这行
# -keep class kotlin.Metadata { *; }
# ✅ 只保留必要的 Kotlin 内容
-keep class kotlin.** { *; }
-keep class kotlinx.** { *; }
# 如果需要反射,只保留特定包
-keep @kotlin.Metadata class com.example.** { *; }
方案2:在 R8 之后手动删除
groovy
// app/build.gradle
android {
applicationVariants.all { variant ->
variant.packageApplicationProvider.get().doLast {
// 在打包完成后删除 kotlin_module
def apkFile = variant.outputs[0].outputFile
exec {
commandLine 'zip', '-d', apkFile.path,
'META-INF/*_release.kotlin_module'
}
}
}
}
方案3:修改 R8 配置
gradle
// gradle.properties
android.enableR8.kotlinMetadataRemoval=true # AGP 8.0+
9. 源码位置参考
R8 源码仓库:
https://r8.googlesource.com/r8
关键文件:
- src/main/java/com/android/tools/r8/kotlin/KotlinMetadataUtils.java
- src/main/java/com/android/tools/r8/shaking/Enqueuer.java
- src/main/java/com/android/tools/r8/shaking/ProguardConfiguration.java
AGP 源码仓库:
https://android.googlesource.com/platform/tools/base/
关键文件:
- build-system/gradle-core/src/main/java/com/android/build/gradle/internal/tasks/MergeJavaResourcesTask.kt
- build-system/gradle-core/src/main/java/com/android/build/gradle/internal/dependency/R8ResourceShrinker.java
总结
核心原理:
-keep class kotlin.Metadata { *; }告诉 R8 保留所有带@Metadata注解的类- R8 解析这些类时,发现注解引用了
kotlin_module文件 - R8 将
kotlin_module标记为强制保留 - AGP 资源合并时,R8 的标记优先级高于
exclude配置 - 最终
kotlin_module被打包到 APK 中
关键点 :不是混淆"生成"了文件,而是混淆过程中 R8 强制保留了原本可能被优化掉的文件。