Android & ProGuard

参考链接首选官方文档>>

  • 开启方式
  • 配置方式
  • 调整方法

开始之前说一下自己对 ProGuard 的理解,更多时候开启 ProGuard 的性价比是比较小的:
在安全方面,简单的混淆并不能阻止反编译,但同时又带来了调试和迭代上的问题(针对项目配置混淆规则、升级/引入第三方库可能存在问题)。
另一方面,很多 APP 的价值都不在本身的逻辑上而依赖于对应的后台/运营,而对于本身的关键代码更应该放到 os 文件里并加固 java 代码来加大反编译的难度。
当然 ProGuard 同时也有压缩资源文件来减小 APK 大小的作用,这里也不是完全否认 ProGuard 的作用。

开启

android {
    buildTypes {
        release {
            minifyEnabled true
            shrinkResources true
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}
  • proguardFiles:
    <SDK>/tools/proguard/proguard-android.txt 是默认的配置;
    app/proguard-rules.pro 是我们自定义规则的文件;

  • app/build.gradle:为不同的 build type 设置不同的配置;

  • minifyEnabled: 移除无效的类、类成员、方法、属性等;把类名、属性名、方法名替换为简短且无意义的名称

  • shrinkResources: 删除 / 合并资源,将 drawable/layout 中没有被引用的文件的内容清空而保留文件;而名称相同的资源被视为重复资源会被合并;
    注意:删除资源很容易出现问题,这时候要找到被误删的资源并在 res/raw/keep.xml 中标注:

<!--tools:keep 属性中指定要保留的资源-->
<!--tools:discard 属性中指定要舍弃的资源-->
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"
    tools:keep="@layout/l_used*_c,@layout/l_used_a,@layout/l_used_b*"
    tools:discard="@layout/unused2" />

构建成功后可以在 <module-name>/build/outputs/mapping/release/ 中可以找到相关文件:
dump.txt: 说明 APK 中所有类文件的内部结构。
mapping.txt: 提供原始与混淆过的类、方法和字段名称之间的转换。
seeds.txt: 列出未进行混淆的类和成员。
usage.txt: 列出从 APK 移除的代码。

配置

自定义的混淆规则的格式如下:

[保持命令] <类> {
    [成员] 
}
  • [保存命令]
命令 说明
-dontwarn 不提示指定包名的混淆打包 warning
-keep 防止类和成员被移除或者被重命名
-keepnames 防止类和成员被重命名
-keepclassmembers 防止成员被移除或者被重命名
-keepnames 防止成员被重命名
-keepclasseswithmembers 保留指定的类和其成员,前提是它们在压缩阶段没有被删除
-keepclasseswithmembernames 防止拥有该成员的类和成员被重命名

<类> 指定了作用文件,可以有如下匹配写法:

  • 具体的类(完整包名)
    -dontnote retrofit2.Platform
  • 访问修饰符(public、protected、private)
    -keep public class * extends android.app.Fragment
  • 通配符 *,匹配任意长度字符,但不含包名分隔符(.)
  • 通配符 **,匹配任意长度字符,并且包含包名分隔符(.)
    -dontwarn com.tencent.bugly.**
  • extends,即可以指定类的基类
    -keep public class * extends android.app.Fragment
  • implement,匹配实现了某接口的类
    -keep public class * implements com.bumptech.glide.module.GlideModule
  • $,内部类

[成员] 指定了类成员相关的限定条件,可以使用:

  • 匹配所有构造器
    public <init>();
  • 匹配所有字段
    <fields>
  • 匹配所有方法
# 保留所有类中的有 @org.greenrobot.eventbus.Subscribe 注解的方法
-keepclassmembers class ** {
    @org.greenrobot.eventbus.Subscribe <methods>;
}
  • 通配符 *,匹配任意长度字符,但不含包名分隔符(.)
  • 通配符 **,匹配任意长度字符,并且包含包名分隔符(.)
  • 通配符 ***,匹配任意参数类型
  • …,匹配任意长度的任意类型参数。
    void test(…)
  • 访问修饰符(public、protected、private)

而一些常用的写法:

# 不混淆某个类
-keep public class name.huihui.example.Test { *; }

# 不混淆某个包所有的类
-keep class name.huihui.test.** { *; }

#不混淆某个类的子类
-keep public class * extends name.huihui.example.Test { *; }

#不混淆所有类名中包含了“model”的类及其成员
-keep public class **.*model*.** {*;}

#不混淆某个接口的实现
-keep class * implements name.huihui.example.TestInterface { *; }

#不混淆某个类的构造方法
-keepclassmembers class name.huihui.example.Test { 
  public <init>(); 
}

#不混淆某个类的特定的方法
-keepclassmembers class name.huihui.example.Test { 
  public void test(java.lang.String); 
}

调整

混淆的规则大部分是固定的,然后重点在于我们要根据项目引用的框架、使用的技术去调整规则:

  • 第三方库所需的混淆规则。正规的第三方库一般都会在接入文档中写好所需混淆规则,使用时注意添加。

  • 在运行时动态改变的代码,例如反射。
    比较典型的例子就是会与 json 相互转换的实体类。假如项目命名规范要求实体类都要放在 Model 包下的话,可以添加类似这样的代码把所有实体类都保持住:
    -keep public class **.*Model*.** {*;}

  • JNI 中调用的类。

  • WebViewJavaScript 调用的方法

#保留 annotation, 例如 @JavascriptInterface 
-keepattributes *Annotation*

#保留跟 javascript 相关的属性 
-keepattributes JavascriptInterface

# #package# 为实际的包名
#-keepclassmembers #package#.JSInterface {
#    <methods>;
#}
  • Layout 布局使用的 View 构造函数、android:onClick 等。
# 保持自定义 View 的 get 和 set 相关方法
-keepclassmembers public class * extends android.view.View {
   void set*(***);
   *** get*();
}

  • 编译时问题
Note: there were 8 references to unknown classes.
You should check your configuration for typos.
([http://proguard.sourceforge.net/manual/troubleshooting.html#unknownclass](http://proguard.sourceforge.net/manual/troubleshooting.html#unknownclass))
Note: there were 272 unkept descriptor classes in kept class members.
You should consider explicitly keeping the mentioned classes
(using '-keep').
([http://proguard.sourceforge.net/manual/troubleshooting.html#descriptorclass](http://proguard.sourceforge.net/manual/troubleshooting.html#descriptorclass))
Note: there were 75 unresolved dynamic references to classes or interfaces.
You should check if you need to specify additional program jars.
([http://proguard.sourceforge.net/manual/troubleshooting.html#dynamicalclass](http://proguard.sourceforge.net/manual/troubleshooting.html#dynamicalclass))
Warning: there were 11 unresolved references to classes or interfaces.
You may need to add missing library jars or update their versions.
If your code works fine without the missing classes, you can suppress
the warnings with '-dontwarn' options.
([http://proguard.sourceforge.net/manual/troubleshooting.html#unresolvedclass](http://proguard.sourceforge.net/manual/troubleshooting.html#unresolvedclass))
Warning: Exception while processing task java.io.IOException: Please correct the above warnings first.

Warning 中提到有 11 个未解析的类或引用,并且给出两个解决方案;
方案一:你可能需要添加丢失的库或更新它们的版本。
方案二:如果你现在代码运行得好好的,也就是没有它们也没关系,那你可以使用 -dontwarn 来禁止这样的警告。

具体的类或引用会出现在日志中:

Warning: okhttp3.internal.platform.ConscryptPlatform: can't find referenced class org.conscrypt.OpenSSLProvider
Warning: okhttp3.internal.platform.ConscryptPlatform: can't find referenced class org.conscrypt.OpenSSLProvider
Warning: okhttp3.internal.platform.ConscryptPlatform: can't find referenced class org.conscrypt.Conscrypt
Warning: okhttp3.internal.platform.ConscryptPlatform: can't find referenced class org.conscrypt.Conscrypt
Warning: okhttp3.internal.platform.ConscryptPlatform: can't find referenced class org.conscrypt.Conscrypt
Warning: okhttp3.internal.platform.ConscryptPlatform: can't find referenced class org.conscrypt.Conscrypt
Warning: okhttp3.internal.platform.ConscryptPlatform: can't find referenced class org.conscrypt.Conscrypt
Warning: okhttp3.internal.platform.ConscryptPlatform: can't find referenced class org.conscrypt.Conscrypt
Warning: okhttp3.internal.platform.ConscryptPlatform: can't find referenced class org.conscrypt.Conscrypt
Warning: okhttp3.internal.platform.ConscryptPlatform: can't find referenced class org.conscrypt.Conscrypt
Warning: okhttp3.internal.platform.ConscryptPlatform: can't find referenced class org.conscrypt.Conscrypt

上述例子中类 okhttp3.internal.platform.ConscryptPlatform 引用了 org.conscrypt.OpenSSLProvider 但混淆压缩后找不到。如果项目中实际并没有用上类 org.conscrypt.OpenSSLProvider 就可以通过以下规则抑制警告:

-dontwarn org.conscrypt.*

-dontwarn okhttp3.internal.platform.ConscryptPlatform
  • 运行时问题
    这一块最麻烦的是,这种问题往往可能藏的比较深,所以除了彻底的回归测试外只有理解这其中的原理才能有效的减少问题。
    配置混淆规则时提到过,动态改变的代码、反射、js 调用等方法 / 类不能被混淆,否则在运行时会报错。
    具体比如 Retrofit 等网络框架或者用 Gson 解析 json 至实体类时,如果没有使用 -keep 标注实体类将会导致实体类的字段名被混淆而无法解析到预想的字段中。

部分参考自 写给 Android 开发者的混淆使用手册

推荐阅读更多精彩内容