首先要说的话:
本文是对好几个博文的摘录再加上我自己的理解,以尊重原创为原则,下面贴出相关博文的链接
Android有效地减少方法数
http://blog.csdn.net/lihenair/article/details/51657099
Android 项目的代码混淆,Android proguard 使用说明
http://blog.csdn.net/catoop/article/details/47208833
Android安全攻防战,反编译与混淆技术完全解析(下)
http://blog.csdn.net/guolin_blog/article/details/50451259
Java 是一种跨平台的、解释型语言,Java 源代码编译成中间”字节码”存储于 class 文件中。由于跨平台的需要,Java 字节码中包括了很多源代码信息,如变量名、方法名,并且通过这些名称来访问变量和方法,这些符号带有许多语义信息,很容易被反编译成 Java 源代码。为了防止这种现象,我们可以使用 Java 混淆器对 Java 字节码进行混淆。
混淆就是对发布出去的程序进行重新组织和处理,使得处理后的代码与处理前代码完成相同的功能,而混淆后的代码很难被反编译,即使反编译成功也很难得出程序的真正语义。被混淆过的程序代码,仍然遵照原来的档案格式和指令集,执行结果也与混淆前一样,只是混淆器将代码中的所有变量、函数、类的名称变为简短的英文字母代号,在缺乏相应的函数名和程序注释的况下,即使被反编译,也将难以
阅读
。同时混淆是不可逆的,在混淆的过程中一些不影响正常运行的信息将永久丢失,这些信息的丢失使程序变得更加难以理解。
混淆器的作用不仅仅是保护代码,它也有精简编译后程序大小的作用。由于以上介绍的缩短变量和函数名以及丢失部分信息的原因, 编译后 jar 文件体积大约能减少25% ,这对当前费用较贵的无线网络传输是有一定意义的。
android默认的混淆配置文件,我们来一起逐行阅读一下。
-dontusemixedcaseclassnames
表示混淆时不使用大小写混合类名。
-dontskipnonpubliclibraryclasses
表示不跳过library中的非public的类。
-verbose
表示打印混淆的详细信息。
-dontoptimize
表示不进行优化,建议使用此选项,因为根据proguard-android-optimize.txt中的描述,优化可能会造成一些潜在风险,不能保证在所有版本的Dalvik上都正常运行。
-dontpreverify
表示不进行预校验。这个预校验是作用在Java平台上的,Android平台上不需要这项功能,去掉之后还可以加快混淆速度。
-keepattributes *Annotation*
表示对注解中的参数进行保留。
-keep public class com.google.vending.licensing.ILicensingService
-keep public class com.android.vending.licensing.ILicensingService
表示不混淆上述声明的两个类,这两个类我们基本也用不上,是接入Google原生的一些服务时使用的。
-keepclasseswithmembernames class * {
native <methods>;
}
表示不混淆任何包含native方法的类的类名以及native方法名,这个和我们刚才验证的结果是一致的。
-keepclassmembers public class * extends android.view.View {
void set*(***);
*** get*();
}
表示不混淆任何一个View中的setXxx()和getXxx()方法,因为属性动画需要有相应的setter和getter的方法实现,混淆了就无法工作了。
-keepclassmembers class * extends android.app.Activity {
public void *(android.view.View);
}
表示不混淆Activity中参数是View的方法,因为有这样一种用法,在XML中配置android:onClick=”buttonClick”属性,当用户点击该按钮时就会调用Activity中的buttonClick(View view)方法,如果这个方法被混淆的话就找不到了。
-keepclassmembers enum * {
public static **[] values();
public static ** valueOf(java.lang.String);
}
表示不混淆枚举中的values()和valueOf()方法,枚举我用的非常少,这个就不评论了。
-keepclassmembers class * implements android.os.Parcelable {
public static final android.os.Parcelable$Creator CREATOR;
}
表示不混淆Parcelable实现类中的CREATOR字段,毫无疑问,CREATOR字段是绝对不能改变的,包括大小写都不能变,不然整个Parcelable工作机制都会失败。
-keepclassmembers class **.R$* {
public static <fields>;
}
表示不混淆R文件中的所有静态字段,我们都知道R文件是通过字段来记录每个资源的id的,字段名要是被混淆了,id也就找不着了。
-dontwarn android.support.**
表示对android.support包下的代码不警告,因为support包中有很多代码都是在高版本中使用的,如果我们的项目指定的版本比较低在打包时就会给予警告。不过support包中所有的代码都在版本兼容性上做足了判断,因此不用担心代码会出问题,所以直接忽略警告就可以了。
好了,这就是proguard-android.txt文件中所有默认的配置,而我们混淆代码也是按照这些配置的规则来进行混淆的。经过我上面的讲解之后,相信大家对这些配置的内容基本都能理解了。不过proguard语法中还真有几处非常难理解的地方,我自己也是研究了好久才搞明白,下面和大家分享一下这些难懂的语法部分。
proguard中一共有三组六个keep关键字,很多人搞不清楚它们的区别,这里我们通过一个表格来直观地看下:
关键字 | 描述 |
---|
keep | 保留类和类中的成员,防止它们被混淆或移除。 |
keepnames | 保留类和类中的成员,防止它们被混淆,但当成员没有被引用时会被移除。 |
keepclassmembers | 只保留类中的成员,防止它们被混淆或移除。 |
keepclassmembernames | 只保留类中的成员,防止它们被混淆,但当成员没有被引用时会被移除。 |
keepclasseswithmembers | 保留类和类中的成员,防止它们被混淆或移除,前提是指名的类中的成员必须存在,如果不存在则还是会混淆。 |
keepclasseswithmembernames | 保留类和类中的成员,防止它们被混淆,但当成员没有被引用时会被移除,前提是指名的类中的成员必须存在,如果不存在则还是会混淆。 |
除此之外,proguard中的通配符也比较让人难懂,proguard-android.txt中就使用到了很多通配符,我们来看一下它们之间的区别:
通配符 | 描述 |
---|
<field> | 匹配类中的所有字段 |
<method> | 匹配类中的所有方法 |
<init> | 匹配类中的所有构造函数 |
* | 匹配任意长度字符,但不含包名分隔符(.)。比如说我们的完整类名是com.example.test.MyActivity,使用com.*,或者com.exmaple.*都是无法匹配的,因为*无法匹配包名中的分隔符,正确的匹配方式是com.exmaple.*.*,或者com.exmaple.test.*,这些都是可以的。但如果你不写任何其它内容,只有一个*,那就表示匹配所有的东西。 |
** | 匹配任意长度字符,并且包含包名分隔符(.)。比如proguard-android.txt中使用的-dontwarn android.support.**就可以匹配android.support包下的所有内容,包括任意长度的子包。 |
*** | 匹配任意参数类型。比如void set*(***)就能匹配任意传入的参数类型,*** get*()就能匹配任意返回值的类型。 |
… | 匹配任意长度的任意类型参数。比如void test(…)就能匹配任意void test(String a)或者是void test(int a, String b)这些方法。 |
虽说上面表格已经解释的很详细了,但是很多人对于keep和keepclasseswithmembers这两个关键字的区别还是搞不懂。确实,它们之间用法有点太像了,我做了很多次试验它们的结果都是相同的。其实唯一的区别就在于类中声明的成员存不存在,我们还是通过一个例子来直接地看一下,先看keepclasseswithmember关键字:
-keepclasseswithmember class * {
native <methods>;
}
这段代码的意思其实很明显,就是保留所有含有native方法的类的类名和native方法名,而如果某个类中没有含有native方法,那就还是会被混淆。
但是如果改成keep关键字,结果会完全不一样:
-keep class * {
native <methods>;
}
使用keep关键字后,你会发现代码中所有类的类名都不会被混淆了,因为keep关键字看到class *就认为应该将所有类名进行保留,而不会关心该类中是否含有native方法。当然这样写只会保证类名不会被混淆,类中的成员还是会被混淆的。
文件
通过eclipse project->android tools->export signed(unsigned) application package生成apk,会自动运行ProGuard。
在debug模式下为了更快调试并不会调用proguard。
如果是ant命令打包apk,proguard信息文件会保存于工程代码下的/bin/proguard文件夹内;
如果用eclipse export命令打包,也会在/proguard文件夹内。其中包含以下文件:
mapping.txt
表示混淆前后代码的对照表,这个文件非常重要。如果你的代码混淆后会产生bug的话,log提示中是混淆后的代码,希望定位到源代码的话就可以根据mapping.txt反推。
每次发布都要保留它方便该版本出现问题时调出日志进行排查,它可以根据版本号或是发布时间命名来保存或是放进代码版本控制中。
dump.txt
描述apk内所有class文件的内部结构。
seeds.txt
列出了没有被混淆的类和成员。
usage.txt
列出了源代码中被删除在apk中不存在的代码。
不能混淆的代码
顾名思义,不能混淆代码如果被混淆了,就会出现错误。
1、需要反射的代码
2、系统接口
3、Jni接口
4、需要序列号和反序列化的代码(即实现Serializable接口的JavaBean)
5、与服务端进行元数据交互的JavaBean(JSON、XML中对应的类)
……
常见错误
1) Proguard returned with error code 1. See console
> 更新proguard版本
> android-support-v4 不进行混淆
> 添加缺少相应的库
2) 使用gson包解析数据时,出现 missing type parameter 异常
> 在 proguard-project.txt 中添加
-dontobfuscate
-dontoptimize
> 在 proguard-project.txt 中添加
# removes such information by default, so configure it to keep all of it.
-keepattributes Signature
# Gson specific classes
-keep class sun.misc.Unsafe { *; }
#-keep class com.google.gson.stream.** { *; }
# Application classes that will be serialized/deserialized over Gson
-keep class com.google.gson.examples.android.model.** { *; }
3) 类型转换错误
> 在 proguard-project.txt 中添加
-keepattributes Signature
4) 空指针异常
> 混淆过滤掉相关类与方法
5) java.lang.reflect.UndeclaredThrowableException
> -keep interface com.dev.impl.**
6) Error: Unable to access jarfile ..libproguard.jar
> 路径问题
7) java.lang.NoSuchMethodError
> 这也是最常见的问题,因为找不到相关方法,方法被混淆了,混淆过滤掉相关方法便可。
默认混淆规则的几点总结:
1.普通类肯定是会被混淆的,不管是类名、方法名还是变量名。没有被调用方法会被认为是多余的代码,在打包的时候就给移除掉。不仅仅是代码,没有被调用的资源同样也会被移除掉,因此混淆可以起到压缩APK包的作用。
2.凡是需要在AndroidManifest.xml中去注册的所有类的类名以及从父类重写的方法名都自动不会被混淆。因此,除了Activity之外,这份规则同样也适用于Service、BroadcastReceiver和ContentProvider。
3.只要一个类中有存在native方法,它的类名就不会被混淆,native方法的方法名也不会被混淆,因为C++代码要通过包名+类名+方法名来进行交互。 但是类中的别的代码还是会被混淆的。 如果native方法没被调用,则该方法被移除;如果类中所有方法都没被调用,那么该类相当于没本地方法,会被混淆。
4.当一个类只是被import,而没有在代码中调用,则这个类会被移除。
5.如果工程代码里的方法数过多,那么构建工程花的时间就长。而如果不被使用的方法也多的话,使用混淆能去掉这些代码,但不能减少构建时间,因为构建所要处理的代码并没有减少。所以在选择库文件或框架的时候尽量选择功能单一的,轻量级的。
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)