一、为什么进行混淆
Java 是一种跨平台的、解释型语言,Java 源代码编译成中间”字节码”存储于 class 文件中。由于跨平台的需要,Java 字节码中包括了很多源代码信息,如变量名、方法名,并且通过这些名称来访问变量和方法,这些符号带有许多语义信息,很容易被反编译成 Java 源代码。为了防止这种现象,我们可以使用 Java 混淆器对 Java 字节码进行混淆。
通过代码混淆可以将项目中的类、方法、变量等信息进行重命名,变成一些无意义的简短名字,同时也可以移除未被使用的类、方法、变量等。所以直观的看,通过混淆可以提高程序的安全性,增加逆向工程的难度,同时也有效缩减了apk的体积。
二、原理
对发布出去的程序进行重新组织和处理,使得处理后的代码与处理前代码完成相同的功能,而混淆后的代码很难被反编译,即使反编译成功也很难得出程序的真正语义。被混淆过的程序代码,仍然遵照原来的档案格式和指令集,执行结果也与混淆前一样,只是混淆器将代码中的所有变量、函数、类的名称变为简短的英文字母代号,在缺乏相应的函数名和程序注释的况下,即使被反编译,也将难以阅读。同时混淆是不可逆的,在混淆的过程中一些不影响正常运行的信息将永久丢失,这些信息的丢失使程序变得更加难以理解。
三、如何进行混淆
Android SDK 自带了混淆工具Proguard。它位于
proguard 就是可以把方法,字段,包和类这些java 元素的名称改成无意义的名称,这样代码结构没有变化,还可以运行,但是想弄懂代码的架构却很难的混淆工具。它可以分析一组class 的结构,根据用户的配置,然后把这些class 文件的可以混淆java 元素名混淆掉。在分析class 的同时,他还有其他两个功能,删除无效代码(Shrinking 收缩),和代码进行优化 (Optimization Options)。
如果开启了混淆,Proguard默认情况下会对所有代码,包括第三方包都进行混淆,可是有些代码或者第三方包是不能混淆的,这就需要我们手动编写混淆规则来保持不能被混淆的部分。
直接在android项目的build.gradle 文件中配置就可以混淆代码了:
android {
buildTypes {
release {
shrinkResources true
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
1> 代码混淆:
配置如 minifyEnabled true。
代码混淆包括4个步骤:压缩, 优化, 混淆, 预校验。
①压缩(shrink):移除无用的类,类的成员,方法,属性等;
②优化(optimize):分析和优化二进制代码,根据Proguard-android-optimize.txt 中的描述,优化可能会造成一定的风险,不能保证在所有版本上的Dalvik正常运行。因此android项目建议关闭该项。
③混淆(obfuscate):把类名,属性名,方法名替换为简短且无意义的名称:使用a、b、c、d这样简短而无意义的名称,对类、字段和方法进行重命名。
④预校验(previrfy):添加预校验信息。这个预校验是作用在java平台的,android平台不需要这个功能,去掉之后还可以加快混淆速度。因此android项目关闭该功能。
这4个流程默认是开启的,我们需要在android的混淆配置文件中关闭代码优化和预校验功能,即 -dontoptimize, -dontpreverify。(在 proguard-android.txt中默认已经关闭了这2项)
2> 资源混淆(删除没有引用资源):
配置如 shrinkResources true
。可以减少apk的大小,一般建议开启。
在应用构建打包的过程中自动删除没有引用的资源,对依赖的库同样有效。shrinkResources必须和minifyEnabled配合使用达到减少包体积的作用,只有删除了无用的代码之后,才能知道哪些资源是无用的。
资源处理包括2个流程:合并资源,移除资源;
①android studio打包时会自动merge资源,不受参数控制。
②移除资源,需要配置参数。(建议可以先用lint自行过一遍)
四、默认基本混淆配置
-
proguard-android.txt
代表系统默认的混淆规则配置文件,该文件在
下,这里面是一些比较常规的不能被混淆的代码规则。一般不要更改该配置文件,因为也会作用于其它项目,除非你能确保所做的更改不影响其它项目的混淆。/tools/proguard -
proguard-rules.pro
代码表当前project
的混淆配置文件,是针对我们自己的项目需要特别定义混淆规则。它在app module
下,可以通过修改该文件来添加适用当前项目的混淆规则。
在
proguard-android.txt这个文件在Android Gradle插件2.2+以上不再不再维护和使用了。相反,Android Gradle插件在构建时生成默认规则,并将它们存储在build目录中。所以说现在混淆的默认规则是由Gradle自动生成的。(例如:...\build\intermediates\proguard-files\proguard-android.txt-4.2.2这个文件就是gradle版本生成的,在新版本中,通过
proguardFiles getDefaultProguardFile('proguard-android.txt')获取到的就是这个文件中的混淆规则
)
系统的proguard-android.txt 中内容:
# This is a configuration file for ProGuard.
# http://proguard.sourceforge.net/index.html#manual/usage.html
# 混淆时不使用大小写混合类名
-dontusemixedcaseclassnames
# 不跳过library中的非public的类
-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
# 不混淆包含native方法的类的类名以及native方法名
-keepclasseswithmembernames class * {
native ;
}
# keep setters in Views so that animations can still work.
# see http://proguard.sourceforge.net/manual/examples.html#beans
# 不混淆View中的setXxx()和getXxx()方法,以保证属性动画正常工作
-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
# 不混淆Activity中参数是View的方法,例如,一个控件通过android:onClick="clickMethodName"绑定点击事件,混淆后会导致点击事件失效
-keepclassmembers class * extends android.app.Activity {
public void *(android.view.View);
}
# For enumeration classes, see http://proguard.sourceforge.net/manual/examples.html#enumerations
# 不混淆枚举类中的values()和valueOf()方法
-keepclassmembers enum * {
public static **[] values();
public static ** valueOf(java.lang.String);
}
# 不混淆Parcelable实现类中的CREATOR字段,以保证Parcelable机制正常工作
-keepclassmembers class * implements android.os.Parcelable {
public static final android.os.Parcelable$Creator CREATOR;
}
# 不混淆R文件中的所有静态字段,以保证正确找到每个资源的id
-keepclassmembers class **.R$* {
public static ;
}
# 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.
# 不对android.support包下的代码警告(如果我们打包的版本低于support包下某些类的使用版本,会出现警告的问题)
-dontwarn android.support.**
# Understand the @Keep support annotation.
# 不混淆Keep类
-keep class android.support.annotation.Keep
# 不混淆使用了注解的类及类成员
-keep @android.support.annotation.Keep class * {*;}
# 如果类中有使用了注解的方法,则不混淆类和类成员
-keepclasseswithmembers class * {
@android.support.annotation.Keep ;
}
# 如果类中有使用了注解的字段,则不混淆类和类成员
-keepclasseswithmembers class * {
@android.support.annotation.Keep ;
}
# 如果类中有使用了注解的构造函数,则不混淆类和类成员
-keepclasseswithmembers class * {
@android.support.annotation.Keep (...);
}
五、混淆配置的语法
可以从上面的代码中看出proguard-android.txt
主要作用是防止指定内容被混淆,其中使用了以-
开头,结合keep
类关键字,*
、<>
等通配符的语法。
下面我们来看看混淆相关项:
1> 混淆常见关键字:
关键字 | 含义 |
---|---|
keep | 保留 类和类成员,防止混淆或移除 |
keepnames | 保留 类和类和类成员,防止被混淆,但是没有引用的类成员会被移除 |
keepclassmembers | 只保留 类成员,防止混淆或移除 |
keepclassmembernames | 只保留 类成员,防止混淆,但没有引用的类成员会被移除 |
keepclasseswithmembers | 保留 类和类成员 ,防止被混淆或移除,如果指定的类成员不存还是会被混淆 |
keppclasseswithmembernames | 保留 类和类成员,防止被混淆,如果指定类成员不存在还是会被混淆,没有被引用的类成员会被移除 |
dontwarn | dontwarn 基本和keep同时出现,尤其是在引入 library时,为了忽略library的警告,保证build的正常进行 |
-dontwarn 主要是避免警告,-keep 主要是保留不被混淆
2> 相关通配符:
通配符 | 含义 |
---|---|
* | 匹配任意长度字符,但不包含分隔符 . 。例如一个类全路径是com.heytap.test.demo,使用com.heytap.test .* 就可以匹配,但是com.heytap.* 就不能匹配 |
** | 匹配任意长度字符,并包含分隔符 . 。例如: com.heytap.test.**可以匹配包下的所有内容 |
*** | 匹配任意参数类型。例如: *** getName(***)可以匹配String getName (String) |
... | 匹配任意长度的任意参数类型。 例如: void setName(...) 可以匹配 void setName(String name0,String name1,String name2) |
匹配类、接口中所有字段 | |
匹配类、接口中所有方法 | |
匹配所有的构造函数 |
六、混淆注意问题
混淆配置官方文档:gradle example
下面是关于混淆配置时要注意的问题:
1、运用了反射的类也不进行混淆。因为代码混淆,类名、方法名、属性名都改变了,而反射它还是按照原来的名字去反射,结果程序崩溃。
2、注解不能混淆。因为注解也用到了java反射,所以不能混淆。
3、Activity不能混淆。因为AndroidManifest.xml文件中是完整的名字
4、自定义View不能混淆。因为被Android Resource 文件引用到的,名字已经固定,也不能混淆。自定义view是带了包名写在xml布局中的。
5、使用了 Gson 之类的工具要使 JavaBean 类即实体类不能混淆。因为json转换用到了java反射。
6、泛型不能混淆。
7、自定义控件类的get/set方法和构造函数不能混淆。
8、内部类,如果会被外部调用到,那么也不能混淆。
9、使用了枚举要保证枚举不被混淆
10、对第三方库中的类不进行混淆
11、不混淆任何包含native方法的,否则找不到本地方法。
12、属性动画兼容库不能混淆。
13、在引用第三方库的时候,一般会标明库的混淆规则的,建议在使用的时候就把混淆规则添加上去,免得到最后才去找
14、有用到 WebView 的 JS 调用也需要保证写的接口方法不混淆,原因和第一条一样
15、Parcelable 的子类和 Creator 静态成员变量不混淆,否则会产生 Android.os.BadParcelableException 异常
16、不混淆Rxjava/RxAndroid
17、对于Android APP来说四大组件和Application不能混淆。现在的系统已经配置为混淆时候会保留Android系统组件
18、数据库驱动
19、其他Anroid 官方建议 不混淆的,如
android.app.backup.BackupAgentHelper
android.preference.Preference
com.android.vending.licensing.ILicensingService
注意:如果你的Android SDK Tools版本足够高(>24),那么在proguard-rules.pro文件其实不用做任何改动,因为Google已经帮我们在proguard-android.txt文件配置好了(如果较低就把下面代码拷贝到proguard-android.txt中)。
混淆代码配置参考:
1.>不混淆,需要保留的东西:
#
保留了继承自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
#
如果有引用android-support-v4.jar包,可以添加下面这行
-keep public class com.null.test.ui.fragment.** {*;}
#
保留Activity中的方法参数是view的方法,
#
从而我们在layout里面编写onClick就不会影响
-keepclassmembers class * extends android.app.Activity {
public void * (android.view.View);
}
#
保留自定义控件(继承自View)不能被混淆
-keep public class * extends android.view.View {
public
public
public
public void set(*);
*** get ();
}
#
保留Serializable 序列化的类不被混淆
-keepclassmembers class * implements java.io.Serializable {
static final long serialVersionUID;
private static final java.io.ObjectStreamField[] serialPersistentFields;
!static !transient
private void writeObject(java.io.ObjectOutputStream);
private void readObject(java.io.ObjectInputStream);
java.lang.Object writeReplace();
java.lang.Object readResolve();
}
对于带有回调函数onXXEvent的,不能混淆
-keepclassmembers class * {
void *
(**
On*
Event);
}
2.>第三方和自己的bean文件是不需要混淆:
-keep public class com.heytap.test.yourBeanPackageName.** {
//全部忽略
*
;
}
-keep public class com.heytap.test.yourBeanPackageName.** {
//忽略get和set方法
public void set*
(***
);
public ***
get*
();
public ***
is*
();
}
//以上两种任意一种都行
3.>内部类混淆处理:
-keep class com.heytap.test.MainActivity$* {
*;
}
4.>避免泛型混淆:
-keepattribute Signature
5.>带有throws的混淆:
-keepattribute Exceptions
七、代码混淆方案
1.组件化混淆
在创建一个Android Module的时候,在Module的build.gradle,会生成 proguard-android-optimize.txt 和 proguard-rules.pro。
配置如下:
buildTypes {
release { minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
defaultConfig { ...
consumerProguardFiles 'consumer-rules.pro'
}
-
proguardFiles
配置的proguard文件不会被打进aar中,只作用于库文件代码,只在编译发布aar的时候有效。 -
consumerProguardFiles
配置的proguard会被打进aar包中,在你将库文件作为一个模块添加到app模块中后,库文件中consumerProguardFiles
配置的proguard文件则会追加到app模块的Proguard配置文件中,作用于整个app代码。
1> 混淆方案1:在app模块中管理所有的混淆规则
优点:所有混淆规则在app模块的proguard-rule.pro
文件中统一管理
缺点:移除某些模块后,需手动移除app模块中的混淆规则。理论上混淆规则添加多了不会造成崩溃或者编译不通过,但是会影响编译效率
2> 混淆方案2:各个组件模块各自管理混淆规则
优点:将混淆文件解耦到每个模块中,并且不会影响编译效率
组件化混淆解耦:
我们可以将固定的第三方混淆放到common模块的consumer-rules.pro
文件中,每个模块独有的第三方引用库混淆放到各自的consumer-rules.pro
文件中,在app模块的proguard-rule.pro
文件中放入Android通用的混淆声明,如四大组件和全局的混淆等配置。这样可以最大限度的完成混淆解耦操作。
2.SDK代码混淆
SDK代码的混淆方案和组件化混淆方案大致相同,sdk代码一般会经过两次混淆过程:
1> 内部混淆
主要是将相关核心代码混淆,将对外暴露的类keep住,混淆配置文件写proguardFiles
配置文件中。
2>外部混淆
当外部依赖我们的SDK时,开启混淆后,也会使得SDK内部的类被混淆,可能会导致反射调用报ClassNotFoundException异常,所以我们需要将这些需要反射的类keep住,这些混淆配置需要写在SDK内部的consumerProguardFiles
配置文件中,这样我们在外部业务方就不需要再次配置混淆文件了。
特别注意:
①我们在开发SDK的时候经常会用到compileOnly这样的依赖方式,通过这种依赖方式引用SDK,SDK内部consumerProguardFiles
配置的proguard也会被引入
② 在打包aar的过程中,consumerProguardFiles中的混淆配置规则不对aar混淆起作用。
八、混淆后奔溃调试
当项目混淆代码后,安装release版本的apk到手机有时根本运行不起来,有时启动了瞬间崩溃,其实原因很简单我们应用的库或者第三方jar被混淆了导致无法正常调用,那怎么查找是哪些不该混淆了的被混淆了?
在Android studio 中生成release包的同时 build\outputs\mapping\release文件夹下也生成了4个文件:
① configuration.txt :总的混淆规则。这个文件包含了打包过程中所有混淆规则的汇总。
② mapping.txt :列出了原始的类,方法,和字段名与混淆后代码之间的映射。
③ seeds.txt :列出了未被混淆的类和成员。
④ usage.txt : 列出了从apk中删除的代码。
一般情况下,我们直接看mapping 和seeds这2个文件夹就可以了,在调试过程中,使用notedpad打开mapping文件,有时日志比较多,可能将近几万行。
mapping文件记录了所有的混淆前后的映射关系。比如:你安装运行apk,根本跑步起来。你检查mapping会有可能发现 是你把不应该混淆的第三方包给混淆了,在mapping文件的映射中第三方包 也变成abc了,这里就是出错的根本,一一找出,在proguard-rules.pro中-dontwarn 包名,-keep class 包名 就可以顺利解决。几万行并不是让你一行一行去看的,点击对应包的关键字滚动查看。一一过滤。
seeds文件夹是帮你查看是否需要混淆的类没有被混淆,当然都是可以修改的,查看过程中会有errormessage,搜索一下,看看是否有不应该混淆的被你混淆掉。
也可以使用工具查看报错问题:
下的proguardgui.bat脚本将Crash堆栈信息还原到混淆前的状态。步骤如下
①双击打开脚本,选择左边的ReTrace选项
②选择Mapping file文件,也就是混淆后打包后在app module/build/outputs/mapping/release
下生成的mapping.txt
③拷贝混淆后的堆栈信息
④点击右下角的ReTrace!按钮,完成Crash堆栈信息的追溯
如下图中间部分就是追溯到的原Crash堆栈信息: