前言
记录一下关于 Android 中关于混淆配置文件的生效规则、混淆规则的细节、build 产物中和混淆相关的内容及其作用。
混淆配置生效规则
现在的 Android 项目一般由一个主 app module
,n 个子 lib module
共同组成。 app module
通过 dependencies 闭包依赖这些子 module ,或者是将这些子 module 上传到中央仓库之后进行依赖。
if (source_code.toBoolean()) {
implementation project(path: ':thirdlib')
} else {
implementation 'com.engineer.third:thirdlib:1.0.0'
}
implementation project(path: ':compose')
implementation project(path: ':common')
implementation 'androidx.navigation:navigation-fragment-ktx:2.5.2'
...
比如对于下图中的几个子 module
可以通过 project(path: 'xxx')
的方式依赖,也可以将这个本地 module 上传到中央仓库之后通过 group_id:artifact_id:version
的方式依赖。
那么这两种方式依赖由哪些差异呢?
- 远程依赖会比直接在本地依赖节省一些编译时间 (当然这不包括下载依赖本身耗费的时间),毕竟可以省去编译源码及资源的时间。
- 对于混淆来说,这两种依赖方式混淆配置规则的生效是有些差异的。这里的差异是说混淆配置文件的差异,而不是说具体的一条混淆配置语法会有差异 。
下面具体来说一下这个差异。关于混淆配置,除了各个 moudle 下我们非常熟悉的肉眼可见 proguard-rules.pro
之外,其实还有别的混淆配置,最终会合并在一起生效。
说到各个 module 的配置文件合并,大家一定会想到 AndroidManifest.xml 。最终打包使用的 AndroidManifest.xml 的内容,就是各个子 module 和主 module merge 后的结果。
需要注意的是,Android 打包过程并不会主动合并本地 module 中的 proguard-rules.pro
文件 。注意,这里说的是本地 module .
也就是说像 common/thirdlib/compose 这类直接在本地依赖的 module, 其内部的 proguard-rules.pro
并不会直接生效。 而通过 implementation group_id:artifact_id:version
依赖的远程 module ,如果其内部有配置 proguard 规则,就会 merge 到最终的混淆配置中。上一篇 发布 Android Lib 到 Maven 解惑 中我们提到, library
通过 gradle 任务发布到中央仓库的时候,会基于本地 consumer-rules.pro
生成最终的 proguard.txt
文件一并打包到 aar 文件中;这里 merge 的就是这个自动生成的 proguard.txt
。而最终的混淆配置规则叠加到一起之后,在 app/build/outputs/mapping/huaweiLocalRelease/configuration.txt
这个文件里。
这个文件是有规则的,会按照段落列出编译过招中所涉及的模块。
001:# The proguard configuration file for the following section is D:\workspace\MinApp\app\build\intermediates\default_proguard_files\global\proguard-android-optimize.txt-7.2.1
121:# The proguard configuration file for the following section is D:\workspace\MinApp\app\proguard-rules.pro
182:# The proguard configuration file for the following section is D:\workspace\MinApp\app\build\intermediates\aapt_proguard_file\huaweiLocalRelease\aapt_rules.txt
392:# The proguard configuration file for the following section is D:\workspace\MinApp\common\build\intermediates\consumer_proguard_dir\release\lib0\proguard.txt
395:# The proguard configuration file for the following section is D:\Android\.gradle\caches\transforms-3\89e35bb901a511dc73379ee56d9a96fb\transformed\navigation-ui-2.3.5\proguard.txt
416:# The proguard configuration file for the following section is D:\Android\.gradle\caches\transforms-3\ed14b9608e236c3cb341584bd1991f2a\transformed\material-1.5.0\proguard.txt
465:# The proguard configuration file for the following section is D:\Android\.gradle\caches\transforms-3\38b91e3dad918eabe8ced61c0f881bef\transformed\jetified-stetho-1.6.0\proguard.txt
470:# The proguard configuration file for the following section is D:\Android\.gradle\caches\transforms-3\380c0daab5f38fa92451c63d6b7f2468\transformed\preference-1.1.1\proguard.txt
494:# The proguard configuration file for the following section is D:\Android\.gradle\caches\transforms-3\a00816a85507c4640738406281464e4f\transformed\appcompat-1.4.1\proguard.txt
519:# The proguard configuration file for the following section is D:\Android\.gradle\caches\transforms-3\19a9b30d1e238c7cf954868475b2d87a\transformed\navigation-common-ktx-2.3.5\proguard.txt
541:# The proguard configuration file for the following section is D:\Android\.gradle\caches\transforms-3\bc1e654ac594a8eec67d83a310d595cd\transformed\rules\lib\META-INF\com.android.tools\r8-from-1.6.0\kotlin-reflect.pro
559:# The proguard configuration file for the following section is D:\Android\.gradle\caches\transforms-3\822d22c7ed69ccdf4d90c18a483e72c5\transformed\rules\lib\META-INF\com.android.tools\r8-from-1.6.0\coroutines.pro
585:# The proguard configuration file for the following section is D:\Android\.gradle\caches\transforms-3\555542c94d89a20ac01618f64dfcfed2\transformed\rules\lib\META-INF\proguard\coroutines.pro
608:# The proguard configuration file for the following section is D:\Android\.gradle\caches\transforms-3\a830f069563364388aaf53b586352be8\transformed\jetified-glide-4.13.1\proguard.txt
625:# The proguard configuration file for the following section is D:\Android\.gradle\caches\transforms-3\9ce146a7d8708a759f2821d06606c176\transformed\jetified-flexbox-1.0.0\proguard.txt
647:# The proguard configuration file for the following section is D:\Android\.gradle\caches\transforms-3\003c1e88ccf7eabdb17daba177d5544b\transformed\jetified-hilt-lifecycle-viewmodel-1.0.0-alpha03\proguard.txt
654:# The proguard configuration file for the following section is D:\Android\.gradle\caches\transforms-3\2675f8213875fddbbb3d30c803c00c9c\transformed\jetified-hilt-android-2.40.1\proguard.txt
665:# The proguard configuration file for the following section is D:\Android\.gradle\caches\transforms-3\3368f9a73434dea0d4e52626ffd9a8c9\transformed\fragment-1.3.6\proguard.txt
687:# The proguard configuration file for the following section is D:\Android\.gradle\caches\transforms-3\58278e1b3a97715913034b7b81fae8cb\transformed\jetified-lifecycle-viewmodel-savedstate-2.3.1\proguard.txt
697:# The proguard configuration file for the following section is D:\Android\.gradle\caches\transforms-3\cb4d77137f22248d78cd200f94d17fc4\transformed\jetified-savedstate-1.1.0\proguard.txt
717:# The proguard configuration file for the following section is D:\Android\.gradle\caches\transforms-3\6a6fcb77b4395418002e332cd9738bfb\transformed\work-runtime-2.7.0\proguard.txt
728:# The proguard configuration file for the following section is D:\Android\.gradle\caches\transforms-3\4ab9d68c51a5e06d113a80174817d2cc\transformed\media-1.0.0\proguard.txt
753:# The proguard configuration file for the following section is D:\Android\.gradle\caches\transforms-3\5599788d3c018cf9be3c21d9a4ff4718\transformed\coordinatorlayout-1.1.0\proguard.txt
778:# The proguard configuration file for the following section is D:\Android\.gradle\caches\transforms-3\0e108ece111c1c104d1543d98f952017\transformed\vectordrawable-animated-1.1.0\proguard.txt
800:# The proguard configuration file for the following section is D:\Android\.gradle\caches\transforms-3\19c137c4f40e8110221a03964c21b354\transformed\recyclerview-1.1.0\proguard.txt
827:# The proguard configuration file for the following section is D:\Android\.gradle\caches\transforms-3\9eb9006bf5796c20208d89f414c860f8\transformed\transition-1.3.0\proguard.txt
848:# The proguard configuration file for the following section is D:\Android\.gradle\caches\transforms-3\848cc86aa556453b7ae2d77cf1ed69f7\transformed\core-1.7.0\proguard.txt
867:# The proguard configuration file for the following section is D:\Android\.gradle\caches\transforms-3\c269ff43c6850351c92a4f3de7a5d26d\transformed\jetified-lifecycle-process-2.4.0\proguard.txt
871:# The proguard configuration file for the following section is D:\Android\.gradle\caches\transforms-3\f082dcda3ea45d057bb4fd056c4b3864\transformed\lifecycle-runtime-2.4.0\proguard.txt
896:# The proguard configuration file for the following section is D:\Android\.gradle\caches\transforms-3\c78621e75bc17f9e3a8dc4279fe51aed\transformed\rules\lib\META-INF\proguard\retrofit2.pro
928:# The proguard configuration file for the following section is D:\Android\.gradle\caches\transforms-3\9dd79968324ef9619ccee991ab21aa68\transformed\rules\lib\META-INF\proguard\rxjava2.pro
931:# The proguard configuration file for the following section is D:\Android\.gradle\caches\transforms-3\9eef9e5128bbcd6232ee9a89f4c5bf00\transformed\lifecycle-viewmodel-2.3.1\proguard.txt
941:# The proguard configuration file for the following section is D:\Android\.gradle\caches\transforms-3\5875adda5cf6fa792736faf48738cf7c\transformed\jetified-startup-runtime-1.0.0\proguard.txt
952:# The proguard configuration file for the following section is D:\Android\.gradle\caches\transforms-3\e06b693ee9109a0e8f8d0949e74720e0\transformed\room-runtime-2.4.0\proguard.txt
957:# The proguard configuration file for the following section is D:\Android\.gradle\caches\transforms-3\6008298b41480f69c56a08890c83e302\transformed\versionedparcelable-1.1.1\proguard.txt
964:# The proguard configuration file for the following section is D:\Android\.gradle\caches\transforms-3\2810bf2a83f304a3ff02e4019efe065f\transformed\rules\lib\META-INF\proguard\androidx-annotations.pro
985:# The proguard configuration file for the following section is D:\Android\.gradle\caches\transforms-3\6dc53357fb30238e16dbc902967a8aab\transformed\jetified-annotation-experimental-1.1.0\proguard.txt
1011:# The proguard configuration file for the following section is D:\Android\.gradle\caches\transforms-3\4233f8c9725e3a6760c0e0e606e43b29\transformed\rules\lib\META-INF\proguard\okhttp3.pro
1025:# The proguard configuration file for the following section is
可以看到,除了我们熟悉的 app/proguard-rules.pro 之外,其实还使用了其他 module 的 xxx.pro 文件。当然,这里有些文件,可能没有配置任何内容,只是一个默认的配置,就像 app/proguard-rules.pro 刚创建时候的样子,有兴趣的话可以打开文件查看。
在这个最终的混淆配置规则里还有一些值得我们注意的地方。
- proguard-android-optimize.txt-7.2.1
# This is a configuration file for ProGuard.
# http://proguard.sourceforge.net/index.html#manual/usage.html
#
# Starting with version 2.2 of the Android plugin for Gradle, this file is distributed together with
# the plugin and unpacked at build-time. The files in $ANDROID_HOME are no longer maintained and
# will be ignored by new version of the Android plugin for Gradle.
也就是说从 AGP 2.2 开始,在编译阶段会使用当前 AGP 插件所携带的混淆规则,而不在使用本地 Android SDK/tools/proguard/ 目录下的混淆配置了。 这个混淆配置文件里规则都非常通用的,比如对于 enum , Keep 注解,webview Interface 等等之类的规则。 这就意味着 AGP 插件的升级有可能会影响到混淆,如果某个 AGP 版本所携带的混淆规则发了一些变化的话。
- aapt_rules.txt
aapt 顾名思义,就是在执行 AAPT 阶段生成的混淆规则,可以看到里面都是基于 Android 应用层源码的一些配置。会根据代码中的资源文件、布局文件等内容生成相应的规则。比如会基于 AndroidManifest.xml 中声明的四大组件,保留相应的 Activity、Service 等类的构造函数,一些自定义 View 的构造函数等。
- META-INF\proguard\okhttp3.pro
这类混淆规则其实是 xxx.jar
文件内部的混淆规则。Android 开发中非常实用的 okhttp、RxJava、Retrofit
等这些纯 Java/Kotlin 代码的 module 打包之后上传到中央仓库的就是 jar 文件,而不是 aar (毕竟不涉及到 UI,因此也不会有资源文件了)。
对于 java-library
类型的 module, 通过上述配置,最终打包的 jar 文件中将包含这个 thirdlib.pro
混淆配置文件。
剩下的就是一些我们常用的类库自身携带的混淆规则了,可以看到这些 aar
类型的库其混淆配置文件都是 proguard.txt 。
从这里我们可以看到,AGP 已经非常完善了,在打包过程中会在基于实际代码自动生成相应的混淆规则,尤其是关于 Android 自身类及组件的配置。平时在网上看到的各种混淆配置,没必要非得对着里面的内容一条一条的进行配置,一些非常基础且共用的混淆规则都是默认的。我们实际需要关心的还是自身业务相关的混淆规则,比如涉及 Json 序列化操作的 Model 类的,以及自己写的涉及反射操作的类。
那么子 moudle 直接在本地依赖的情况下,混淆配置是如何生效的呢?
子 module 的生效规则
这里我们可以重点关注一下 392:# The proguard configuration file for the following section is D:\workspace\MinApp\common\build\intermediates\consumer_proguard_dir\release\lib0\proguard.txt
从路径就可以猜出来了,这里的 lib0 就是本地依赖的 common
module 。
这部分在 configuration.txt 中是这样的。
# The proguard configuration file for the following section is D:\workspace\MinApp\common\build\intermediates\consumer_proguard_dir\release\lib0\proguard.txt
-keep class com.engineer.common.utils.AndroidFileUtils {*;}
# End of content from D:\workspace\MinApp\common\build\intermediates\consumer_proguard_dir\release\lib0\proguard.txt
这部分就是子 module 的混淆配置。子 module 混淆配置生效有两种方式,而这两种方式都依赖 consumerProguardFiles
这个属性。
直接使用 consumer-rules.pro
直接在子 module 的 consumer-rules.pro
中配置要混淆的规则。然后在 build.gradle 中通过默认的配置生效
defaultConfig {
minSdk ext.minSdkVersion
targetSdk ext.targetSdkVersion
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles "consumer-rules.pro"
}
子 module 创建的时候,会默认在 defaultConfig 闭包中添加 consumerProguardFiles 这个配置,因此只要在 consumer-rules.pro
中配置了混淆规则,就会生效。
使用 proguard-rules.pro
如果你不习惯使用 consumer-rules.pro
的话,也可以使用 proguard-rules.pro
,直接配置一下就可以了。
buildTypes {
release {
minifyEnabled false
consumerProguardFiles "proguard-rules.pro"
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
这两种方式配置的内容,最终都会生效。再次谨记,混淆规则是叠加生效的,并不存在什么先后顺序。打包过程中只要找到了可用的配置文件,就会照单全收
混淆产物
说完了混淆配置生效的规则,可以一并再看一下混淆的产物。打包完成后,会在 app/build/outputs/mapping/{flavor}/
目录下生成一些混淆相关的文件。
文件名 | 作用 |
---|---|
configuration.txt | 所有混淆配置的汇总 |
mapping.txt | 原始与混淆过的类、方法、字段名称间的转换 |
resources.txt | 资源优化记录文件,哪些资源引用了其他资源,哪些资源在使用,哪些资源被移除 |
seeds.txt | 未进行混淆的类与成员 |
usage.txt | APK中移除的代码 |
通过这些文件,我们就可以看到一次打包过程中,混淆具体做了哪些事情。比较常用的是 mapping.txt 文件,当混淆过后的包出现问题时,通过 stacktrace 定位问题的时候,由于代码被混淆会无法识别,这时候就是通过 mappting.txt 文件解混淆。这里使用过 bugly 的同学应该很熟悉了。线上代码出现问题,上传 mapping 文件就可以快速定位到出现问题的具体代码了。
通过 seeds.txt 也可以看到哪些文件没有被混淆,内容是否符合预期。
上面混淆配置生效规则里提到了,打包过程中会综合各个 module 的混淆配置文件。因此,有时候我们会发现,自己明明没有配置某些类的 keep ,但是这些类依然没有被混淆,这时候可能就是由于项目本身依赖的 module 的混淆规则生效了。 比如 configuration.txt 中 Android fragment 包的这条规则
# The proguard configuration file for the following section is /Users/rookie/.gradle/caches/transforms-3/3368f9a73434dea0d4e52626ffd9a8c9/transformed/fragment-1.3.6/proguard.txt
# The default FragmentFactory creates Fragment instances using reflection
-if public class ** extends androidx.fragment.app.Fragment
-keepclasseswithmembers,allowobfuscation public class <1> {
public ();
}
# End of content from /Users/rookie/.gradle/caches/transforms-3/3368f9a73434dea0d4e52626ffd9a8c9/transformed/fragment-1.3.6/proguard.txt
所有继承自 androidx.fragment.app.Fragment 的类都会随着其构造方法的一起被 keep 。这样最终混淆结果中就会有很多的业务相关的 XXXFragment 类无法被混淆。至于原因,上面的注释解释的很清楚了,需要通过反射创建 Fragment 的实例。
所以,在混淆过程中,如果发现一些没有类没有被混淆,不妨在 configuration.txt
中找找原因。
严格来说,resources.txt 是由于配置了 shrinkResources true
对无效资源文件进行移除操作后产生的结果,不算是混淆,但是这里可以理解为混淆过程
混淆规则
混淆规则本质上非常灵活,很难用一句话概括清楚。这里引用郭神的Android安全攻防战,反编译与混淆技术完全解析(下) 中的表述 ,感觉比较清晰。
keep 关键字规则
关键字 | 描述 |
---|---|
keep | 保留类和类中的成员,防止它们被混淆或移除。 |
keepnames | 保留类和类中的成员,防止它们被混淆,但当成员没有被引用时会被移除。 |
keepclassmembers | 只保留类中的成员,防止它们被混淆或移除。 |
keepclassmembernames | 只保留类中的成员,防止它们被混淆,但当成员没有被引用时会被移除。 |
keepclasseswithmembers | 保留类和类中的成员,防止它们被混淆或移除,前提是指名的类中的成员必须存在,如果不存在则还是会混淆。 |
keepclasseswithmembernames | 保留类和类中的成员,防止它们被混淆,但当成员没有被引用时会被移除,前提是指名的类中的成员必须存在,如果不存在则还是会混淆。 |
通配符规则
通配符 | 描述 |
---|---|
|
匹配类中的所有字段 |
|
匹配类中的所有方法 |
|
匹配类中的所有构造函数 |
* |
匹配任意长度字符,但不含包名分隔符(.)。比如说我们的完整类名是com.example.test.MyActivity,使用com.,或者com.exmaple.都是无法匹配的,因为无法匹配包名中的分隔符,正确的匹配方式是com.exmaple..,或者com.exmaple.test.,这些都是可以的。但如果你不写任何其它内容,只有一个*,那就表示匹配所有的东西。 |
** |
匹配任意长度字符,并且包含包名分隔符(.)。比如proguard-android.txt中使用的-dontwarn android.support.**就可以匹配android.support包下的所有内容,包括任意长度的子包。 |
*** | 匹配任意参数类型。比如void set()就能匹配任意传入的参数类型,* get*()就能匹配任意返回值的类型。 |
… |
匹配任意长度的任意类型参数。比如void test(…)就能匹配任意void test(String a)或者是void test(int a, String b)这些方法。 |
网上大部分文章提到的混淆配置语法都大同小异,都是从正面出发,这样有时候其实是不太灵活的。比如在某些场景下我们需要保留所有实现了 Serializable
接口的类,因为这些类涉及到序列化操作。
-keep class * implements java.io.Serializable {*;}
这条规则本身没问题,但是其实这个规则的范围是很大的。因为我们常用的 enum 的具体实现 Enum 类
public abstract class Enum>
implements Comparable, Serializable { ....}
也是实现了 Serializable
接口的。因此会导致所有的 enum 类型无法被混淆。实际上 Java 集合框架也有很多类实现了这个接口。虽然 Android 官方不建议使用枚举,但是现实中使用的还是挺多的,比如 glide 。这样就会导致原本可以被混淆的类受到牵连。
那么可以避免这种情况吗?其实是有办法的,细心的话你也许已经发现了,在上面 FragmentFactory
的混淆配置语法里有条件判断的逻辑。
# The default FragmentFactory creates Fragment instances using reflection
-if public class ** extends androidx.fragment.app.Fragment
-keepclasseswithmembers,allowobfuscation public class <1> {
public ();
}
看到这里的 if 你是不是有点想法了呢?强烈建议在需要配置混淆规则的时候多参考一下 configuration.txt
中一些官方库的配置规则,也许会让你打开一扇新的打门。
混淆认知
混淆配置规则看起来简单,但其实结合实际场景会变得有些复杂,尤其是代码包含内部类,匿名内部,静态内部类等等不同场景下。这些具体的规律还是需要结合实际场景通过不断的验证。 关于代码混淆,最好的学习方法就是自己通过写代码,组合各类配置不断验证。打包后可以用 jadx-gui 查看混淆的 apk 文件。
最后再补充一个进行混淆配置验证时的小技巧。
android {
//...
lint {
checkReleaseBuilds false
}
}
直接在 app/build.gradle android 闭包下配置关闭 releaseBuild 时的 lint 检查。毕竟混淆规则的修改不会影响代码本身,因此可以通过跳过检测,节省编译时间。毕竟这个 lint 检查的耗时还是很可观的。这样就可以避免每次打包时的等待了。 有些时候临时打 release 包验证一些问题的时候,也可以临时加上这个配置关闭检测。
参考
- 补齐Android技能树 - 从害怕到玩转Android代码混淆
- Android安全攻防战,反编译与混淆技术完全解析(下)
- Android代码混淆及ProGuard手册
- Android开发:请你吃一顿史上最全的Android混淆大餐
- 混淆的另一重境界
作者:IAM四十二
链接:https://juejin.cn/post/7148456353332215838