前言
在开发中,混淆是相当重要的一个环节,任何一个 app 都应该开启代码混淆、资源压缩、移除无用资源。Android 的 SDK 提供了 ProGuard 来实现这一过程。ProGuard 会检测和移除封装应用中未使用的类、字段、方法和属性,包括自带代码库中的未使用项(这使其成为以变通方式解决 64k 引用限制的有用工具)。ProGuard 还可优化字节码,移除未使用的代码指令,以及用短名称混淆其余的类、字段和方法。混淆过的代码可令您的 APK 难以被逆向工程,这在应用使用许可验证等安全敏感性功能时特别有用。
ProGuard 简介
ProGuard 一共包括四个功能:
- 压缩(Shrink):检测并移除代码中无用的类、字段、方法和特性
- 优化(Optimize):对字节码进行优化,移除无用的指令
- 混淆(Obfuscate):使用 a、b、c、d 这样的名称,对类、字段和方法进行重命名
- 预检(Preveirfy):在 Java 平台上对处理后的代码进行预检
混淆由这个四个步骤构成,其中每个步骤也都是可选的,可通过配置脚本决定执行其中哪儿步骤。
混淆命令
- -keep {Modifier} {class_specification} 防止类和成员被移除或者被重命名
- -keepclassmembers {modifier} {class_specification} 防止成员被移除或者被重命名
- -keepclasseswithmembers {class_specification} 防止拥有该成员的类和成员被移除或者被重命名
- -keepnames {class_specification} 防止成员被重命名
- -keepclasseswithmembernames {class_specification} 防止拥有该成员的类和成员被重命名
- -keepclasseswithmembers
- optimizationpasses 指定压缩级别
- dontoptimize 不优化输入的类文件
- dontusemixedcaseclassnames 包名不混合大小写
- dontskipnonpubliclibraryclasses 不跳过非公共的库的类成员
- dontpreverify 混淆时不做预校验
- dontwarn 如果有警告也不终止
- verbose 混淆时记录日志
- optimizations 混淆时采用的算法
元素不参与混淆的规则
形如:
[保持命令] [类] {
[成员]
}
“类”代表类相关的限定条件,它将最终定位到某些符合该限定条件的类。它的内容可以使用:
- 具体的类
- 访问修饰符(public、protected、private)
- 通配符*,匹配任意长度字符,但不含包名分隔符(.)
- 通配符**,匹配任意长度字符,并且包含包名分隔符(.)
- extends,即可以指定类的基类
- implement,匹配实现了某接口的类
- $,内部类
“成员”代表类成员相关的限定条件,它将最终定位到某些符合该限定条件的类成员。它的内容可以使用:
- 匹配所有构造器
- 匹配所有域
- 匹配所有方法
- 通配符*,匹配任意长度字符,但不含包名分隔符(.)
- 通配符**,匹配任意长度字符,并且包含包名分隔符(.)
- 通配符*,匹配任意参数类型
- …,匹配任意长度的任意类型参数。比如void test(…)就能匹配任意 void test(String a) 或者是 void test(int a, String b) 这些方法。
- 访问修饰符(public、protected、private)
常用自定义混淆规则
- -keep public class com.proguard.example.Test { *; } 不混淆某个类
- -keep class com.bumptech.glide.* { ; } 不混淆某个包下面的所有类
- -keep public class * extends com.proguard.example.Test { *; } 不混淆某个类的子类
- -keep public class .model. {*;} 不混淆所有类名包含 model 的类及其成员
- -keep public class * extends android.os.IInterface 不混淆某个接口的实现
- -keepclassmembers class com.proguard.example.Test {public ();} 不混淆某个类的构造方法
- -keepclassmembers class * {void *(**On*Event);} 不混淆带有回调函数 OnXXEvent的方法
混淆配置
android {
buildTypes {
release {
minifyEnabled true
shrinkResources true
//Zipalign优化
zipAlignEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
一般混淆的时间比较长,所以只在 release 模式下开启。shrinkResources 是打开资源压缩。
通用混淆代码
#指定压缩级别
-optimizationpasses 5
#不跳过非公共的库的类成员
-dontskipnonpubliclibraryclassmembers
#混淆时采用的算法 后面的参数是一个过滤器,这个过滤器是谷歌推挤的算法
-optimizations !code/simplification/arithmetic,!code/simplification/cast,!field/*,!class/merging/*
#把混淆类中的方法名也混淆了
-useuniqueclassmembernames
#优化时允许访问并修改有修饰符的类和类的成员
-allowaccessmodification
#包明不混合大小写
-dontusemixedcaseclassnames
#不去忽略非公共的库类
-dontskipnonpubliclibraryclasses
#混淆时是否记录日志
-verbose
# 混淆时不做预校验 Android 不需要做 preverify,去掉可加快混淆速度
-dontpreverify
#将文件来源重命名为“SourceFile”字符串
-renamesourcefileattribute SourceFile
#保留行号
-keepattributes SourceFile,LineNumberTable,Deprecated
#忽略警告
-ignorewarning
#生成日志数据,gradle build时在本项目根目录输出
-dump class_files.txt #apk包内所有class的内部结构
-printseeds seeds.txt #未混淆的类和成员
-printusage unused.txt #打印未被使用的代码
-printmapping mapping.txt #混淆前后的映射
#保护注解
-keepattributes *Annotation*,InnerClasses,Deprecated
#避免混淆泛型 如果混淆报错建议关掉
-keepattributes Signature
-keepattributes EnclosingMethod
#保持 native 方法不被混淆
-keepclasseswithmembernames class * {
native <methods>;
}
#不提示兼容库的错误警告
-dontwarn android.support.**
#保持所有实现 Serializable 接口的类成员
-keepclassmembers class * implements java.io.Serializable {
static final long serialVersionUID;
private static final java.io.ObjectStreamField[] serialPersistentFields;
private void writeObject(java.io.ObjectOutputStream);
private void readObject(java.io.ObjectInputStream);
java.lang.Object writeReplace();
java.lang.Object readResolve();
}
# 保持测试相关的代码
-dontnote junit.framework.**
-dontnote junit.runner.**
-dontwarn android.test.**
-dontwarn android.support.test.**
-dontwarn org.junit.**
#Fragment不需要在AndroidManifest.xml中注册,需要额外保护下
-keep public class * extends android.support.v4.app.Fragment
-keep public class * extends android.app.Fragment
-keep public class * extends android.app.Activity
-keep public class * extends android.app.Application
-keep public class * extends android.app.Service
-keep public class * extends android.content.BroadcastReceiver
-keep public class * extends android.content.ContentProvider
-keep public class * extends android.app.backup.BackupAgentHelper
-keep public class * extends android.preference.Preference
-keep public class * extends android.view.View
-keep public class com.android.vending.licensing.ILicensingService
-keep class android.support.** {*;}
-keep public class * extends android.os.IInterface
-keep public class * extends android.view.View{
*** get*();
void set*(***);
public <init>(android.content.Context);
public <init>(android.content.Context, android.util.AttributeSet);
public <init>(android.content.Context, android.util.AttributeSet, int);
}
#不混淆资源类及其方法
-keep class **.R$* {
*;
}
# 对于带有回调函数onXXEvent的,不能混淆
-keepclassmembers class * {
void *(**On*Event);
}
# 保持自定义控件类不被混淆
-keepclasseswithmembers class * {
public <init>(android.content.Context, android.util.AttributeSet);
}
# 保持自定义控件类不被混淆
-keepclasseswithmembers class * {
public <init>(android.content.Context, android.util.AttributeSet, int);
}
# 保持自定义控件类不被混淆
-keepclassmembers class * extends android.app.Activity {
public void *(android.view.View);
}
-keepclassmembers enum * { # 保持枚举 enum 类不被混淆
public static **[] values();
public static ** valueOf(java.lang.String);
}
# 保持 Parcelable 不被混淆
-keep class * implements android.os.Parcelable {
public static final android.os.Parcelable$Creator *;
}
混淆检查
混淆过的包必须进行检查,避免因混淆引入的bug。一方面,需要从代码层面检查。使用上文的配置进行混淆打包后在 /build/outputs/mapping/release/ 目录下会输出以下文件:
- dump.txt 描述APK文件中所有类的内部结构
- mapping.txt 提供混淆前后类、方法、类成员等的对照表
- seeds.txt 列出没有被混淆的类和成员
- usage.txt 列出被移除的代码
我们可以根据 seeds.txt 文件检查未被混淆的类和成员中是否已包含所有期望保留的,再根据 usage.txt 文件查看是否有被误移除的代码。另一方面,需要从测试方面检查。将混淆过的包进行全方面测试,检查是否有 bug 产生。