ProGuard 最全混淆规则说明

13,703 阅读20分钟

说明:本文参考(翻译)自Android SDK根目录下的proguard目录下的说明文档,是其中的一篇。,文中除了翻译外加了一些作者的实际验证。文章对Android混淆规则做一个解释说明。作者才疏学浅,如有错误,请谅解!<@_@>
Android混淆入门可参考《Android 代码混淆零基础入门》


5326.png

Input/Output Options 输入输出选项

-include filename
递归引入目录的配置文件
-basedirectory directoryname
-injars class_path
指定应用程序要处理的jars包(或者wars、ears、zip、或者目录结构),它们里面的class文件会被处理并被写入到输出jars里面。它们里面的任何非class文件会被直接复制过去但是不会处理。(需要注意过滤调一些IDE自动生成的文件);
-outjars class_path
指定输出jars(wars、ears、zip、目录结构)的名称;由-injars 指定的被处理的jars将被写入到指定的输出jars里。如果不指定outjars将不会有class文件被写入。
-libraryjars class_path 不混淆指定的jar库(android 项目中一般不混淆引入的第三方类库)
-skipnonpubliclibraryclasses 不混淆指定jars中的非public calsses
-dontskipnonpubliclibraryclasses 不忽略指定jars中的非public calsses (默认选项)和上面的选手想对
-dontskipnonpubliclibraryclassmembers
不忽略指定类库的public类成员(变量和方法),默认情况下,ProGuard会忽略他们
-keepdirectories [directory_filter] 指定要保持的目录结构,默认情况下会删除所有目录以减小jar的大小。
-target version
指定java版本号。 版本号可以是1.0,1.1,1.2,1.3,1.4,1.5(或仅5),1.6(或仅6)或1.7(或仅7)中的一个。 默认情况下,类文件的版本号保持不变。 例如,您可能想要将类文件升级到Java 6,通过更改其版本号并对其进行预验证。
-forceprocessing 强制处理输入(-injars)jars。即使输出jars是最新的。通过指定的输入,输出和配置文件或者目录的时间戳判断是否最新。

Keep Options 保留选项

-keep [,modifier,...] class_specification
指定需要保留的类和类成员(作为公共类库,应该保留所有可公开访问的public方法)
-keepclassmembers [,modifier,...] class_specification
指定需要保留的类成员:变量或者方法
-keepclasseswithmembers [,modifier,...] class_specification
指定保留的类和类成员,条件是所指定的类成员都存在(既在压缩阶段没有被删除的成员,效果和keep差不多)
-keepnames class_specification
[-keep allowshrinking class_specification 的简写]
指定要保留名称的类和类成员,前提是在压缩阶段未被删除。仅用于模糊处理
-keepclassmembernames class_specification
[-keepclassmembers allowshrinking class_specification 的简写]
指定要保留名称的类成员,前提是在压缩阶段未被删除。仅用于模糊处理
-keepclasseswithmembernames class_specification
[-keepclasseswithmembers allowshrinking class_specification 的简写]
指定要保留名称的类成员,前提是在压缩阶段后所指定的类成员都存在。仅用于模糊处理
-printseeds [filename]
指定详尽列出由各种-keep选项匹配的类和类成员。 列表打印到标准输出或给定文件。 该列表可用于验证是否真的找到了预期的类成员,特别是如果您使用通配符。 例如,您可能想要列出所有应用程序或您保存的所有小程序。


Keep选项概述对比(Overview of Keep Options)

作用范围 保持所指定类、成员 所指定类、成员在压缩阶段没有被删除,才能被保持
类和类成员 -keep -keepnames
仅类成员 -keepclassmembers -keepclassmembernames
类和类成员(前提是成员都存在) -keepclasseswithmembers -keepclasseswithmembernames
  • 建议初学者,如果不确定用那个keep选项就尽量用-keep,这个比较简单且不易使混淆代码出错。
  • 如果只指定类被保留,那么它的成员同样可能会被压缩、优化或者混淆。
  • 如果指定类成员被保留,那么其他代码也有可能会被压缩、优化或者混淆

Keep命令修饰符说明 [,modifier,...] :

可用在keep、keepclassmembers、keepclasseswithmembers命令后面

  • allowshrinking 指定对象可能会被压缩,即使他们被keep选项保留。如果所指定的对象不是必需的,则他们可能会被删出(在压缩步骤)。反之如果他们是必须的,则他们可能不会被优化或者混淆。

    如: 类ClassOneThree在代码中并没有被引用,是一个没有使用的类.
    -keep class com.dev.demo.one.ClassOneThree*{*;}
    -keep命令让上ClassOneThree保留下来,但是如果加了allowshrinking 修饰符结果就不一样了,下面这2条命令效果其实是一样的:ClassOneThree在压缩阶段就被删除了,没有被保留,因为它并没有被引用到。
    -keep,allowshrinking class com.dev.demo.one.ClassOneThree*{*;} -keepnames class com.dev.demo.one.ClassOneThree*{*;}

  • allowoptimization 指定对象可能会被优化,即使他们被keep选项保留。所指定对象可能会被改变(优化步骤),但可能不会被混淆或者删除。该修饰符只对实现异常要求有用。

  • allowobfuscation 指定对象可能会被混淆,即使他们被keep保留。所指定对象可能会被重命名,但可能不会被删除或者优化。该修饰符只对实现异常要求有用。

Shrinking Options 压缩选项

压缩是默认开启的。压缩会删除没有使用的类以及类成员,除了由各种“-keep”选项列出的类和它们直接或间接依赖的类。 在每个优化步骤之后都会进行压缩步骤,因为一些优化后可能会出现更多可以删除的类及类成员
-dontshrink
关闭压缩
-printusage [filename]
列出被那些未使用的代码,并可输出到指定文件。仅用于收缩阶段
-whyareyoukeeping class_specification
打印指定的类在压缩阶段为什么会保留其类、类成员的详细信息


Optimization Options 优化选项

优化是默认情况下启用的;。所有方法都在字节码级进行优化。
-dontoptimize
关闭优化
-optimizations optimization_filter
指定更精细级别的优化,仅用于优化阶段
-optimizationpasses n
指定优化次数,默认情况下1次。多次可以更进一步优化代码,如果在一次优化后没有改进则优化结束。仅用于优化阶段
-assumenosideeffects class_specification
指示没有任何副作用的类方法。优化过程中如果确定这些方法没有被调用或者返回值没有被使用则删除它们。ProGuard会分析出库代码以外的程序代码。如指定System.currentTimeMillis()方法,任何对他的空闲调用将被删除。仅实用与优化。慎用!
-allowaccessmodification
指示容许修改类和类成员的访问修饰符,这可以改进优化结果。
-mergeinterfacesaggressively
指示容许合并接口,及时他们的实现类没有实现所有接口方法。这个可以减少类的总数来减小输出的大小。仅实用于优化


Obfuscation Options 混淆

混淆是默认开启的。混淆使类和类成员名称变成短的随机名,被各种“-keep”选项保护的类、类成员除外。对调试有用的内部属性(源文件名称,变量名称,行号)将被删除。
-dontobfuscate
关闭混淆
-printmapping [filename]
打印旧名称到重命名的类、类成员的新名称的映射关系,可输出到指定文件。
仅实用于混淆处理
-applymapping [filename]
指定文件为映射文件,混淆时映射文件中列出的类和类成员接收指定的名称,文件未提及的类和类成员接收新名称。
-obfuscationdictionary [filename]
指定一个文本文件,其中所有有效字词都用作混淆字段和方法名称。 默认情况下,诸如“a”,“b”等短名称用作混淆名称。 使用模糊字典,您可以指定保留关键字的列表,或具有外来字符的标识符,例如: 忽略空格,标点符号,重复字和#符号后的注释。 注意,模糊字典几乎不改善混淆。 有些编译器可以自动替换它们,并且通过使用更简单的名称再次混淆,可以很简单地撤消该效果。 最有用的是指定类文件中通常已经存在的字符串(例如'Code'),从而减少类文件的大小。 仅适用于混淆处理。
-classobfuscationdictionary [filename]
指定一个文本文件,其中所有有效词都用作混淆类名。 与-obfuscationdictionary类似。 仅适用于混淆处理。
-packageobfuscationdictionary [filename]
指定一个文本文件,其中所有有效词都用作混淆包名称。与-obfuscationdictionary类似。 仅适用于混淆处理。
-overloadaggressively
深度重载混淆。这个选项可能会使参数和返回类型不同的方法和属性混淆后获得同样的名字,这个选项会使代码更小(且不易理解),仅实用于混淆处理

-useuniqueclassmembernames
指定为具有相同名称的类成员分配相同的混淆名称,并为具有不同名称(对于每个给定类成员签名)的类成员分配不同的混淆名称。 没有该选项,更多的类成员可以映射到相同的短名称,如'a','b'等。因此选项稍微增加了生成的代码的大小,但它确保保存的混淆名称映射可以始终 在随后的增量混淆步骤中受到尊重。

-dontusemixedcaseclassnames
混淆时不生成大小写混合的类名。 默认情况下,混淆的类名称可以包含大写字符和小写字符的混合。仅适用于混淆处理。

-keeppackagenames [package_filter]
指定不混淆给定的包名称。package_filter过滤器是以逗号分隔的包名称列表。包名称可以包含 ? * ** 通配符。仅适用于混淆处理。

-flattenpackagehierarchy [package_name]
将所有重命名后的包移动到给定的包中重新打包,如果没有参数或者空字符串,包将被移动到根包中,此选项进一步混淆包名称,可以使代码跟小更不易理解。仅适用于混淆处理。
-repackageclasses [package_name]
重新打包所有重命名的类文件,将它们移动到给定包中。 如果包中没有参数或一个空字符串,包被完全删除。 此选项将覆盖-flattenpackagehierarchy选项。 它可以使处理后的代码更小,更不容易理解。 它的已弃用名称是-defaultpackage。 仅适用于混淆处理。
-keepattributes [attribute_filter]
指定要保留的任何可选属性(注释...)。 可以使用一个或多个-keepattributes指令指定属性。 attribute_filter过滤器是以逗号分隔的属性名称列表。如果代码依赖于注释,则可能需要保留注释。 仅适用于混淆处理。
-keepparameternames
指定保留参数名称和保留的方法类型。 此选项实际上保留调试属性LocalVariableTable和LocalVariableTypeTable的修剪版本。 它在处理库时很有用。 一些IDE可以使用该信息来帮助使用库的开发人员,例如使用工具提示或自动完成。仅适用于混淆处理。
-renamesourcefileattribute [string]
指定要放在类文件的SourceFile属性(和SourceDir属性)中的常量字符串。 请注意,该属性必须以开头存在,因此也必须使用-keepattributes指令明确保留。 例如,您可能希望使已处理的库和应用程序生成有用的混淆堆栈跟踪。仅适用于混淆处理。
-adaptclassstrings [class_filter]
混淆和类名称对应的字符串常量,如果没有filter则匹配与类名称相对应的所有字符串常量,使用filter则仅匹配与filter匹配的类中的字符串常量。 仅适用于混淆处理。
-adaptresourcefilenames [file_filter]
根据相应类文件(如果有)的混淆名称指定要重命名的资源文件。 如果没有过滤器,则重命名与类文件相对应的所有资源文件。 使用过滤器,仅重命名匹配的文件。仅适用于混淆处理。
-adaptresourcefilecontents [file_filter]
指定要更新其内容的资源文件。 根据相应类的混淆名称(如果有)重命名资源文件中提到的任何类名。 如果没有过滤器,所有资源文件的内容都会更新。 使用过滤器,仅更新匹配的文件。 资源文件使用平台的默认字符集进行解析和编写。 您可以通过设置环境变量LANG或Java系统属性file.encoding来更改此默认字符集。 仅适用于混淆处理。


Preverification Options 预验证

预验证默认情况下启动的。能提高虚拟机的加载效率。
-dontpreverify
关闭预验证
-microedition
指定已处理的类文件针对Java Micro Edition。 然后,preverifier将添加适当的StackMap属性,这些属性与Java Standard Edition的默认StackMapTable属性不同。 例如,如果您正在处理midlet,则需要此选项。


General Options 一般选项

-verbose
混淆过程中打印详细信息,如果异常终止则打印整个堆栈信息
-dontnote [class_filter]
不打印配置类中可能的错误或遗漏的注释,如类名称中的拼写错误,或者可能有用的缺失选项。 可选class_filter是正则表达式; ProGuard不打印关于具有匹配名称的类的注释。
-dontwarn [class_filter]
不对指定的类、包中的不完整的引用发出警告
-ignorewarnings
忽略警告继续处理
-printconfiguration [filename]
打印配置信息,到指定文件。包括文件和替换的变量
-dump [filename]
打印类文件内部结构到指定文件


Class Paths

class_paths 主要用以指示选项(-injars、-outjars、-libraryjars)的输入输出文件路径。ProGuard接受类路径的泛化。由一系列文件和分隔符组成(在Unix上为':',windows上为';');文件的顺序决定着优先级。
输入项可包括:

  • class 文件或者 resource文件;
  • jar 文件, 可包含上述任意文件;
  • war文件,可包含上述任意文件;
  • ear文件, 可包含上述任意文件;
  • zip文件, 可包含上述任意文件;
  • 包含上面任意文件的目录结构。

直接指定类文件或者资源文件的路径会被忽略,类文件可作为jar文件、war、ear、zip或者目录结构的一个部分。此外,类文件的路径在归档或目录中不应有任何其他目录前缀。

输出项可包括:

  • jar 文件, 包含了所处理的所有class文件或者资源文件;
  • war文件,包含上述任意文件;
  • ear文件, 包含上述任意文件;
  • zip文件, 包含上述任意文件;
  • 包含上面任意文件的目录结构。

ProGuard 在输出时,会以合理的形式打包输出结果,根据需要重新构建输入文件。将所有类容写入输出目录是ProGuard最便捷的方式,输出目录包含了输入类容完整的重构。包可以是任意复杂的,可以是处理整个应用程序,zip文件中的包及其文档,将再次作为zip文件被处理。

ProGuard容许在文件名中称使用过滤器,它们能相对于完整的文件名过滤条一些文件或者其内容,过滤器位于括号()之间,他能支持5个类型以“;隔开”:

  • 对所有的zip文件名称的过滤;
  • 对所有的ear文件名称的过滤;
  • 对所有的war文件名称的过滤;
  • 对所有的jar文件名称的过滤;
  • 对于所有的class文件名,资源文件名。

括号中的过滤器如果少于5个,则它们将被假设为最后一个过滤器,空的过滤器将被忽视, 格式如下:
classpathentry([[[[zipfilter;]earfilter;]warfilter;]jarfilter;]filefilter)
“[]” 表示其内容是可选的。

rt.jar(java/**.class,javax/**class)
#匹配了rt.jar文件中,所有文件夹java、javax下的所有class文件

input.jar(!**.gif,images/**)
#匹配了input.jar中,images文件夹下除.gif格式的所有文件
#不同的过滤器可同时在一个条目中,它会被应用于所有相应的类型;不管它们的嵌套关系如何他们都是相交的
input.war(lib/**.jar,support/**jar;**.class,**.gif) 
#只包含了lib、support目录下的jar文件,并且匹配了它其中的所有.class文件和.gif文件

一些例子:

#所有输出类和资源将被合并到一个jar文件中。
-injars classes
-injars in1.jar
-injars in2.jar
-injars in3.jar
-outjars out.jar
# 将classes目录、in1.jar、in2.jar、in3.jar重构合并到out.jar文件中
#如果想保留输入的结构,则可以输出到目录结构(或者 war、ear、zip)中。
-injars in1.jar
-injars in2.jar
-injars in3.jar
-outjars out
# 输入文件能够保留原来文件名在目录out中构建

通过分组使用-injars和-outjars可以灵活的将jars(wars、ears、zips 或者 directories)合并到不同的jars(wars、ears、zips 或者 directories)中:

-injars base_in1.jar
-injars base_in2.jar
-injars base_in3.jar
-outjars base_out.jar

-injars  extra_in.jar
-outjars extra_out.jar

# base_in*.jar文件会被合并到base_out.jar文件中,extra_in.jar被合并到extra_out.jar文件中。
#过滤掉了所有images下的所有文件
-injars  in.jar(!images/**)
-outjars out.jar

#忽略掉java运行时不相关的类; “<>”表示引用系统变量java home(JAVA_HOME)
-libraryjars <java.home>/lib/rt.jar(java/**,javax/**)
-injars  in.jar
-outjars code_out.jar(**.class)
-outjars resources_out.jar
# **.class文件将被输出到code_out.jar文件中,而所有其他文件将被输出到resources_out.jar文件中。

文件名称(File Names)

ProGuard 接受绝对路径和相对路径作为文件或者路径名。相对路径规则为:

  • 首先相对与基础路径,如果没有设置则:
  • 相对于指定的配置路径,如果没有则:
  • 相对于工作目录。

名称中可包含java 系统配置,用“<>”括号包裹。系统配置将自动替代它们:

"<java.home>/lib/rt.jar" 将被解释为 "/usr/local/java/jdk/jre/lib/rt.jar."
"<user.home>" 解释为用户的Home目录
"<user.dir>"解释为当前的工作目录

注意:具有空格和括号的特殊字符名称必须用单引号或者双引号引用,名称列表中的每个文件名必须单独引用。在命令行中引号本身可能需要转义,以避免shell误解


文件过滤符号说明(File Filters)

参考 -adaptresourcefilenames、-adaptresourcefilecontents

“file_filter” 和一般的Filters一样,是可以用逗号(",")分隔的一个过路调的文件列表,只读取匹配到的文件,支持一下通配符:
匹配单个字符;
* 匹配任意多个字符,不包括目录分隔符;
** 匹配任意多个字符,不包括目录分隔符;
表示否(取反)。

java/**.class,javax/**.class //匹配java和javax目录以及其子目录下的所有.class文件
-keep class org.codehaus.jackson.* //保持org.codehaus.jackson下面的类文件,不包括其子包里面类文件
-keep class org.codehaus.jackson.**//保持org.codehaus.jackson下面所有类文件,包括其子包里面类文件
!**.gif,images/** 匹配images目录下面所有文件,但不包括.gif文件
-injars  in.jar(!images/**) //指定输入jar包,但移除images目录下面的所有文件

Filter

ProGuard 容许配置的很多选项(文件、目录、类、包、属性、优化...等的名称)使用过滤器。可以使用逗号","分隔。

"foo,*bar"匹配foo文件,和所有以bar结尾的名称。
"!foobar,*bar 匹配所有bar结尾名称,foobar除外。

Class Specifications(类的书写规范模版)

类规范是一个类和类成员的书写模版。它主要用在 -keep、-assumenosideeffects选项后面。它使得相应的选项只适应与模版匹配的类和类成员。
规范看起来类似Java语法,外加一些通配符。下面看一下类规范格式:

[@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 ... ] *;
    ...
}]
说明:

"[]“方括号表示里面的内容是可选的;
"..."省略号表示可以能很多选项;
"|"竖线划分了不同的选择
"()"普通非粗体括号表示属于同一组的属性

  • class关键字表示任何类或接口,interface 仅指示接口,enum关键字仅指示枚举;interface,enum前面加一个"!"表示非既是不是接口也不是枚举。
  • 每个classname必须是全名,如:java.lang.String、com.example.ClassName;同时classname还可以是包含下面一些通配符的正则表达式:

    ? 问号匹配单个字符,但不能是包名分隔符;如:"mypackage.Test?",能匹配"mypackage.Test1"、
    "mypackage.Test2"
    ,但是不能匹配"mypackage.Test12";

    * 单个星号能匹配任意多个字符,但除了包分隔符,如:"mypackage.*Test*",能匹配”mypackage.Test、mypackage.YourTestApplication“,但是不能匹配"mypackage.mysubpackage.MyTest"; "mypackage.*"匹配包mypackage下面的类,但不包括它的子包里面的类;
    ** 双星号匹配任意多个字符,包括包名的分隔符;如:"**.Test",匹配了所有Test类,"mypackage.*"匹配了包mypackage下面的所有子类,包括它的子包的类。

  • extends 、implements 指示一个特定的类,既是表示继承或者是实现了指定的类(接口)的类才符合条件。

  • @ 指示被注释类型注释了的类或类成员。注释类型与类名一样被指定。
  • 类成员、方法和java语法差不多,方法参数就想javadoc一样,不用包含参数名称。成员、方法也可以包含一些通配符:
    <init> 匹配了构造方法;
    <fields> 匹配了成员变量;
    <methods> 匹配了方法;
    * 匹配了任意成员、方法;
    上面这些没有任何返回类型,只有"<init>"有参数列表。
    成员变量、方法也可以是包含通配符的正则表达式。可以包含以下通配符:
    ? 匹配方法中任何单个字符;
    * 匹配方法中任何多个字符;
    类型可以包含一下通配符
    % 匹配方法中的原始类型("boolean"、"int"...,不包括"void");
    ? 匹配类名中的单个字符;
    * 匹配类名中的多个字符,不包括包分隔符;
    ** 匹配类名中的多个字符,包括包分隔符;
    *** 匹配原始类型,非原始类型,数组、非数组
    --- 匹配任意数量和类别的参数。
    注意:?、*、** 不会匹配原始类型,只有***可以;如:"** get*()"匹配“java.lang.Object getObject()” 不会匹配"float getFloat()" 也不匹配"java.lang.Object[] getObjects()";
  • 构造函数可以用短名(不含包名),或者全名(含包名);java中构造函数可以有参数,但没有返回类型。
  • 可以设置类,类成员访问修饰符(public,private...),它们通常帮助限制通配符类和成员。只能匹配具有指定修饰符的类或者成员。"!"表示非。
    容许设置多个标识("public static"),但如果它们是冲突的就只能设置一个(如publicprivate)

ProGuard支持编译器设置的一些修饰符synthetic、bridge、varargs;

下面是一些简单的列子:

#保持了类ClassOneOne里面所有public 修饰的成员和方法
-keepclassmembernames class com.dev.demo.one.ClassOneOne {
    public *;
}
#保持了ClassOne里面public 修饰的构造函数
-keep class com.dev.demo.ClassOne {
    public <init>();
}
#保持了类ClassTwoTwo里面public修饰的参数为int的构造函数
-keep class com.dev.demo.two.ClassTwoTwo {
    public <init>(int);
}

#保持了类ClassTwoThree 里面public修饰的方法和private修饰的成员变量
-keepclassmember class com.dev.demo.two.ClassTwoThree {
    public <methods>;
    private <fields>;
}
#保持了ClassTwoThree的子类以及里面的成员和方法
-keep class * extends com.dev.demo.two.ClassTwoThree {*;}

#保持了前缀为ClassOne的类以及里面的成员和方法
-keepnames class com.dev.demo.one.ClassOne*{*;}

#保持了ClassTwoTwo的内部类ClassTwoTwoInner里面的成员和方法
-keep class com.dev.demo.two.ClassTwoTwo$ClassTwoTwoInner{*;}