Android 混淆规则是如何生效的?

前言

记录一下关于 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

你可能感兴趣的:(Android 混淆规则是如何生效的?)