最近被keep
keepnames
keepclassmembers
等几个混淆规则搞得晕头转向,看起来虽然简单,但用起来却经常跟自己预想的一样。所以决定放弃看他人总结的博客,直接看ProGuard官方文档,目前为止,总算是有一定了解了。
1 ProGuard简介
通常我们都认为ProGuard是一个代码混淆工具,实际上其作用还不至于此,而是包括了四部分内容:
- shrink(删减):删减无用代码,包括无用的类、无用的变量、方法等
- optimize(优化):优化方法字节码
- obfuscate(混淆):混淆现有代码
- preverify(预校验):给类添加预校验信息,这是J2ME和Java 6及以上要求的
Proguard的整个工作过程如下图所示:
了解了Proguard的四大步骤,我们才能更好地理解Proguard混淆规则。
1.1 Entry Points
entry points
就是程序入口点。ProGuard以entry points
作为代码扫描入口,遍历所有代码,并最终决定哪些代码需要被丢弃或混淆。比较典型的 entry points
包括:
- main方法
- applets
- midlets
- activities
在ProGuard的不同阶段中,entry points
起到不同的作用:
- shrinking: 在shrinking阶段以
entry points
作为起点递归遍历所有代码,不可达的代码会被丢弃 - optimization:在optimization阶段会对代码进行进一步优化:
- 非入口代码可能会被改为private、static、final
- 未被使用参数会被删除
- 部分方法可能会被优化为内联方法
- obfuscation:obsfucation阶段会将非入口代码进行混淆。被标识为入口的代码则会免于被混淆
2 Keep 选项
keep选项是为了在代码混淆的过程中保留部分类及其字段不被混淆以满足程序运行需求。keep选项一共有如下6种规则:
- keep
- keepnames
- keepclassmember
- keepclassmembernames
- keepclasseswithmembers
- keepclasseswithmembernames
2.1 keep
keep
规则用于标识程序入口,被keep
规则修饰的类及其成员
会被指定为程序入口,从而免于被混淆。
2.2 keepnames
被keepnames
修饰的类及其成员不会被混淆,但前提是对应的成员在shrinking
类没有被删减掉。比如保留所有实现Serializable接口的类名:
-keepnames class * implements java.io.Serializable
2.3 keepclassmembers
keepclassmembers
仅保留指定的类成员不被混淆,但类名会被混淆。接着上面的例子,如果我们不仅向保留所有实现Seriablizable接口的类名,同时还要保留其所有的接口方法:
-keepnames class * implements java.io.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();
}
2.4 keepclassmembernames
keepclassmembernames
保留指定类成员不被混淆,前提是相关的类成员没有在shrinking阶段被删减。
2.5 keepclasseswithmembers
keepclasseswithmembers
会保留类和类成员不被混淆,前提是对应的类包含所有指定的类成员。keepclasseswithmembers
适用于指定一批拥有功能类成员的方法,而不用一一列举。比如保留所有又main
方法的类:
-keepclasseswithmembers public class * {
public static void main(java.lang.String[]);
}
2.6 keepclasswithmembernames
keepclasseswithmembernames
保留类和类成员不被混淆,前提是对应的类包含所有指定的类成员,同时对应的类成员在shrinking阶段没有被删减。比如保留所有native方法:
-keepclasseswithmembernames class * {
native ;
}
2.7 关系梳理
看完上述几个规则一定有点晕,没有关系,记住下面这个表就是:
Keep | From being removed or renamed | From being renamed |
---|---|---|
Classes and class members | -keep |
-keepnames |
Class members only | -keepclassmembers |
-keepclassmembernames |
Classes and class members, if class members present | -keepclasseswithmembers |
keepclasseswithmembernames |
每一条keep规则都应该跟一个类说明(specification of classes and class members)。如下就是一个类说明的例子:
class * {
native ;
}
类说明的规则将在下一节详细介绍。
如果你不清楚到底该用哪个keep规则,建议直接使用keep,被keep标明的类及其类成员不会被删减或重命名。需要注意的是,如果仅仅指明要keep的类,而不指明其类成员:
keep class yourpackage.demo
那ProGuard仅会保留其类和无参数构造方法不被删减或重命名。
3 类说明(Class Specification)
类说明(class specification
)是一个用于描述要keep的类及其成员的描述模板,其完整的格式如下所示:
[@annotationtype] [[!]public|final|abstract|@ ...] [!]interface|class|enum classname
[extends|implements [@annotationtype] classname]
[{
[@annotationtype] [[!]public|private|protected|static|volatile|transient ...] |
(fieldtype fieldname);
[@annotationtype] [[!]public|private|protected|static|synchronized|native|abstract|strictfp ...] |
(argumenttype,...) |
classname(argumenttype,...) |
(returntype methodname(argumenttype,...));
[@annotationtype] [[!]public|private|protected|static ... ] *;
...
}]
看起来好像很复杂,这是因为其功能强大,提供的选项很多,实际我们在实际使用过程中都是使用的简化模式。
不过这里我们还是来看看完整的格式,类说明模板有很多符号,理解这些符号的作用很有必要:
-
[]
表示可选项 -
!
表示非 -
|
表示或,如public|private
表示要修饰的对象是public
或private
理解了基本的符号含义,再来看这个模板就简单些了,整个表达式分为两部分:
- 类描述
- 类成员描述
3.1 类描述
类描述用于限定类本身,其对应的是上面完整表达式的:
[@annotationtype] [[!]public|final|abstract|@ ...] [!]interface|class|enum classname
[extends|implements [@annotationtype] classname]
部分。
-
[@annotationtype]
用于描述类注解(可选) -
[[!]public|final|abstract|@ ...]
用于描述类的访问权限 -
[!]interface|class|enum
用于描述要类的类型:- class:可以表示任何类或接口
- interface:仅表示接口
- enum:枚举
- classname 类名
-
[extends|implements [@annotationtype] classname]
用于描述继承、实现关系,通常用于描述一组类,如上文中提到过的所有实现Serializable
接口的类:`class * implements java.io.Serializable
这个类描述中所涉及到的两个classname
都支持通配符,用以指定一组类,其通配符使用说明如下:
通配符 | 描述 |
---|---|
? | 匹配所有的单个字符。比如mypackage.test? 可以指代mypackage.test1 或mypackage.test2 ,但不能指代mypackage.test12 |
* | 匹配任意长度的类名,但不包括分隔符. 。比如mypackage.*Test* 可以描述mypackage.Test 或mypackage.YourTestApplication 。但无法描述mypackage.mysubpackage.MyTest 。通常的用法是,用mypackage.* 来描述mypacakge 包下的所有类,但不包括其子包中的类 |
** | 匹配任意长度的类名,包括分隔符. 。mypackage.** 用于描述mypackage 包下的所有类,也包括其子包中的类。 |
3.2 类成员描述
类成员描述对应于上文中的:
[{
[@annotationtype] [[!]public|private|protected|static|volatile|transient ...] |
(fieldtype fieldname);
[@annotationtype] [[!]public|private|protected|static|synchronized|native|abstract|strictfp ...] |
(argumenttype,...) |
classname(argumenttype,...) |
(returntype methodname(argumenttype,...));
[@annotationtype] [[!]public|private|protected|static ... ] *;
...
}]
部分。
首先,整个类成员描述部分都是可选的,这部分不写也是可以的,比如keep class mypackage.MyTest
,这种情况下keep或保留类名和类的无参数构造方法不被移除或混淆。
类成员描述的形式大致有三种,也就是类成员描述模板中用;
分割开来的三个表达式,接下来分别讲下。
3.2.1 类成员变量描述
[@annotationtype] [[!]public|private|protected|static|volatile|transient ...] |
(fieldtype fieldname);
用于描述类的成员变量,[@annotationtype]
、[[!]public|private|protected|static|volatile|transient ...]
用于限定变量的注解类型和访问权限,均为可选。而变量名的描述也有两种方式:
-
:指代所有的变量 -
(fieldtype fieldname)
:指定具体的某个变量。注意,fieldtype和fieldname必须成对出现
3.2.2 类成员方法描述
[@annotationtype] [[!]public|private|protected|static|synchronized|native|abstract|strictfp ...] |
(argumenttype,...) |
classname(argumenttype,...) |
(returntype methodname(argumenttype,...));
前半部分和类成员变量的描述一致,[@annotationtype]
、[[!]public|private|protected|static|...]
用于限定方法的注解类型和访问权限。而方法名的描述有四种方式:
-
:指代所有方法 -
:指代构造方法。(argumenttype,...)
指代所有的构造方法,(argumenttype,...)
描述方法的参数列表 -
classname(argumenttype,...)
:另一种指代构造方法的方式,因为只有构造方法才没有返回类型 -
(returntype methodname(argumenttype,...))
:指代特定成员方法
3.2.3 类成员描述通配符
类成员的描述也支持通配符:
通配符 | 描述 |
---|---|
% | 描述任何的原型类型,如:boolean、int、但不包括void |
? | 描述任意的单个字符 |
* | 匹配任意长度的类名,但不包括分隔符. 。 |
** | 匹配任意长度的类名,包括分隔符. 。 |
*** | 匹配所有的数据类型,(原型类型或非原型类型,数组或非数组) |
... | 匹配任意长度的任意类型参数列表 |