0x6、R8又干了啥?
不知道,细心你的有没有发现,D8竟在r8的包里:
接着在 TaskManager.java
搜下D8,经过各种跳转,来到源头:createPostCompilationTasks()
,可以看到在创建D8相关的Transform前还做了一些其他的操作~
① R8 - 执行前
从注释不难看出,这些Tasks就是用来把.class转dex文件的,还加了一些可选步骤如混淆、jacoco(代码覆盖率工具),还创建了一个 TransformManager
实例,用来管理各种TransformManager。继续:
这里引起了我的好奇,可能创建脱糖Task?脱糖不是在D8里进行的吗?跟一下代码:
这里的DESUGAR,猜测是旧版本的D8兼容,AS 3.0引入的,而现在默认是D8,所以这里其实不会创建脱糖Task。继续:
获取外部扩展,合并Java资源,对合并算法感兴趣的可以点进去 MergeJavaResourcesTransform.transform()
看下,这里不展开讲~
继续往下走:
② R8 - Java代码压缩
再往下走,就碰到关键词R8了:
这里有下述三个maybe:
- maybeCreateJavaCodeShrinkerTransform → Java代码压缩
- maybeCreateResourcesShrinkerTransform → 资源压缩
- maybeCreateDexSplitterTransform → dex分割
先看第一个:
这里区分PROGUARD和R8,创建不同的混淆TransformTask,关注 createR8Transform()
核心代码如下:
// 前面获取dex的文件列表、混淆列表等,初始化R8Transform实例时传入 R8Transform transform = new R8Transform( variantScope, userMainDexListFiles, userMainDexListProguardRules, inputProguardMapping, variantScope.getOutputProguardMappingFile()); // 处理混淆规则,callback用于在混淆后执行后续操作 return applyProguardRules( variantScope, inputProguardMapping, variantScope.getOutputProguardMappingFile(), testedVariantData, transform, callback);
跟下 applyProguardRules()
,关注下述核心代码(前面的是和测试相关的):
// This is a "normal" variant in an app/library. applyProguardConfigForNonTest(transform, variantScope);
跟下此方法:
补充其他混淆规则的来源,如AAPT生成的混淆文件,判断如果是AAR的话,直接keep。
跟下 R8Transform.transform(),又是参数,最后调用下述方法:
最后在 r8Tool.kt
中定位到了此方法,核心代码如下:
// 初始化了一个r8CommandBuilder实例 val r8CommandBuilder = CompatProguardCommandBuilder(!useFullR8, D8DiagnosticsHandler(messageReceiver)) // 然后调用一系列方法,如混淆相关 addMainDexRules() setMainDexListConsumer() addProguardConfigurationFiles() addProguardConfiguration() setProguardMapOutputPath() // 配置相关:是否禁用缩小、摇树、脱糖、编译模式 setDisableMinification(toolConfig.disableMinification) setDisableTreeShaking(toolConfig.disableTreeShaking) setDisableDesugaring(toolConfig.disableDesugaring) setMode(compilationMode) setProgramConsumer(programConsumer) ... // 初始化r8ProgramResourceProvider实例,用来给R8提供所有资源 val r8ProgramResourceProvider = R8ProgramResourceProvider() // 各种传参设置 // 最后调用R8.run() ClassFileProviderFactory(libraries).use { libClasspath -> r8CommandBuilder.addLibraryResourceProvider(libClasspath.orderedProvider) R8.run(r8CommandBuilder.build()) }
具体的逻辑,可以追溯到 R8.class → run()
,做了这些事:
- 代码删除:通过语法树静态分析技术、发现并删除未使用的代码,如未实例化的Class等;
- 代码优化:对运行时代码进行优化,删除死代码、未使用的参数,选择性内联、类合并等;
- 代码混淆:优化标识符名字,减少代码量,会判断混淆规则中是否允许修改标识符名字;
- 行号重新映射 等。
③ R8 - 资源压缩
接着跟下第二个 maybeCreateResourcesShrinkerTransform()
:
跟下 ShrinkResourcesTransform.transform()
,
跟下 SplitterRunnable.run()
,看下具体是怎么压缩资源的,核心代码如下:
// ① 创建资源优化记录文件 File reportFile = null; if (params.mappingFile != null) { File logDir = params.mappingFile.getParentFile(); if (logDir != null) { reportFile = new File(logDir, "resources.txt"); } } // ② 分析资源及使用情况 analyzer.analyze(); // ③ 重写.ap_文件(上面AAPT2生成的),去掉没有用到的资源,实际上是没有删除本地资源的! analyzer.rewriteResourceZip(params.uncompressedResourceFile, params.compressedResourceFile); // ③ 导出统计数据
解压AAPT生成的.ap_文件,然后判断是是否为未使用资源,是的话移除,感兴趣可以跟下 rewriteResourceZip()
。
④ R8 - 调D8拆Dex
只有使用了 Multidex
才会走这里,跟下代码:
跟下 DexSplitterTransform.transform()
跟下 DexSplitterTool.Builder
跟下 DexSplitter.run()
跟下 DexSplitterHelper.run()
所以最后还是用的D8打Dex,总算把大概的过程过完了~
0x7、自定义混淆字典
之前在反编译人家的APP时看到标识符竟然不是abcd,而是中文和特殊字符,怎么做到的呢?其实不难,自定义一个混淆字典就好,在app的proguard-rules的同级目录创建一个文件,比如 dictionary,内容示例如下:
﹢ ﹣ × ÷ ...太长省略
接着在 proguard-rules
添加下述配置:
-obfuscationdictionary ./dictionary -classobfuscationdictionary ./dictionary -packageobfuscationdictionary ./dictionary
再接着 gradle assemble
打下包,用jadx打开生成的Release APK康康:
0x8、模块化混淆
混淆的开启由app模块控制,与子模块无关。
建议 在app模块设置公共混淆规则,子模块设置专属混淆规则,子模块区分project和aar:
# Project类型,配置方法同app模块 buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } # AAR类型 android { defaultConfig { consumerProguardFiles 'lib-proguard-rules.txt' } ... }
当然,你想让混淆规则都由app模块控制也是可以的,移除模块时记得删掉对应的混淆就好~
小结
淦(gàn),看源码到吐血,Gradle相关的东西真的是无底洞啊,学完一个又一个,还有著名的ASM字节码插桩,基于APK构建过程Task的HOOK的各种开源性能优化/检测工具等。
参考文献:
- 写给Android开发者的混淆使用手册
- Android CPU, Compilers, D8 & R8
- 关于D8/R8那些事:Desugaring脱糖、APK包体积优化等
- Android 兼容 Java 8 语法特性的原理分析
- 库模块可以包含自己的 ProGuard 配置文件
- 缩减、混淆处理和优化应用
- Android | 代码混淆到底做了什么?