什么是代码混淆
代码混淆就是将代码中的各种元素,如变量,方法,类和包的名字改写成无意义的名字,增加项目反编译后被读懂的难度。
Android代码混淆使用ProGuard工具,ProGuard是一个压缩、优化和混淆Java字节码文件的免费的工具,它可以删除无用的类、字段、方法和属性。
以下是官网对ProGuard的说明:
ProGuard是一个对Java类文件进行压缩,优化,混淆和校验的工具。
压缩过程查找并删除没有使用到的类,字段,方法和属性。优化过程对方法的字节码进行分析和优化。
混淆过程把剩余的元素名字该写成简短且无意义的名字。这些过程会使程序体积更小,运行更高效,更难被反编译。
最后的校验过程为类增加校验信息,但这个过程依赖J2ME和JDK6或以上的编译环境。
rom编译
Android.mk文件中,用LOCAL_PROGUARD_ENABLED来配置混淆的模式;LOCAL_PROGUARD_FLAG_FILES用来指定配置文件。LOCAL_PROGUARD_ENABLED的取值如下:
full:使用编译系统默认的配置:压缩但不混淆和优化,默认的混淆配置文件是build/core/proguard.flags
custom:和full一样,但不包括aapt生成的resource相关的混淆配置。
nosystem:不使用系统的默认配置,但使用aapt生成的resource相关的混淆配置,其他混淆由模块自己负责。
disabled:关闭混淆
obfuscation:和full一样,并且开启混淆
optimization:和full一样,并且开启优化
不设置时,如果是app,默认为full,如果是library,则默认为disabled。
编译userdebug版本时,编译脚本会把app的obfuscation改成full,即不混淆;所以userdebug版本的app是不混淆的。想了解更多信息,可以自行阅读project_src/build/core/下的java.mk,package_internel.mk,java_library.mk,proguard.flags,proguard_base_keeps.flags等文件。
Android Studio
项目目录下的build.gradle文件中minifyEnabled设置为true为开启,false为关闭;proguardFiles用来指定混淆配置文件。使用Build菜单下的Generate Signed APK进行打包即可。记得在Build Type:选项下选择release,否则只打包不会混淆。
Eclipse
项目目录下的project.properties文件中添加配置即可开启混淆:proguard.config=xxx,xxx为混淆配置文件路径,多个配置文件用:分隔。 然后Export APK就可以了,注意直接运行程序生成的安装包是没有经过混淆的。
如何使用混淆
理想的目标是将所有元素都加入混淆,但混淆会另反射无法工作。因此反射以及反射延伸出来的功能使用到的元素都不能混淆。
因为Android开发中有些内容每次都要配置,所以sdk中提供了一份默认配置文件,我们新建项目时可以复制或引用sdk下的默认配置,在此基础上再增加自己的需求。默认配置文件在android_sdk/tools/proguard/proguard-android.txt。
下面介绍一些常用配置以及Android开发中哪些元素不应该混淆。常用配置:
-keep
keep用来指定哪些元素不进行混淆,它有很多变种,比如:
-keep 保留指定的包,类和类成员不被混淆。
-keepclassmembers 保留指定的类成员不被混淆,但包名类名会被混淆。
-keepclasseswithmembers 保留指定的类成员及其类不被混淆。
当未配置-dontshrink(该配置是关闭压缩功能,也就是不会删除未使用的元素,未配置时,也即是开启压缩功能)时,以上3个配置指定的元素即使未使用过,也不会被删除。 以下3个命令与以上3个命令对应,区别是在上述情况中,指定的元素未使用过就会被删除。
-keepnames 也可以写成-keep,allowshrinking
-keepclassmembernames 也可以写成-keepclassmembers,allowshrinking
-keepclasseswithmembernames 也可以写成-keepclasseswithmembers,allowshrinking
示例:
保留Util类名,但内部成员会被混淆
-keep public class com.test.proguard.util.Util
保留Util类名及其内部成员
-keep public class com.test.proguard.util.Util {;}
保留util包及其下级包的类和内部成员
-keep public class com.test.proguard.util.* {;}
保留第三方lib库及继承自第三方的类:
======= Sina Weibo SDK =========
-dontwarn com.sina.*
-keep class com.sina.{;}
-keep interface com.sina.{;}
-keep public class * extends com.sina.**
保留util包下的所有类成员不被混淆,但包名类名会被混淆
-keepclassmembers public class com.test.proguard.util.** {*;}
保留所有名为showText并且是public void的方法不被混淆
-keepclassmembers class * {
public void showText(...);
}
保留Serializable的所有子孙类中所有的private String的属性。
-keepclassmembers class * extends java.io.Serializable {
private java.lang.String *;
}
保留Serializable的所有子孙类中所有的private String的属性以及该类名。
-keepclasseswithmembers class * extends java.io.Serializable {
private java.lang.String *;
}
-dontwarn
dontwarn和keep可以说是形影不离,尤其是处理引入的lib库时.引入的lib库可能存在一些无法找到的引用和其他问题,在build时可能会发出警告,如果我们不进行处理,通常会导致build中止.因此为了保证build继续,我们需要使用dontwarn忽略这些我们无法解决的lib库的警告.
示例:
忽略com.google.zxing包相关的警告
-dontwarn com.google.zxing.**
其他配置
-dontshrink 不压缩,作用于全局
-dontoptimize 不优化,作用于全局
-dontobfuscate 不混淆,作用于全局
-dontwarn 忽略所有警告,使混淆不会因为警告而停止运行,但会打印警告信息
-useuniqueclassmembernames 类和成员都使用唯一的名字,如果没有这个选项,会有很多变量或方法或类名都叫‘a’,‘b’
-dontusemixedcaseclassnames 不使用大小写混合类名
-verbose 混淆过程中打印更多信息,如果因为异常停止混淆,则会输出stack trace,而不仅仅是异常信息
-keepattributes [attribute_filter] Class文件中包含一些与运行无关的信息,比如SourceFile(从哪个源文件编译而来),SourceDir(源文件的文件目录),LineNumberTable(代码行),Exceptions,InnerClasses,Signature,Deprecated,Annotation等等,混淆过程会默认移除掉这些信息,但可以用keepattributes来指定保留那类信息,比如-keepattributes SourceFile,LineNumberTable可以保留代码行和源文件信息。
-include 引入其他的配置文件
不应该混淆的元素
需要反射的元素
由于反射是通过元素名字来查找的,因此当名字改写后,无法找到目标,会导致出现ClassNotFoundException,NoSuchFiledException,NoSuchMethodException等异常。
例如如下代码会抛出ClassNotFoundException:
try {
String str = "com.test.proguard.util.Util";
Class clazz = Class.forName(str);
Object object = clazz.newInstance();
} catch (Exception e) {
e.printStackTrace();
}
有趣的是,上面这段代码如果改写成下面这样,则会顺利找到Util类:
try {
Class clazz = Class.forName("com.test.proguard.util.Util");
Object object = clazz.newInstance();
} catch (Exception e) {
e.printStackTrace();
}
这两段代码的区别在于forName传入的参数是常量还是变量,传入常量的调用方式被ProGuard混淆处理了,所以可以正常运行。
ProGuard还对其他一些反射用法进行了处理。例如:
Class.forName("SomeClass")
SomeClass.class
SomeClass.class.getField("someField")
SomeClass.class.getDeclaredField("someField")
SomeClass.class.getMethod("someMethod", new Class[] {})
AtomicIntegerFieldUpdater.newUpdater(SomeClass.class, "someField")
枚举:Enum.valueOf(String)用到反射,不能混淆
四大组件:四大组件必须在manifest中注册,混淆后类名被改写将无法被找到,会抛出异常。
aidl:aidl
GSON:GSON是一个利用反射进行序列化的第三方lib库。
实现Parcelable接口的可序列化类:进程间通信的话,要保证两端类名相同,进程内传递时反序列化时需要反射CREATOR对象。
注解:很多场景下注解被用作在运行时反射来确定一些元素的特征。
自定义View
native方法
jni调用的java方法
js调用的java方法
如何恢复被混淆的trace
Proguard进行混淆时会生成一个映射表,文件名是mapping.txt,通过sdk下的retrace.sh脚本和mapping.txt就可以把混淆的trace恢复到原来的样子
示例:
trace.txt文件:
java.lang.Exception
at com.test.proguard.a.b.a(Util.java:39)
at com.test.proguard.a.a.a(TestStart.java:14)
at com.test.proguard.MainActivity.a(MainActivity.java:32)
at com.test.proguard.MainActivity.a(MainActivity.java:31)
at com.test.proguard.b.onClick(MainActivity.java:26)
at android.view.View.performClick(View.java:5217)
at android.view.View$PerformClick.run(View.java:21278)
at android.os.Handler.handleCallback(Handler.java:739)
at android.os.Handler.dispatchMessage(Handler.java:95)
at android.os.Looper.loop(Looper.java:148)
at android.app.ActivityThread.main(ActivityThread.java:5547)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:935)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:726)
运行命令:
./retrace.sh ~/mapping.txt ~/trace.txt
输出:
java.lang.Exception
at com.test.proguard.util.Util.showText(Util.java:39)
at com.test.proguard.util.TestStart.start(TestStart.java:14)
at com.test.proguard.MainActivity.test(MainActivity.java:32)
at com.test.proguard.MainActivity.access$0(MainActivity.java:31)
at com.test.proguard.MainActivity$1.onClick(MainActivity.java:26)
at android.view.View.performClick(View.java:5217)
at android.view.View$PerformClick.run(View.java:21278)
at android.os.Handler.handleCallback(Handler.java:739)
at android.os.Handler.dispatchMessage(Handler.java:95)
at android.os.Looper.loop(Looper.java:148)
at android.app.ActivityThread.main(ActivityThread.java:5547)
at java.lang.reflect.Method.invoke(Method.java)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:935)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:726)
常见问题
1.反射导致找不到类、方法、属性
当反射时抛出:ClassNotFoundException,NoSuchMethodException,NoSuchFieldException时请检查反射目标是否被混淆了。
2.进程间通信传递Parcelable序列化类时报异常
java.lang.RuntimeException: Unable to start activity ComponentInfo{com.test.parcel/com.test.parcel.MainActivity}:
android.os.BadParcelableException: ClassNotFoundException when unmarshalling: com.test.model.Student
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2514)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2575)
at android.app.ActivityThread.access$900(ActivityThread.java:160)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1415)
原因:序列化类被混淆后,与另一端的序列化类名称匹配不上,导致抛出ClassNotFoundException异常。
解决:序列化类不应该被混淆。
注意,在Android7.0上Parcelable类的keep需要跟之前的不一样,如下的做法很常见(android本身在proguard_basic_keeps.flags中也是这样写的):
// Parcelable CREATORs must be kept for Parcelable functionality
-keep class * implements android.os.Parcelable {
public static final ** CREATOR;
}
但是这样的写法在Android7上不管用。需要如下写法:
-keepclasseswithmembers class * implements android.os.Parcelable {*;}
或者:
-keepclassmembers class * implements android.os.Parcelable {
public static ;
}
3.Intent传递Parcelable序列化类时报异常
java.lang.RuntimeException: Unable to start service com.smartisan.feedbackhelper.upload.ReliableUploader@431b6290
with Intent { cmp=com.smartisan.gamestore/com.smartisan.feedbackhelper.upload.ReliableUploader (has extras) }:
android.os.BadParcelableException: Parcelable protocol requires a Parcelable.Creator object called CREATOR
on class com.smartisan.feedbackhelper.utils.e
原因:序列化类被混淆后,CREATOR对象变量名被改写,无法被找到,导致抛出异常。
解决:序列化类不应该被混淆。
4.aidl相关类不应该混淆
Parcel : **** enforceInterface() expected 'com.xy.bizport.service.aidl.IXyRemoteCallable' but read 'com.xy.bizport.a.a.a'
原因:混淆后两端类名无法匹配,导致异常。
5.js和java不能互相调用,提示找不到方法
原因:混淆后方法名无法匹配。
解决:增加如下配置:
-keepattributes Annotation, JavascriptInterface
-keepattributes 建议只写一行,因为在odin上配置-keepattributes时,前面的会被后面的覆盖。