这篇文章上次修改于 1921 天前,可能其部分内容已经发生变化,如有疑问可询问作者。
本文已授权微信公众号「玉刚说」独家发布。

这篇是我们「Java 混淆那些事」第五讲,其实通过前四篇大家已经能够写出正常的混淆规则了,这一篇是简单的介绍一下不怎么常用的一些命令,个人觉得重要的会单独拿出来写个例子。大家可以简单看一遍用到的时候再来查或者直接去参考官方文档。

输入输出选项

命令解释
-include filename指定其他配置文件,可以指定多个。例如:-include proguard1.pro
@filename-include filename 的缩写。例如:@proguard1.pro、@test/proguard2.pro
-basedirectory directoryname配置后续出现相对路径的基目录。他默认的基目录是配置文件的目录,如果不存在配置文件基目录就是工作目录。例如:-basedirectory 'C:\test' ,可以指定多次。注:「后续」这个词很重要
-injars class_path指定要处理的文件( jar 或 aars,war,ear,zips,apks 或目录)。默认情况下,非类文件不会进行更改。请注意如果目录中有任何临时类文件(例如由 IDE 创建)不想被修改,可以过滤类路径中的条目。可以使用多个 -injars 选项指定类路径条目。
-outjars class_path指定输出文件(jar 或 aars,wars,ear,zips,apks 或目录)的名称。将 -injars 选项的文件处理完成过后输入到指定的文件。切记输出文件不能覆盖任何输入文件,否则容易出错。为了更好的可读性,可以使用多个 -outjars 选项指定类路径条目。如果不写 -outjars 选项,则不会有输出。
-libraryjars class_path指定混淆时需要依赖的类库文件( jar 或 aars,wars,ear,zips,apk s或目录)。这些 jar 里面的文件不会处理和输出,可以使用多个 -libraryjars 选项指定类路径条目。
-skipnonpubliclibraryclasses不处理 library 里面非 public 修饰的类。以加快处理速度并减少 ProGuard 的内存使用量。
-dontskipnonpubliclibraryclasses处理 library 中非 public 类。从 4.5 版本开始这是默认配置。
-dontskipnonpubliclibraryclassmembers处理 library 中public 的类成员。默认不处理。
-keepdirectories [directory_filter]输出文件中保留指定目录。默认情况下,目录会被移除。这会减少输出文件的大小。 
-target version将类文件 java 版本处理为指定版本。默认情况下,类文件的版本号保持不变。例如,我们给予 Java 5 写的项目想使用在 Java 6 的环境下,就可以通过指定 -target 更改版本号并将其预先验证,将类文件升级。但是降级需谨慎可能会有一些问题。
-forceprocessing强制输出,即使保证输出文件每次都是最新状态。

压缩选项

命令解释
-dontshrink关闭压缩功能。默认情况下,会开启压缩; 
-printusage [filename]把被压缩的类和方法输出到文件。主要用来验证自己的混淆规则正确不正确。
-whyareyoukeeping class_specification打印出压缩过程中保留了这些类文件和类成员的具体原因。例如:-whyareyoukeeping class **Manager { *; }

混淆选项

命令解释
-dontobfuscate关闭混淆功能。默认情况下,开启混淆。
-printmapping [filename]把重命名的类和类成员的新、旧名称的映射文件输入到指定文件。
-applymapping filename指定要重用的映射文件,映射文件中没有的类和类成员会被混淆为新的名称。如果代码结构发生根本变化,ProGuard 可能会打印出应用映射文件导致冲突的警告。您可以通过 -useuniqueclassmembernames 在两个混淆运行中指定选项来降低此风险。只允许一个映射文件。仅在混淆时适用。
-obfuscationdictionary filename指定类、方法及字段混淆后时用的混淆字典。默认使用 ‘a’,’b’ 等短名称作为混淆后的名称。
-classobfuscationdictionary filename指定类名的混淆字典。
-packageobfuscationdictionary filename指定包名的混淆字典。
-overloadaggressively开启侵入性重载混淆。多个字段及方法允许同名,只要它们的参数及返回值类型不同。该选项可使处理后的代码更小(及更难阅读)。只有开启混淆时可用。注:Dalvik 不能处理重载的静态字段。
-useuniqueclassmembernames类和成员混淆的时候,使用唯一的名字。
-dontusemixedcaseclassnames不使用大小写混合类名,注意,windows用户必须为 ProGuard 指定该选项,因为 windows 非大小写敏感,输出文件可能将会相互覆盖。
-keeppackagenames [package_filter]不混淆指定的包名。有多个包名可以用逗号隔开。包名可以包含 ?、*、** 通配符,还可以在包名前加上 ! 否定符。只有开启混淆时可用。如果你使用了 mypackage.MyCalss.class.getResource(""); 这些代码获取类目录的代码,就会出现问题。需要使用 -keeppackagenames 保留包名。
-flattenpackagehierarchy [package_name]将所有重命名的包重新打包到给定的单一包中。如果没参数或字符串为空,包移动到根包下。
-repackageclasses [package_name]把所有重命名的类重新打包到给定的单一包中。如果没参数或字符串为空,类的包会被完全移除。此选项会覆盖该 -flattenpackagehierarchy 选项。低版本的参数名是 -defaultpackage 。
-keepattributes [attribute_filter]保留任何可选属性。过滤器是由逗号分隔的 JVM 及 ProGuard 支持的属性列表。属性名可以包含 ?、*、** 通配符,并且可以在属性名前加上 ! 否定符。例如:处理库文件时应该加上 Exceptions,InnerClasses,Signature 属性。同时保留 SourceFile 及 LineNumberTable 属性使混淆后仍能获取准确的堆栈信息。同时如果你的代码有使用注解你可能会保留 annotations 属性。只有开启混淆时可用。
-keepparameternames保留方法参数名称和保留的方法类型。
-renamesourcefileattribute [string]指定一个常量字符串作为 SourceFile 属性的值。需要被 -keepattributes 选项指定保留。只有开启混淆时可用。
-adaptclassstrings [class_filter]混淆与完整类名一致的字符串。没指定过滤器时,所有符合现有类的完整类名的字符串常量均会混淆。只有开启混淆时可用。
-adaptresourcefilenames [file_filter]以混淆后的类文件作为样本重命名指定的源文件。没指定过滤器时,所有源文件都会重命名。
-adaptresourcefilecontents [file_filter]以混淆后的类文件作为样本混淆指定的源文件中与完整类名一致的内容。没指定过滤器时,所有源文件中与完整类名一致的内容均会混淆。

优化选项

命令解释
 -dontoptimize关闭优化功能。默认情况下启用优化。 
-optimizations optimization_filter指定优化的粒度规则,后面的参数是一个粒度过滤器,一般不做更改默认即可。
-optimizationpasses n表示对你的代码进行迭代优化的次数。参数是整数。一般来说设置为 10 以下即可,因为优化次数多了也不会有什么实质性的优化了,如果你有什么特殊业务需求,按自己需求调整就好了。
-assumenosideeffects class_specification优化阶段删除指定代码,比如: 删除所有日志 -assumenosideeffects class com.Log { \*; }
-assumenoexternalsideeffects class_specification优化阶段删除指定代码,力度比 -assumenosideeffects 强,因为它可以优化参数或堆。例如,删除日志记录代码时,如果日志包含 String 拼接的字节码就可以彻底删除了。 -assumenosideeffects 是无法在字节码层面删除的。
-assumenoescapingparameters class_specification 指定不允许其引用参数转义到堆的方法。 这些方法可以使用,修改或返回参数,但不能直接或间接地将它们存储在任何字段中。 例如,System.arrayCopy 方法不允许其引用参数转义,但方法 System.setSecurityManager 不会。
-assumenoexternalreturnvalues class_specification指定在调用时不返回已在堆上的引用值的方法。例如,ProcessBuilder#start 返回一个 Process 引用值,但它是一个在堆上并没有使用的新实例。
-allowaccessmodification优化时允许优化并修改类和类的成员的访问修饰符。对外提供的 SDK 包需要注意此选项。
-mergeinterfacesaggressively指定接口可以合并,即使它们的实现类未实现合并后接口的所有方法。该选项可以通过减少类的总数减少输出文件的大小。只有开启优化时可用。

保持选项

其他选项之前的文章已经介绍过了,这里只介绍之前没说过的。

命令解释
-if class_specification如果 if 指定类和类成员存在,随后的keep选项(-keep,-keepclassmembers,...)才会进行匹配。
-printseeds [filename]将详尽列出由各种 -keep 选项匹配的类和类成员列表输出到指定文件。该列表可用于验证是否确实找到了预期的类成员,尤其是在使用通配符时。例如,您可能希望列出您保留的所有应用程序或所有小程序。

预校验选项

命令解释
-dontpreverify关闭预校验功能。默认情况下,如果类文件针对 Java ME 或 Java 6 或更高版本,则会对其进行预验证。对于 Java ME ,需要预验证。对于 Java 6,预验证是可选的,但从 Java 7开始,它是必需的。如果类文件针对 Android 时,没有必要,因此您可以将其关闭以减少处理时间。
-microedition指定已处理的类文件以 Java ME 为目标。然后,预验证程序将添加适当的StackMap属性,这些属性与 Java SE 的默认 StackMapTable 属性不同。例如,如果要处理 midlet,则需要此选项。
-android指定已处理的类文件以Android平台为目标。然后ProGuard确保某些功能与 Android 兼容。

常规选项

命令解释
 -verbose在处理期间打印更多信息。如果程序以异常终止,则此选项将打印出整个堆栈跟踪,而不仅仅是异常消息。
-dontnote [class_filter]指定不打印有关配置中潜在错误或遗漏的信息,例如配置中的类名拼写错误或可能缺失有用的选项。会有提示。
-dontwarn [class_filter]指定找不到引用或其他重要问题时不打印警告信息。例如,在某个类的引用中找不到相关类,会有警告提示。使用 dontwarn 就可以忽略提示。
-ignorewarnings打印找不到引用或其他重要问题的警告信息。
-printconfiguration [filename]将已解析的整个配置输出到指定文件。
-dump [filename]指定将类文件的内部结构输出到指定文件。
-addconfigurationdebugging指定用调试语句对处理过的代码进行测试,这些调试语句会打印出缺少 ProGuard 配置的建议。如果处理过的代码由于仍然缺少一些反射配置而崩溃,他会提示一些简易的配置。例如,代码可能正在使用 GSON 库序列化类,您可能需要对其进行一些配置。通常,您可以将控制台中的建议 复制/粘贴 到配置文件中。注:不要在发行版本中使用此选项,因为它会将混淆信息添加到处理过的代码中。

选项参数格式

上面的命令后面有很多参数格式,我们来说一说各个参数怎么写。
首先说一下带 [] 的参数是可选的不是必填,而不带 [] 的参数是必填。

参数名作用
filename表示具体文件名,可以是相对路径,也可以是绝对路径。比如 test/test111.txttest111.txt
string表示随便一段字符串,比如 "xxx"
class_filter表示类过滤器,具体用法参考上一章类名过滤器
class_path表示类文件路径,可以是 jars, aars, wars, ears, zips, apks 或目录,可以是相对路径,也可以是绝对路径。
file_filter表示文件过滤器,具体用法参考上一篇文件相关的过滤器
class_specification表示类规范是类和类成员的模板,上一篇有提到
directory_filter表示目录过滤器,比如 com/httpcom/* 具体用法参考上一篇文件相关的过滤器
package_name表示包名 com.http
package_filter表示包名过滤器,比如 com.*
version表示 Java 版本号可以是 1.0,1.1,1.2,1.3,1.4,1.5,1.6,1.7 (7)、1.8(8)、1.9(9)或 10 。

还有两个指令不是一两句话可以说明,我们单独拿出来讲。

optimization_filter

如果有多个规则,可以使用通配符或 ,(逗号) 隔开写多个规则。在调整优化粒度规则的时候,希望你能够研究明白再使用,以免出现什么问题。
支持三个通配符 ? 、 * 、! ,它们代表什么我就不多说了。

我列举一下固定的,其他的不稳定的我就不列举了,有需要希望大家去看看帮助文档。

规则作用
class/marking/final尽可能将类标记为 fianl 类。
class/unboxing/enum尽可能将枚举类型简化为整数常量。
class/merging/vertical垂直合并类 ( 上下层级的类 进行合并 )。
class/merging/horizontal水平合并类 ( 同一层级的类 进行合并 )。
field/removal/writeonly移除只读字段。
field/marking/private尽可能将字段标记为私有。
field/propagation/value在方法中传递属性值。
method/marking/private尽可能将方法标记为私有。
method/marking/static尽可能将方法标记为静态。
method/marking/final尽可能将方法标记为 final 方法。
method/removal/parameter删除没有用到的方法。
method/propagation/parameter将方法参数的值从方法调用传到调用的方法。
method/propagation/returnvalue将方法返回的值从方法传到它们的调用处。
method/inlining/short合并比较短的方法。
method/inlining/unique合并只调用一次的方法。
method/inlining/tailrecursion简化尾部递归调用。(尾递归转循环)
code/merging合并相同的代码块。
code/simplification/variable变量加载和存储时,使用窥孔优化(peephole optimization)技术,一种优化技术。
code/simplification/arithmetic对算术指令进行窥孔优化。
code/simplification/cast对类型转换进行窥孔优化。
code/simplification/field对属性加载和存储使用窥孔优化。
code/simplification/branch对分支指令使用窥孔优化。
code/simplification/string对常量字符串使用窥孔优化,最好使用 code/removal/variable 删除。
code/simplification/advanced基于控制流分析和数据流分析简化代码。
code/removal/advanced基于控制流分析和数据流分析删除死代码。
code/removal/simple基于简单控制流分析简化代码。
code/removal/variable从本地变量中,删除未使用的变量。
code/removal/exception当 try-catch 中,try块内为空时,删除exceptions。
code/allocation/variable在本地变量中优化变量分配。

attribute_filter

类文件实质上定义了类,它们的字段和方法。许多基本和非必要数据作为属性附加到这些类,字段和方法。例如,属性可以包含字节码,源文件名,行号表等。ProGuard 的混淆步骤删除了执行代码通常不需要的属性。如果我们需要保留就需要使用 -keepattributes 来进行保留。同样多个使用逗号,支持三个通配符 ? 、 * 、!

可选的属性
可选项解释
*Annotation* 注解。
SourceFile 从中编译类文件的源文件的名称。
SourceDir从中编译类文件的源目录的名称。
InnerClasses 类及其内部类和外部类之间的关系。
EnclosingMethod定义类的方法。
Deprecated表示不推荐使用类,字段或方法。
Synthetic 表示编译器生成了类,字段或方法。
Signature 类、字段或方法的通用签名,代码可以通过反射访问此签名。
MethodParameters方法参数的名称和访问标志。
Exceptions指定方法可能抛出的异常。
LineNumberTable方法的行号。
LocalVariableTable 方法的局部变量的名称和类型。
LocalVariableTypeTable 方法的局部变量的名称和泛型类型。
RuntimeVisibleAnnotations 在运行时,类,字段和方法可见的注释。
RuntimeInvisibleAnnotations 在编译时对类,字段和方法可见的注释。
RuntimeVisibleParameterAnnotations 方法参数在运行时可见的注释。
RuntimeInvisibleParameterAnnotations 方法参数在编译时可见的注释。
RuntimeVisibleTypeAnnotations 在运行时可见的注释,用于泛型类型,指令等。
RuntimeInvisibleTypeAnnotations 在编译时可见的注释,用于泛型类型,指令等。
AnnotationDefault注释的默认值。

例子

flattenpackagehierarchy

// 混淆规则
-keep class **Manager {
    <fields>;
    <methods>;
}
-flattenpackagehierarchy "xxx"

混淆前的结构

混淆后的结构

  1. 所有改变包名的都放到了 xxx 包下

repackageclasses

// 混淆规则
-keep class **Manager {
    <fields>;
    <methods>;
}
-repackageclasses "xxx"

混淆前的结构

混淆后的结构

  1. 所有改变类名的类都放到了 xxx 包下

adaptclassstrings

// 混淆规则
-keep class **Manager {
    <fields>;
    <methods>;
}
-keepclassmembers,allowobfuscation class **{
    public java.lang.String get();
}
-adaptclassstrings

// 混淆前的代码
public String get() {    
    String ss1 = "com.test.http.HttpRequest";
    return "请求成功 "+ss1;
}

// 混淆后的代码
public String a() {
    return "请求成功 " + "com.test.a.a";
}
  1. 混淆与类名相同的字符串,用途大家可以想一想,比如反射的时候可以用。Class.forName("com.test.http.HttpRequest")

adaptresourcefilenames

// 混淆规则
-keep class **Manager {
    <fields>;
    <methods>;
}
-adaptresourcefilenames

混淆前的资源文件

混淆后的资源文件

  1. 混淆与类名相同的资源文件

renamesourcefileattribute

// 混淆脚本
-keepattributes SourceFile
-renamesourcefileattribute "renamesourcefileattribute Test"

// 代码
/* compiled from: renamesourcefileattribute Test */
public final class a {
}

发现保留了 SourceFile 属性,并且指定了一个字符串。

adaptresourcefilecontents

// 混淆脚本
-adaptresourcefilecontents

//混淆前的资源文件
test:com.test.http.HttpRequest

//混淆后的资源文件
test:com.test.a.a
  1. 大家可以想想用途,比如 Android 中想混淆 Activity 就需要修改清单文件类路径,这个属性就可以自动修改了。

小结

这一篇内容比较多大家过一遍就好,如果你自己觉得还有其他能用到的选项就需要自己动手去试一试效果了。