一、什么是ProGuard
为了保证代码的可读性,编写的函数或者变量一般都使用有意义的名字,一看到函数名就知道这个函数是干什么的,一看到变量名就知道这个变量存放的值代表什么含义。这样,对于以后代码的维护会有很多的好处,将代码交接给别人维护也会非常的方便。
但是Android应用是用Java代码写的,最后这些代码会被编译成一个包含Dalvik指令的dex文件,并且所有的函数名、变量名都会原封不动的保留在dex文件中。对于某些居心叵测的人,可以使用dex2jar和jd-gui试着反推出原始的Java代码或者程序的大致逻辑,然后用apktool修改代码并重新打包。这时候,清晰的函数名和变量名将会给这些黑客以很大的帮助。
拿我写的一个应用来说,在没有经过任何处理的情况下,用dex2jar将apk文件转换成jar文件后,再用jd-gui将其打开:
可以看到,其实现的逻辑几乎一览无疑,想要了解背后的实现原理,根本没有任何难度。
那么有没有什么办法既能保证原始代码的可读性,又可以防止这些有用的信息被包含在编译后的代码中,从而泄露给其他的人呢?Google为此提供了一个ProGuard的工具。
ProGuard是在Android SDK中提供的一个免费的开源工具,它能够使用语义上隐晦的名称来重命名代码中的类、变量和函数等,达到混淆代码的功能。
二、如何使用ProGuard
当在 Eclipse 中创建一个新的 Android 工程时,在工程目录的根路径下,会有一个 project.properties 文件,将其打开,可以看到下面的两行:
#To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home):
#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt
所以,默认情况下,
ProGuard
是被关闭的,如果想打开的话,将
proguard.config
前面的井号(“
#
”)去掉就行了。
可以看到,在这个配置项中指定了两个文件。第一个位于Android SDK中,它是Android为所有项目定义的一个通用的配置文件;而第二个位于本工程的根目录下(项目创建时,会在根目录下自动创建一个proguard-project.txt配置文件),其里面的内容是空的,如果第一个通用配置文件无法满足你的需要,还需要专门为你自己的项目添加配置项的话,可以加在这个文件中。一般情况下,通用配置文件已经够用了,并不需要对项目进行特殊的定制。
到此,我们已经通过简单的配置,在Eclipse工程中打开了ProGuard工具混淆Android代码的功能。当你下次在Eclipse中导出你的Android应用时,ProGuard会自动混淆你的代码:
ProGuard只在release模式下编译应用程序才会起作用。而如果在debug模式下编译的话,并不会触发ProGuard对代码进行混淆。
下面我们来看看混淆的效果,还是同样的工程,当开启ProGuard混淆功能后,经过dex2jar处理,在jd-gui中看到的结果如下:
可以看出来,相对于前面来说,混淆过后的代码,其函数和变量名都被替换成了没有任何含义的a、b、c、d等字母,单纯看函数名和变量名已经没法猜出其具体作用了。
当你通过Eclipse导出了一个通过ProGuard混淆过的apk后,会在项目的根目录下创建一个叫做proguard的目录,自动生成四个文件:
1)dump.txt:描述apk内所有类文件的内部结构。
2)mapping.txt:列出了原始的类、方法和变量名与混淆后代码间的映射关系。这个文件非常重要,如果你的代码产生bug的话,那么在log中显示的是混淆后的代码。如果希望定位到原始的源代码的话,可以根据记录在mapping.txt文件中的映射关系进行反推。
3)seeds.txt:列出了没有被混淆的类和成员函数及变量。
4) usage.txt : 列出了被删除的无用代码。注意,对于下列几种情况,ProGuard不会对代码进行混淆:
1) 反射用到的类;
2) 在AndroidManifest.xml文件中引用到的类;
3) 需要使用JNI调用的类。
三、如何配置ProGuard
其实 ProGuard 的配置文件主要用来告诉 ProGuard ,项目中的哪些代码是不能被混淆的。要想保证代码的某些部分不被 ProGuard 混淆,必须要在配置文件中添加一些以 -keep 打头的配置项,一共有 6 种关键字,它们的区别和含义如下:
ProGuard在对代码混淆的同时,还会剔除一些无用的代码,也就是从任何调用路径都无法被执行的代码,这样做的好处是可以有效减小应用程序的大小。第一列的关键词可以保证指定的代码不被改名也不会被剔除,即使没有用;第二列的关键词只保证指定代码不会被改名,但如果ProGuard判断这些代码没有用的话,任然会将其删除掉。
这6个关键字后面都要包含一个类规格(Class Specification),说明作用的范围,如:
-keep class_specification
这个所谓的类规格是一个非常复杂的概念,其大致定义如下:
[[!]public|final|abstract ...] [!]interface|class|enum classname
[extends|implements classname]
[{
[[!]public|private|protected|static|volatile|transient ...] <fields> |
(fieldtype fieldname);
[[!]public|private|protected|static|synchronized|native|abstract ...] <methods> |
<init>(argumenttype,...) | (returntype methodname(argumenttype,...));
[[!]public|private|protected|static ... ] *;
...
}]
是不是觉得非常复杂?下面我们来解释一下。表达式中的方括号(“
[ ]
”)表示里面的内容是可选的,可以有也可以没有;省略号(“
…
”)表示前面的内容均可以以任何次序和组合同时使用;竖线(“
|
”)表示这些选项只能出现一个。
表达式中的class关键字表示任何接口类、抽象类和普通类;interface关键字表示只能是接口类;enum关键字表示只能是枚举类。如果在interface和enum关键字前面加上感叹号(“!”)分别表示不是接口类的类和不是枚举类的类。class关键字前面是不能加感叹号的。
对于类名(classname)来说,可以是类全名,或者可以包含以下一些特殊字符的正则表达式:
1)?:问好代表一个任意字符,但不能是句号(“.”,因为句号是包名分隔符);
2)*:单个星号代表任意个任意字符,但不能代表句号;
3 ) ** :两个星号代表任意个任意字符,且能代表句号。对于单个星号来说,如果类名部分只有一个星号,不包含其它任何字符,为了保证兼容性,其代表任何类,就跟两个星号的作用一样了。
extends和implements表示限定类一定要扩展自一个指定类或者实现了一个指定接口类,这时候通常类名部分是一个星号。
对于类中的成员变量(Fields)来说,可以通过变量类型fieldtype和变量名fieldname来精确指定,也可以通过<fields>表示类中的任何成员变量。
对于类中的成员函数(Methods)来说,可以通过返回类型returntype、方法名methodname和参数类型argumenttype来唯一限定,也可以通过<methods>来表示类中的任何成员函数。
对于类的构造函数来说,可以用<init>加上构造函数的参数来指定。
星号(“*”)可以匹配类中的任何成员变量和函数。
对于类中的成员函数名methodname和成员变量名fieldname来说也可以使用通配符来匹配,同样问号(“?”)可以匹配一个任意字符,而星号(“*”)可以匹配任意多个任意字符。
对于类中的成员变量的类型、成员函数的返回类型和参数类型,以及构造函数的参数类型来说,可以使用下面这些通配符来匹配:
1) %:匹配任何原始类型,如boolean、int等,但不包括void;
2) ?:匹配一个任意字符,不包括句号;
3) *:匹配任意个任意字符,不包括句号;
4) **:匹配任意个任意字符,包括句号;
5) ***:匹配任意类型,包括原始类型和非原始类型,数组类型和非数组类型;
6) … :匹配任何数目个任何类型的参数。在类名前、类中成员变量和成员函数名前,可以加上访问限定符(如public、private、protected等,修饰类、成员变量和成员函数的访问限定符各不相同)。如果加上了访问限定符后,就表示要匹配的类、成员变量或成员函数的定义中必须包含这些限定符。如果在限定符前面加上感叹号“!”,则刚好相反,定义中必须不包含这些限定符。
事实上还可以加上对特定类型注释(Annotation)的限定条件,但一般用不到,这里就不再多说了。
作为例子,我们可以看看Android SDK中已经为我们预定义的一些ProGuard规则。这些规则保证一般的Android应用不需要任何配置就可以使用ProGuard进行混淆,并且混淆后的代码还可以无错误的运行,具体解释请看注释:
#两个特定的类不能被混淆和删除
-keep public class com.google.vending.licensing.ILicensingService
-keep public class com.android.vending.licensing.ILicensingService
#所有包含JNI调用的类以及其内部的成员都不能被混淆
-keepclasseswithmembernames class * {
native <methods>;
}
#扩展自android.view.View类的任何public类的setter和getter方法都不能被混淆和删除
-keepclassmembers public class * extends android.view.View {
void set*(***);
*** get*();
}
#扩展自android.app.Activity类的任何类中访问属性是public,返回值是void,参数是android.view.View类型的所有函数都不能被混淆或删除
-keepclassmembers class * extends android.app.Activity {
public void *(android.view.View);
}
#任何枚举类中的values和valueOf静态方法都不能被混淆和删除
-keepclassmembers enum * {
public static **[] values();
public static ** valueOf(java.lang.String);
}
#实现了android.os.Parcelable接口类的任何类,以及其内部定义的Creator内部类类型的public final静态成员变量,都不能被混淆和删除
-keep class * implements android.os.Parcelable {
public static final android.os.Parcelable$Creator *;
}
#所有自动生成的R类中的public静态成员变量
-keepclassmembers class **.R$* {
public static <fields>;
}
最后总结一下,ProGuard是一个非常方便的代码混淆工具。只需要在你的项目开发过程中,花一点点时间将其集成进去,并稍微多做一下测试,就可以起到很好的效果,可以极大的增加破解的难度,建议大家多多使用。