ProGuard学习笔记
ProGuard是一个开源的Java类文件(.class)处理工具,相比起其它的Java混淆器,ProGuard更快,更易用。它同时提供了命令行和图形界面。ProGuard能够:
- 压缩(Shrunk ):检测未使用的类、字段、方法、属性,并移除
- 优化(Optimization):分析并优化方法的字节码,可以进行多步骤的优化
- 混淆(Obfuscation ):重命名类、字段、方法,使用更短且无意义的名字
- 预校验(Preverification ):为字节码添加预校验信息,预校验信息对Java 6+是需要的
上述功能可以单独使用,也可以在一次调用中按照下面的步骤一次性完成:
作为ProGuard输入的可以是jar、war、ear、zip、apk或者目录,经过最终处理后,输出到相应的压缩文件或者目录中。需要注意的是,输入文件可以包含资源文件,其名称和内容会被更新,以反映被混淆后的类名。
你需要为ProGuard指定库文件(lib jars),这些库用来编译被处理的那些类,这些库不会被改变。
为了判断哪些代码必须保留,那些可以被删除或者混淆,你需要为ProGuard指定“入口点”。入口点是一些类,它们通常是Main classes、methods、applets、activities,等等。ProGuard按如下规则使用入口点:
- 在压缩阶段:从入口点开始,递归的判断哪些类、类成员被使用,所有其它的则被丢弃
- 在优化阶段:非入口点函数或者方法,可能被设置为private、static、final、未使用的参数可能被移除,某些方法可能被内联
- 在混淆阶段:非入口点类、类成员被重命名
- 预校验阶段不需要知道入口点信息
反射和内省机制为ProGuard的处理带来困难,被动态创建(例如 Class.forName() )或者动态调用(根据字符串名称通过反射)的类/方法,必须作为入口点声明。在大量使用反射、基于配置文件的应用中(例如SSH框架),基本上不可能自动判断哪些类可以被保留,你需要在配置文件中使用 -keep 选项。
尽管如此,ProGuard能够自动识别并处理以下反射调用:
- Class.forName("SomeClass")
- SomeClass.class
- SomeClass.class.getField("someField")
- SomeClass.class.getDeclaredField("someField")
- SomeClass.class.getMethod("someMethod", new Class[] {})
- SomeClass.class.getMethod("someMethod", new Class[] { A.class })
- SomeClass.class.getMethod("someMethod", new Class[] { A.class, B.class })
- SomeClass.class.getDeclaredMethod("someMethod", new Class[] {})
- SomeClass.class.getDeclaredMethod("someMethod", new Class[] { A.class })
- SomeClass.class.getDeclaredMethod("someMethod", new Class[] { A.class, B.class })
- AtomicIntegerFieldUpdater.newUpdater(SomeClass.class, "someField")
- AtomicLongFieldUpdater.newUpdater(SomeClass.class, "someField")
- AtomicReferenceFieldUpdater.newUpdater(SomeClass.class, SomeType.class, "someField")
这些调用中引用的类、类成员不会被删除,如果执行混淆,它们会自动引用混淆后的名称。
命令格式如下:
1 2 3 |
java -jar proguard.jar @myconfig.pro #配置文件提供的选项 -verbose #命令行提供的选项 |
支持通过配置文件提供选项,配置文件中的注释以#开始。选项中,文件名具有空格的,必须使用引号包含。
选项 | 说明 |
@filename | 等价于-include filename,从文件中读取配置选项 |
-basedirectory dir | 指定后面所有选项中,相对路径的基准目录 |
-injars classpath | 指定需要处理的压缩包(各种ar)或者目录。其中的文件会被处理,并输出到-outjars指定的文件中。对于资源文件,默认不做修改的拷贝,可以使用过滤器选项对文件进行过滤。该选项可以出现多次 |
-outjars classpath | 指定输出压缩包或者目录,在此选项之前的所有-injars中的文件,都被输出到该选项指定的文件。可以使用过滤器选项对文件进行过滤 |
-libraryjars classpath | 指定输入所依赖的库,至少要包含输入类锁继承的那些类,仅仅被调用的类则不需要包含(但是包含了可以提升优化效果)。需要注意的是,程序使用的运行时库需要明确指定(例如rt.jar) |
-skipnonpubliclibraryclasses | 是否跳过依赖库中非public类,默认不跳过,跳过可以提升性能,因为库的非公共类一般与程序无关 |
-dontskipnonpubliclibraryclasses | 不跳过依赖库中的非public类,4.5+默认值 |
-dontskipnonpubliclibraryclassmembers | 不跳过包可见的依赖库类成员(字段、方法),通常依赖库中的default 成员与程序无关,但是某些程序可能刻意的把自己的包名弄得和依赖库一样 |
-keepdirectories filter | 指定在输出文件中保留的目录,如果不指定过滤器,则所有目录都保持。默认情况下所有目录都被移除 |
-target version | 指定输出class的版本,默认保持和输入版本一致。如果提高版本,则需要预校验;一般不能降低版本,因为低版本可能不支持某些构造 |
-forceprocessing | 强制处理,即使输出文件已经是最新的(up to date) |
注意选项的命名规律:-keep*用于防止目标被移除或者重命名、-keep*names则仅仅用于防止重命名。
选项 | 说明 |
-keep | -keep [,modifier,...] class_spec 作为入口点保留的类、类成员。对于库,所有公共成员都需要保留 |
-keepclassmembers | -keepclassmembers [,modifier,...] class_spec 指定要保留的类成员,如果它们所属的类被保留的话 |
-keepclasseswithmembers | -keepclasseswithmembers [,modifier,...] class_spec 指定需要保留的类,如果其成员符合条件 |
-keepnames class_spec | 等价于-keep,allowshrinking class_spec 指定其名称需要保留的类和类成员,如果在压缩阶段这些类没有被删除的话。该选项仅用于混淆阶段 |
-keepclassmembernames | -keepclassmembernames class_spec 指定名称要保留的类成员,如果在压缩阶段这些类没有被删除的话。该选项仅用于混淆阶段 |
-keepclasseswithmembernames | -keepclasseswithmembernames class_spec 指定要保留的类和类成员,如果所有指定的类成员在经历了压缩阶段还存在 |
-printseeds [filename] | 打印所有匹配-keep的类和类成员,默认打印到标准输出 |
-keep选项支持添加限定符,格式为: -keep,限定符 ,限定符包括:
限定符 | 说明 |
includedescriptorclasses | 方法、字段的类型描述符中的任何类,跟随被keep的方法/字段一并保留,通常用于保留Native方法的名字时,防止Native方法参数的名字被修改,以保证和Native库兼容 |
allowshrinking | 指定是否入口点可以被压缩 |
allowoptimization | 指定是否入口点可以被优化 |
allowobfuscation | 指定是否入口点可以被混淆 |
选项 | 说明 |
-dontshrink | 不压缩,默认压缩输入文件:除了匹配-keep的、以及它们直接、间接依赖的所有类、类成员,都被移除。压缩阶段可能随着优化阶段运行 |
-whyareyoukeeping class_spec | 打印类、类成员被保留的原因 |
选项 | 说明 |
-dontoptimize | 不进行优化。默认情况下优化启用,所有方法都在字节码级别进行优化 |
-optimizations opt_filter | 在更细粒度上控制进行哪些优化 |
-optimizationpasses n | 优化的步骤数,默认1步,如果发现没有可优化的,后续步骤自动省略 |
-assumenosideeffects class_spec | 指定不具有副作用(不改变任何状态信息)的方法规格,如果这些方法的返回值没有被使用,那么这样的调用会清除 |
-allowaccessmodification | 是否允许放宽类、类成员的访问限定符。这可能有利于优化,例如对getter()进行内联,需要相应字段成为public的 |
-mergeinterfacesaggressively | 允许接口合并,甚至在实现类没有实现所有接口方法的情况下 |
选项 | 说明 |
-dontobfuscate | 是否进行混淆,默认是。除了匹配-keep系列选项的类、类成员的名字将被随机的改为短名。为了方便调试而保留的内部属性,例如源代码名称、变量名、行号,都将被移除 |
-printmapping [filename] | 打印混淆前后类名、类成员名的对照 |
-applymapping filename | 指定一个先前生成的混淆名对照表,本次依照该对照继续混淆,不在表中的成员生成新的名字 |
-obfuscationdictionary filename | 指定存放有效混淆后变量名的文件 |
-overloadaggressively | 混淆时支持激进的重载,允许多个字段、方法使用重复的名字,只要参数和返回值不同 |
-useuniqueclassmembernames | 让不同名的类成员具有不同的混淆后的名称,让同名的类成员混淆后的名称依旧相同 |
-dontusemixedcaseclassnames | 混淆后,不使用混合大小写的类名 |
-keeppackagenames [pkg_filter] | 指定不混淆的包的过滤器,过滤器支持* **和前导的! |
-flattenpackagehierarchy [pkg_name] | 对所有被重命名的包进行重新打包,如果不指定参数值,则打包到根目录 |
-repackageclasses [pkg_name] | 对所有被重命名的类进行重新打包,如果不指定参数值,则打包到根目录 |
-keepattributes [attr_filter] | 指定需要保留的所有可选属性,参数值是逗号分隔的,所有JVM或ProGuard支持的属性值。支持使用? * ** !。在处理库的时候,至少应该保留Exceptions, InnerClasses, Signature属性,如果程序依赖于注解,则应该保留 |
-keepparameternames | 保留方法参数的名称和类型。该选项实质上保留了一个修剪(trim)后的LocalVariableTable、LocalVariableTypeTable这两个调试属性,在处理库时可能使用。注意,方法局部变量的名称依旧会混淆 |
-renamesourcefileattribute [string] | 设置SourceFile、SourceDir 属性为指定的常量值 |
-adaptclassstrings [class_filter] | 代表了类名的字符串常量值,是否也被混淆(与目标类的名字保持一致)。如果不指定参数值,所有代表类名的字符串常量都被混淆 |
-adaptresourcefilenames [file_filter] | 是否重命名资源文件,如果其文件名反映了一个被混淆的类的名字 |
-adaptresourcefilecontents [file_filter] | 是否修改资源文件中的类名,如果对应的类的名字已经被混淆。ProGuard使用平台默认字符集读取文件,如果需要改变这一行为,需要设置LANG环境变量或者JVM系统属性file.encoding |
选项 | 说明 |
-dontpreverify | 指定不进行字节码预校验,对于Java6+默认开启 |
-microedition | 只是目标类文件将在JME平台上运行 |
选项 | 说明 |
-verbose | 在处理时打印冗长的信息 |
-dontnote [class_filter] | 不打印配置选项中,与正则式匹配的类相关的错误或者疏忽 |
-dontwarn [class_filter] | 不打印配置选项中,与正则式匹配的类相关的重要错误,例如unresolved references |
-ignorewarnings | 忽略所有警告,强制进行处理。这可能很危险 |
-printconfiguration [filename] | 打印解析后的配置信息到目标文件 |
-dump [filename] | 打印处理后的类文件的内部结构 |
ProGuard支持通过Classpath指定输入文件、输出文件。Classpath的条目使用传统的分隔符(Unix:,Windows;),条目的顺序决定优先级,如果出现重复类,前面的生效。
每个输入条目可以是 :类/资源文件、apk、jar、war、aar、zip、目录;每个输出条目可以是apk、jar、war、aar、zip、目录。各种压缩包类型支持嵌套,每个输入条目输出到最接近它的输出条目中。
ProGuard支持对Classpath条目的内容进行过滤,下面是几个例子:
- rt.jar(java/**.class,javax/**.class) 匹配rt.jar中所有java/javax开头的包中的类
- input.jar(!**.gif,images/**)匹配input.jar中images/目录中所有文件,除了GIF
- input.war(lib/**.jar,support/**.jar;**.class,**.gif) 匹配input.war的lib、support目录下所有jar,以及所有类文件和GIF文件
?匹配文件名中的单个字符;通配符*匹配文件名中的多个字符;**匹配路径中的任意字符(包括多级目录层次);!表示取反。这种过滤器语法适用于配置选项的多个方面,例如文件、目录、类、包、属性、优化,等等。
ProGuard支持通过绝对路径/相对路径指定文件名,相对路径按如下规则顺序解析:
- 相对于-basedirectory目录
- 相对于配置文件所在目录
- 相对于工作目录
文件名可以包含使用 <> 包围的Java系统属性,例如 <java.home>/lib/rt.jar
如果文件名有空格,必须使用引号包围,例如 -injars "/your dir/your prog.jar"
类规格用于指定类、类成员的匹配规则,语法如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
# [] 表示其中的内容是可选的 # ... 表示前面列表中的项的任意组合是支持的 # | 用于分割两个备选项 # class/interface/enum用于引用相应的Java类型 # classname为全限定的类名,内部类使用$界定,例如java.lang.Thread$State。类名支持* ** ?通配符 # extends/implements的作用引用继承或者实现的Java类型 # @用于限定类、类成员被指定的类型所注解,annotationtype的格式和class一致 # <init> <fields> <methods> *分别匹配任意构造器、任意字段、任意方法和任意类成员 # 字段名、方法名支持通配符?、* # argumenttype fieldtype returntype等支持以下通配符: # %匹配任意基本类型;?匹配类名的单个字符;*匹配类全名中的多个字符,单不能跨越包分隔符 **匹配类全名中任意多个字符 # ***匹配任意类型;...匹配任意类型的任意数量的参数 [@annotationtype] [[!]public|final|abstract|@ ...] [!]interface|class|enum classname [extends|implements [@annotationtype] classname] [{ [@annotationtype] [[!]public|private|protected|static|volatile|transient ...] <fields> | (fieldtype fieldname); [@annotationtype] [[!]public|private|protected|static|synchronized|native|abstract|strictfp ...] <methods> | <init>(argumenttype,...) | classname(argumenttype,...) | (returntype methodname(argumenttype,...)); [@annotationtype] [[!]public|private|protected|static ... ] *; ... }] |
压缩、优化并混淆一个简单的Java程序:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
-injars myapplication.jar -outjars myapplication_out.jar -libraryjars <java.home>/lib/rt.jar #使用系统属性 -printmapping myapplication.map #打印混淆名称的映射关系 -optimizationpasses 3 #三步优化 -overloadaggressively -repackageclasses '' -allowaccessmodification #以main函数为入口点 -keep public class mypackage.MyMain { public static void main(java.lang.String[]); } |
压缩、优化并混淆一个库的典型配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
-injars in.jar -outjars out.jar -libraryjars <java.home>/lib/rt.jar -printmapping out.map #保留本地变量表(LocalVariableTable)、本地变量类型表(LocalVariableTypeTable) -keepparameternames -renamesourcefileattribute SourceFile #保留异常表,以便编译器知道哪些异常可能被抛出 #保留InnerClasses,否则外部无法引用内部类 #保留Signature,否则无法访问泛型 -keepattributes Exceptions,InnerClasses,Signature,Deprecated, SourceFile,LineNumberTable,*Annotation*,EnclosingMethod #保留所有公共类的公共/保护方法,这是库暴露的接口部分 -keep public class * { public protected *; } #保留native方法的规格,以便能与native库链接 -keepclasseswithmembernames,includedescriptorclasses class * { native <methods>; } -keepclassmembers,allowoptimization enum * { public static **[] values(); public static ** valueOf(java.lang.String); } #保留必要的类成员,以便串行化机制可用 -keepclassmembers class * implements java.io.Serializable { static final long serialVersionUID; private static final java.io.ObjectStreamField[] serialPersistentFields; private void writeObject(java.io.ObjectOutputStream); private void readObject(java.io.ObjectInputStream); java.lang.Object writeReplace(); java.lang.Object readResolve(); } |
压缩、优化并混淆一个完整的安卓应用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
#注意,标准的Android SDK构建过程已经集成了ProGuard,仅需要解除project.properties中的proguard.config=... #因此下面的配置示例并不需要,这里仅仅说明如何从头实现合适的ProGuard配置 -injars bin/classes -injars libs -outjars bin/classes-processed.jar -libraryjars /usr/local/java/android-sdk/platforms/android-9/android.jar -dontpreverify -repackageclasses '' -allowaccessmodification -optimizations !code/simplification/arithmetic -keepattributes *Annotation* #这些类型必须被原样的保留,不能移除或者重命名 -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.view.View { public <init>(android.content.Context); public <init>(android.content.Context, android.util.AttributeSet); public <init>(android.content.Context, android.util.AttributeSet, int); public void set*(...); } -keepclasseswithmembers class * { public <init>(android.content.Context, android.util.AttributeSet); } -keepclasseswithmembers class * { public <init>(android.content.Context, android.util.AttributeSet, int); } -keepclassmembers class * extends android.content.Context { public void *(android.view.View); public void *(android.view.MenuItem); } -keepclassmembers class * implements android.os.Parcelable { static ** CREATOR; } -keepclassmembers class **.R$* { public static <fields>; } -keepclassmembers class * { @android.webkit.JavascriptInterface <methods>; } |
通过指定日志方法没有副作用,可以移除代码中的日志记录调用:
1 2 3 4 5 6 7 |
-assumenosideeffects interface org.slf4j.Logger { public void trace(...); public void debug(...); public void info(...); public void warn(...); public void error(...); } |
升级字节码文件为Java6版本,这样可以更高效的载入JVM:
1 2 3 4 5 6 7 8 9 |
-injars in.jar -outjars out.jar -libraryjars <java.home>/lib/rt.jar -dontshrink -dontoptimize -dontobfuscate -target 1.6 |
保留所有类、类成员上的所有注解设置:
1 |
-keepattributes *Annotation* |
保留数据库驱动:
1 2 |
#数据库驱动往往是动态加载的 -keep class * implements java.sql.Driver |
使用依赖注入的应用中,防止私有类成员被移除:
1 2 3 4 5 6 7 8 9 |
-keepclassmembers class * { @javax.annotation.Resource *; } -keepclassmembers class * { @javax.inject.Inject *; } -keepclassmembers class * { @org.springframework.beans.factory.annotation.Autowired *; } |
当程序被混淆后,适配资源文件名、资源文件内容:
1 2 |
-adaptresourcefilenames **.properties,**.gif,**.jpg -adaptresourcefilecontents **.properties,META-INF/MANIFEST.MF |
可以通过Maven插件proguard-maven-plugin进行集成,参考:Maven POM文件配置示例
[…] https://blog.gmem.cc/proguard-study-note […]
[…] https://blog.gmem.cc/proguard-study-notehttp://developer.android.com/intl/zh-cn/tools/help/proguard.html […]