声明
这篇文章,借鉴参考了下面的两篇文章,算是一个自己对混淆这块的总结。
写给Android开发者的混淆使用手册
Android混淆打包那些事儿
混淆
简介
说到混淆,就要说到proGuard,Android的混淆是有proGuard来完成的,ProGuard是一个开源项目在SourceForge上进行维护。
流程
代码混淆是包含了代码压缩、优化、混淆等一系列行为的过程。如上图所示,混淆过程会有如下几个功能:
- 压缩。移除无效的类、类成员、方法、属性等;
- 优化。分析和优化方法的二进制代码,移除无用指令;根据proguard-android-optimize.txt中的描述,优化可能会造成一些潜在风险,不能保证在所有版本的Dalvik上都正常运行。
- 混淆。把类名、属性名、方法名替换为简短且无意义的名称;
- 预校验。添加预校验信息。这个预校验是作用在Java平台上的,Android平台上不需要这项功能,去掉之后还可以加快混淆速度。
这四个流程默认开启。
如何混淆
混淆配置
一般的,我们在build.gradle都会这样写:
buildTypes {
release {
zipAlignEnabled true
minifyEnabled true
// 移除无用的resource文件
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.xxxxx
}
}
minifyEnabled true
设为true开启混淆,proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
设置混淆文件的配置。其中proguard-android.txt
是Android自带的混淆配置,我们可以在SDK的sdk\tools\proguard
目录找到该文件,里边配置了一些基本需要的混淆。proguard-rules.pro
是我们要添加的自己的混淆配置,比如项目中引用了第三方的项目等。
proguard-android.txt
的内容:
# This is a configuration file for ProGuard.
# http://proguard.sourceforge.net/index.html#manual/usage.html
-dontusemixedcaseclassnames
-dontskipnonpubliclibraryclasses
-verbose
# Optimization is turned off by default. Dex does not like code run
# through the ProGuard optimize and preverify steps (and performs some
# of these optimizations on its own).
-dontoptimize
-dontpreverify
# Note that if you want to enable optimization, you cannot just
# include optimization flags in your own project configuration file;
# instead you will need to point to the
# "proguard-android-optimize.txt" file instead of this one from your
# project.properties file.
-keepattributes *Annotation*
-keep public class com.google.vending.licensing.ILicensingService
-keep public class com.android.vending.licensing.ILicensingService
# For native methods, see http://proguard.sourceforge.net/manual/examples.html#native
-keepclasseswithmembernames class * {
native <methods>;
}
# keep setters in Views so that animations can still work.
# see http://proguard.sourceforge.net/manual/examples.html#beans
-keepclassmembers public class * extends android.view.View {
void set*(***);
*** get*();
}
# We want to keep methods in Activity that could be used in the XML attribute onClick
-keepclassmembers class * extends android.app.Activity {
public void *(android.view.View);
}
# For enumeration classes, see http://proguard.sourceforge.net/manual/examples.html#enumerations
-keepclassmembers enum * {
public static **[] values();
public static ** valueOf(java.lang.String);
}
-keepclassmembers class * implements android.os.Parcelable {
public static final android.os.Parcelable$Creator CREATOR;
}
-keepclassmembers class **.R$* {
public static <fields>;
}
# The support library contains references to newer platform versions.
# Don't warn about those in case this app is linking against an older
# platform version. We know about them, and they are safe.
-dontwarn android.support.**
# Understand the @Keep support annotation.
-keep class android.support.annotation.Keep
-keep @android.support.annotation.Keep class * {*;}
-keepclasseswithmembers class * {
@android.support.annotation.Keep <methods>;
}
-keepclasseswithmembers class * {
@android.support.annotation.Keep <fields>;
}
-keepclasseswithmembers class * {
@android.support.annotation.Keep <init>(...);
}
一般的proguard-android.txt文件中已经有的混淆配置,我们可以不再添加,当然写了也没有问题!
查看混淆结果
混淆过的包必须进行检查,避免因混淆引入的bug。
一方面,需要从代码层面检查。
使用上文的配置进行混淆打包后在·app/build/outputs/mapping/release/
目录下会输出以下文件:
- dump.txt
描述APK文件中所有类的内部结构 - mapping.txt
提供混淆前后类、方法、类成员等的对照表 - seeds.txt
列出没有被混淆的类和成员 - usage.txt
列出被移除的代码
我们可以根据 seeds.txt 文件检查未被混淆的类和成员中是否已包含所有期望保留的,再根据 usage.txt 文件查看是否有被误移除的代码。
另一方面,需要从测试方面检查
将混淆过的包进行全方面测试,检查是否有 bug 产生。这时候解出混淆栈。
混淆后的类、方法名等等难以阅读,这固然会增加逆向工程的难度,但对追踪线上 crash 也造成了阻碍。我们拿到 crash 的堆栈信息后会发现很难定位,这时需要将混淆反解。
在 sdk/tools/proguard/
路径下有附带的的反解工具(Window 系统为 proguardgui.bat
,Mac 或 Linux 系统为 proguardgui.sh
)。
这里以 Window 平台为例。双击运行proguardgui.bat
后,可以看到左侧的一行菜单。点击 ReTrace,选择该混淆包对应的 mapping 文件(混淆后在 app/build/outputs/mapping/release/
路径下会生成 mapping.txt
文件,它的作用是提供混淆前后类、方法、类成员等的对照表),再将 crash 的 stack trace 黏贴进输入框中,点击右下角的 ReTrace ,混淆后的堆栈信息就显示出来了。
以上使用 GUI 程序进行操作,另一种方式是利用该路径下的 retrace 工具通过命令行进行反解,命令是
retrace.bat|retrace.sh [-verbose] mapping.txt [<stacktrace_file>]
例如:
retrace.bat -verbose mapping.txt obfuscated_trace.txt
自定义的混淆配置
保持元素不被混淆的相关命令
-keep
防止类和成员被移除或者被重命名-keepnames
防止类和成员被重命名,但当成员没有被引用时会被移除-keepclassmembers
防止成员被移除或者被重命名-keepclassmembernames
防止成员被重命名,但当成员没有被引用时会被移除-keepclasseswithmembers
防止拥有该成员的类和成员被移除或者被重命名-keepclasseswithmembernames
防止拥有该成员的类和成员被重命名
保持元素不被混淆的相关规则
规则形如:
[保持命令] [类] {
[成员]
}
①保持命令:就是指上边说的几个保持命令
②类:类相关的限定条件,它将最终定位到某些符合该限定条件的类。它的内容可以使用:
- 具体的类
- 访问修饰符(public、protected、private)
- 通配符
*
,匹配任意长度字符,但不含包名分隔符(.) - 通配符
**
,匹配任意长度字符,并且包含包名分隔符(.) extends
,即可以指定类的基类implement
,匹配实现了某接口的类$
,内部类
③成员:代表类成员相关的限定条件,它将最终定位到某些符合该限定条件的类成员。它的内容可以使用:
<init>
匹配所有构造器<fields>
匹配所有域<methods>
匹配所有方法- 通配符
*
,匹配任意长度字符,但不含包名分隔符(.) - 通配符
**
,匹配任意长度字符,并且包含包名分隔符(.) - 通配符
***
,匹配任意参数类型 …
,匹配任意长度的任意类型参数。比如void test(…)就能匹配任意 void test(String a) 或者是 void test(int a, String b) 这些方法。- 访问修饰符(public、protected、private)
通用的混淆规则
混淆的时候,可以直接复制:
#不优化输入的类文件
-dontoptimize
-dontwarn android.support.**
#keep相关注解
-keep class android.support.annotation.Keep
-keep @android.support.annotation.Keep class * {*;}
-keepclasseswithmembers class * {
@android.support.annotation.Keep <methods>;
}
-keepclasseswithmembers class * {
@android.support.annotation.Keep <fields>;
}
-keepclasseswithmembers class * {
@android.support.annotation.Keep <init>(...);
}
# 代码混淆压缩比,在0~7之间,默认为5,一般不下需要修改
-optimizationpasses 5
# 混淆时不使用大小写混合,混淆后的类名为小写
# windows下的同学还是加入这个选项吧(windows大小写不敏感)
-dontusemixedcaseclassnames
# 指定不去忽略非公共的库的类
# 默认跳过,有些情况下编写的代码与类库中的类在同一个包下,并且持有包中内容的引用,此时就需要加入此条声明
-dontskipnonpubliclibraryclasses
# 指定不去忽略非公共的库的类的成员
-dontskipnonpubliclibraryclassmembers
# 不做预检验,preverify是proguard的四个步骤之一
# Android不需要preverify,去掉这一步可以加快混淆速度
-dontpreverify
# 有了verbose这句话,混淆后就会生成映射文件
# 包含有类名->混淆后类名的映射关系
# 然后使用printmapping指定映射文件的名称
-verbose
-printmapping priguardMapping.txt
# 指定混淆时采用的算法,后面的参数是一个过滤器
# 这个过滤器是谷歌推荐的算法,一般不改变
-optimizations !code/simplification/artithmetic,!field/*,!class/merging/*
# 保护代码中的Annotation不被混淆
# 这在JSON实体映射时非常重要,比如fastJson
-keepattributes *Annotation*
# 避免混淆泛型
# 这在JSON实体映射时非常重要,比如fastJson
-keepattributes Signature
#将文件来源重命名为“SourceFile”字符串
-renamesourcefileattribute SourceFile
# 抛出异常时保留代码行号
-keepattributes SourceFile,LineNumberTable
# 保留所有的本地native方法不被混淆
-keepclasseswithmembernames class * {
native <methods>;
}
#把混淆类中的方法名也混淆了
-useuniqueclassmembernames
#优化时允许访问并修改有修饰符的类和类的成员
-allowaccessmodification
# 保留了继承自Activity、Application这些类的子类
# 因为这些子类有可能被外部调用
# 比如第一行就保证了所有Activity的子类不要被混淆
-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
#Fragment不需要在AndroidManifest.xml中注册,需要额外保护下
-keep public class * extends android.support.v4.app.Fragment
-keep public class * extends android.app.Fragment
# 保持测试相关的代码
-dontnote junit.framework.**
-dontnote junit.runner.**
-dontwarn android.test.**
-dontwarn android.support.test.**
-dontwarn org.junit.**
# 保留Activity中的方法参数是view的方法,
# 从而我们在layout里面编写onClick就不会影响
-keepclassmembers class * extends android.app.Activity {
public void * (android.view.View);
}
# 枚举类不能被混淆
-keepclassmembers enum * {
public static **[] values();
public static ** valueOf(java.lang.String);
}
# 保留自定义控件(继承自View)不能被混淆
-keep public class * extends android.view.View {
public <init>(android.content.Context);
public <init>(android.content.Context, android.util.AttributeSet);
public <init>(android.content.Context, android.util.AttributeSet, int);
public void set*(***);
*** get* ();
}
# 保留Parcelable序列化的类不能被混淆
-keep class * implements android.os.Parcelable{
public static final android.os.Parcelable$Creator *;
}
# 保留Serializable 序列化的类不被混淆
-keepclassmembers class * implements java.io.Serializable {
static final long serialVersionUID;
private static final java.io.ObjectStreamField[] serialPersistentFields;
!static !transient <fields>;
private void writeObject(java.io.ObjectOutputStream);
private void readObject(java.io.ObjectInputStream);
java.lang.Object writeReplace();
java.lang.Object readResolve();
}
# 对R文件下的所有类及其方法,都不能被混淆
-keepclassmembers class **.R$* {
*;
}
# 对于带有回调函数onXXEvent的,不能混淆
-keepclassmembers class * {
void *(**On*Event);
}
#实体类不能被混淆
-keep class com.test.beans.** {*;}
#忽略get和set方法
-keep class com.test.beans.** {
public void set*(***);
public *** get*();
public *** is*();
}
#以上两种任意一种都行
#对于内部类,$就是用来分割内嵌类和母体的标志
#-keep class com.test.**$*{*;}
#保留support下的所有类及其内部类
-keep class android.support.** {*;}
#不需要提示 警告
-dontwarn android.support.**
# support-v7-appcompat
-keep public class android.support.v7.widget.** { *; }
-keep public class android.support.v7.internal.widget.** { *; }
-keep public class android.support.v7.internal.view.menu.** { *; }
-keep public class * extends android.support.v4.view.ActionProvider {
public <init>(android.content.Context);
}
# support-design
-dontwarn android.support.design.**
-keep class android.support.design.** { *; }
-keep interface android.support.design.** { *; }
-keep public class android.support.design.R$* { *; }
其他混淆规则
OkHttp3
-dontwarn okhttp3.logging.**
-keep class okhttp3.internal.**{*;}
-dontwarn okio.**
Retrofit2.x
-dontwarn retrofit2.**
-keep class retrofit2.** { *; }
RxJava RxAndroid
-dontwarn sun.misc.**
-keepclassmembers class rx.internal.util.unsafe.*ArrayQueue*Field* {
long producerIndex;
long consumerIndex;
}
-keepclassmembers class rx.internal.util.unsafe.BaseLinkedQueueProducerNodeRef {
rx.internal.util.atomic.LinkedQueueNode producerNode;
}
-keepclassmembers class rx.internal.util.unsafe.BaseLinkedQueueConsumerNodeRef {
rx.internal.util.atomic.LinkedQueueNode consumerNode;
}
Gson
-keep class com.google.gson.** { *; }
-keepattributes EnclosingMethod
Glide
-keep public class * implements com.bumptech.glide.module.GlideModule
-keep public enum com.bumptech.glide.load.resource.bitmap.ImageHeaderParser$** {
**[] $VALUES;
public *;
}
# for DexGuard only
-keepresourcexmlelements manifest/application/meta-data@value=GlideModule
ButterKnife
-keep class butterknife.** { *; }
-dontwarn butterknife.internal.**
-keep class **$$ViewBinder { *; }
-keepclasseswithmembernames class * {
@butterknife.* <fields>;
}
-keepclasseswithmembernames class * {
@butterknife.* <methods>;
}
资源压缩
资源压缩将移除项目及依赖的库中未被使用的资源,这在减少 apk 包体积上会有不错的效果,一般在打realease包的时候建议开启。
具体做法是在 build.grade 文件中,将shrinkResources
属性设置为 true。需要注意的是,只有在用minifyEnabled true
开启了代码压缩后,资源压缩才会生效。
资源压缩包含了“合并资源”和“移除资源”两个流程。
合并资源
“合并资源”流程中,名称相同的资源被视为重复资源会被合并。需要注意的是,这一流程不受shrinkResources属性控制,也无法被禁止, gradle 必然会做这项工作,因为假如不同项目中存在相同名称的资源将导致错误。gradle 在四处地方寻找重复资源:
①src/main/res/ 路径
②不同的构建类型(debug、release等等)
③不同的构建渠道
④项目依赖的第三方库
合并资源时按照如下优先级顺序:
依赖 -> main -> 渠道 -> 构建类型
举个例子,
假如重复资源同时存在于main文件夹和不同渠道中,gradle 会选择保留渠道中的资源。同时,如果重复资源在同一层次出现,比如src/main/res/
和 src/main/res2/
,则 gradle 无法完成资源合并,这时会报资源合并错误。
移除资源
资源移除的时候,跟代码混淆一样,也可以定义哪些资源需要被保留。
保持某些资源不被移除
用shrinkResources true
开启资源压缩后,所有未被使用的资源默认被移除。假如你需要定义哪些资源必须被保留,在 res/raw/ 路径下创建一个 xml 文件,例如 keep.xml。
通过一些属性的设置可以实现定义资源保持的需求,可配置的属性有:
tools:keep
定义哪些资源需要被保留(资源之间用“,”隔开)tools:discard
定义哪些资源需要被移除(资源之间用“,”隔开)tools:shrinkMode
开启严格模式
当代码中通过 Resources.getIdentifier()
用动态的字符串来获取并使用资源时,普通的资源引用检查就可能会有问题。例如,如下代码会导致所有以“img_”
开头的资源都被标记为已使用。
String name = String.format("img_%1d", angle + 1);
res = getResources().getIdentifier(name, "drawable", getPackageName());
我们可以设置 tools:shrinkMode
为 strict 来开启严格模式,使只有确实被使用的资源被保留。
以上就是自定义资源保持规则相关的配置,举个例子:
<?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"
tools:shrinkMode="strict"/>
移除替代资源
一些替代资源,例如多语言支持的 strings.xml
,多分辨率支持的 layout.xml
等,在我们不需要使用又不想删除掉时,可以使用资源压缩将它们移除。
我们使用 resConfig
属性来指定需要支持的属性,例如
android {
defaultConfig {
...
resConfigs "en", "fr"
}
}
其他未显式声明的语言资源将被移除。
好了,Android混淆相关就下总结到这里!
淘到一个在线的混淆的网站,可以参考一下!
https://proguard.herokuapp.com/