Merit
- smaller APK size (20% ~ 40%)
- more difficult to reverse engineer
Demerit
- unable to get into step-by-step debug mode
- risk of making reflection failures
- risk of introduce unexpected bugs
- may loss bug tracking of obfuscated code
- may break down the Smart App Updates
- more cost on rules maintaince and fully test
Discussion on Smart App Updates:
Why are the updates still in MBs?! Understandable for major updates, but I'm hoping when an app has only bug fixes, it would be a very, very small update.
It's because of the way code gets compiled. When devs compile their Java code into an APK, they also use a tool called "Proguard," which is used to optimize and obfuscate their code. It's what makes it harder for other people to just decompile apps and steal work, and also slims down APKs and makes them run slightly faster. Unfortunately, Proguard obfuscates code differently depending on what changes you've made. While a developer might only change a line, the obfuscated and optimized version might actually have changed quite a bit.
see:
ProGuard配置
常规启用
导入 ProGuard 规则文件:
build.gradle: buildTypes {
release {
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
minifyEnabled true
表示使用 ProGuard 进行混淆
shrinkResources true
表示精简未使用的资源文件(不包括 values),基于代码精简化后的结果进行引用判断(包括动态计算获取资源ID等隐式资源调用)
getDefaultProguardFile
表示在以下 SDK 位置读取指定的默认配置:
Android SDK -> Android SDK Location /tools/proguard/
该配置包含绝大部分默认必备选项,并自动移除未使用代码,但涉及以下情况的代码,需要通过 -keep
配置或 @Keep
注解,手动声明进行保留:
- 仅通过 AndroidManifest.xml 引用的类
- 通过 JNI 调用的方法
- 通过反射等方式,在运行时动态操作代码
ProGuard 会自动重命名类、方法、字段等,通过反射等方式,在运行时动态引用类型、字段、资源时需要特别注意,根据需要手动声明保留:
- 在 GSON 等框架中进行自动类型转换的 Model 类(保留字段名,或使用
@SerializedName
显式绑定字段注入) - 在 XML 中引用的第三方View类(可能触发 InflateException)
- 在 JNI 中反调 Java 的方法函数
- 在 JS 等外部代码中回调的 Java 方法函数
- Serializable 实现类的相关序列化方法名
- 需要静态配置文件控制的 Bean 类
- Database drivers
proguard-android.txt 内置实现以下部分的处理:
- native方法、枚举方法、注解
- R类、Parcelable、动画XML对View的getter/setter、@Keep、support库
每次编译时,ProGuard 会输出以下文件:
-
dump.txt
:描述 APK 内所有 class 文件的内部结构 -
mapping.txt
:原始代码与混淆代码的名称映射表,可用于 Google Play 的反混淆处理 -
seeds.txt
:列出未被混淆的所有类和成员 -
usage.txt
:列出在 APK 中被移除的代码
※ 以上文件存储到该目录:
<module-name>/build/outputs/mapping/release/
注意
在使用 Instant Run 时,ProGuard 会被自动临时禁用。(Android Studio 2.0 & minSdkVersion >= 21)
productFlavors 与 buildTypes 中指定的 proguardFiles、proguardFile 会同时叠加生效,并且 buildTypes > productFlavors
build.gradle 配置了 consumerProguardFiles
项的第三方库,其引用的规则文件会自动引入到当前项目。
如果一个包中有需要不混淆的内容,则整个包名都不会被混淆。
proguard-android.txt
使用了 dontoptimize
,优化不启用,-assumenosideeffects
不会生效。可使用 proguard-android-optimize.txt
替代。
移除重复资源
Gradle 方面会强制自动合并重复的资源项(同名+同类型+同限定属性),不受 ProGuard 的配置影响。
重复资源项按序扫描以下目录,并选择保留最后扫描到的重复项:
- 项目库依赖目录
- 当前资源主目录,通常指向为:
src/main/res/
-
Build flavor
配置目录 -
Build type
配置目录
合并重复资源时,Gradle 会生成 resources.txt
文件,列出资源的依赖关系,及被移除的资源列表。
该文件在:<module-name>/build/outputs/mapping/release/
目录中
常用规则
-keepattributes Signature 保留泛型类型信息
-keepattributes SourceFile,LineNumberTable 保留文件名、行号,用于崩溃反馈
-keep class com.xxx.** { *; } 保留第三方类库源码
移除Log代码 需要启用Optimization(不使用 -dontoptimize)
-assumenosideeffects class android.util.Log {
public static *** d(...);
}
参考:
- ProGuard的使用方式 - Android Developer:Google官方指导
- ProGuard FAQ:包含相关术语解释、基本问题答案
- ProGuard Manual:ProGuard 完全使用手册
- ProGuard Troubleshooting:错误及解决方案查询目录
- Proguard configuration:高能推荐,Proguard 与多个常用类库的集成配置指导
- Configuring ProGuard:另一份配置指导
- ProGuard 保守配置指导
- ProGuardによるクラッシュ・不具合を正しく回避する
- Android Proguard(混淆) - 简书
- GSON proguard.cfg
- Crashlytics Configure ProGuard and DexGuard
ProGuard 过程
Shrink -> Optimize -> Obfuscate -> Preverify
保留规则
以下规则不对类成员执行 Shrink 自动移除
-
-keep
:完整保留指定类及指定成员(不论是否持有该成员) -
-keepclassmembers
:保留指定类的指定成员(类名依然接受混淆) -
-keepclasseswithmembers
:精确指定,仅对拥有指定的成员的类,进行完整保留
以下规则在 Shrink 过程执行完毕后生效
-
-keepnames
:对剩余的指定成员及其宿主类进行命名保留 -
-keepclassmembernames
:对指定类的剩余指定成员进行命名保留(类名依然接受混淆) -
-keepclasseswithmembernames
:精确指定,对仍然持有指定的成员的类,保留该类及其剩余所有成员的命名
可选的附件条件
-
allowshrinking
:可执行 Shrink 过程,例如-keepnames
=-keep,allowshrinking
-
allowoptimization
:可执行 Optimize 过程,默认被保留的对象不执行优化(优化过程可能会裁减掉该对象),仅用于实现极端需求 -
allowobfuscation
:可执行 Obfuscate 过程,默认被保留的对象不执行混淆(直接导致-keepnames
类保留变得没有意义),仅用于实现极端需求
-printseeds [filename]
:打印被上述 keep 规则匹配保留的类及类成员,或输出到指定文件
※1 仅指定类,不指定成员时,仅对类名本身生效
※2 指定的类成员,仅对其名称生效,不影响其相关逻辑代码的后续优化、适配过程
类、成员指定条件
# 指定类型(必选)
[@annotationtype] [[!]public|final|abstract|@ ...] [!]interface|class|enum classname
[extends|implements [@annotationtype] classname]
# 指定成员(可选)
[{
[@annotationtype] [[!]public|private|protected|static|volatile|transient ...] <fields> | (fieldtype fieldname);
[@annotationtype] [[!]public|private|protected|static|synchronized|native|abstract|strictfp ...] <methods> | <init>(argumenttype,...) | classname(argumenttype,...) | (returntype methodname(argumenttype,...));
[@annotationtype] [[!]public|private|protected|static ... ] *;
...
}]
通用指定
- 可选指定是否包含特定注解标记
- 可选使用“非”限定符
!
进行反向指定 - 可使用任意限定修饰符,并可组合指定,对于存在冲突的组合,则表示匹配对象必须符合其中任意一个指定
指定类型(必选)
- 可指定接口、类、枚举类型(指定内部类时,按 Java 语法使用
$
进行定位) - 可指定该类型派生、实现的上级类型
- 类型名必须以全限定名形式标记,如
java.lang.String
- 类型名可通过逗号 , 列表方式并列指定,但不符合 Java 声明范式,需要有节制的使用
-
extends
与implements
仅在语义上有区别,使用上作用一致
类型名通配符
-
?
匹配任意单个字符,不包括包名分隔符.
,如mypackage.Test?
-
*
匹配任意长度字符,不包括包名分隔符.
,如mypackage.*Test*
-
**
匹配任意长度字符,可包括包名及其分隔符.
,如**.Test
※ 单以 *
标记类型名,表示匹配所有类型名,不考虑包地址
指定成员(可选)
- 可指定任意数量成员,每个指定规则末尾使用分号
;
标记 - 方法成员参数列表仅需列出参数类型
- 字段和方法成员名,可使用正则表达式标记
- 构造函数可通过短类名或全限定类名匹配指定
成员名通配符
-
<init>
匹配构造函数,可指定参数列表 -
<fields>
匹配任意字段成员 -
<methods>
匹配任意方法成员,不可指定参数列表 -
?
匹配任意单个字符 -
*
匹配任意字段或方法成员,或任意长度字符
成员类型描述语句中的类型通配符
-
%
表示任意基本类型 -
?
匹配任意单个字符,不匹配基本类型 -
*
匹配任意长度类型名,不匹配基本类型,不包括包名分隔符.
-
**
匹配任意长度类型名,不匹配基本类型,可包括包名及其分隔符.
-
***
匹配任意类型,包括基本类型、引用类型、数组和非数组 -
...
匹配任意数量的方法参数
※1 基本类型可用 %
或 ***
匹配
※2 数组类型仅可使用 ***
匹配
DexGuard
ProGuard 的进阶版替换方案,除基本代码混淆优化外,更提供了算式混淆、控制流混淆、本地代码混淆、反射回调、类型加密、本地库加密、字符串加密、资源混淆加密、证书检查、debug和模拟器检测等高阶保护过程,可有效阻止apktool完整反编译过程,收费,至少350欧。