Android混淆之ProGuard

4,938 阅读20分钟

@[TOC]

初识ProGuard

Android开发的小伙伴们都或多或少的接触过混淆,很多人都对混淆很困惑。需要发版的时候,从网上load一份混淆文件,或从其他项目中拷贝一份过来,修改一下,管用就不去管了,有问题就卡住了,各种baidu也不一定能解决问题。本文力求让大家对混淆规则轻车熟路,能快速的上手。知其然也能知其所以然。

从Android Studio2.3开始,已经集成了ProGuard。ProGuard是一款Java类文件的混淆器,集成了压缩器,优化器,混淆器和预验证器。 与其他Java混淆器相比,ProGuard的主要优势可能是其紧凑的基于模板的配置。通常只需几个直观的命令行选项或一个简单的配置文件即可。ProGuard减少了处理后的代码的大小,并带来了一些潜在的效率提高。处理几兆字节的程序和库只需要几秒钟。

ProGuard的典型用途是: 创建更紧凑的代码,以实现更小的代码归档,更快的网络传输,更快的加载和更小的内存占用。 使程序和库更难以逆向工程。 列出无效代码,可以将其从源代码中删除。 重新定位和预先验证Java 6的现有类文件,以充分利用Java 6更快的类加载速度。

在gradle中配置ProGuard开启混淆很简单:

android {
    buildTypes {
        release {
            minifyEnabled true
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

minifyEnabled 属性设为 true即开启混淆。 proguardFiles 属性指定了混淆文件的所在目录。proguard-android.txt为sdk路径下默认的混淆文件,后面的'proguard-rules.pro'就是我们自定义的混淆文件。

ProGuard处理代码的流程如下: Shrunk压缩器可检测并删除未使用的类,字段,方法和属性。 Optimize优化器分析并优化了方法的字节码。 Obfuscate混淆器使用简短的无意义名称重命名其余的类,字段和方法。 这些步骤使代码库更小,更有效,并且更难以逆向工程。 最后的Preverify预验证器将预验证信息添加到类中,这对于Java Micro Edition是必需的,可以缩短Java 6的启动时间。

在这里插入图片描述

ProGuard详解

什么在压缩?

Java源代码(.java文件)通常被编译为字节码(.class文件)。字节码比Java源代码更紧凑,但是字节码仍可能包含许多未使用的代码,尤其是在包含程序库的情况下。压缩程序(例如ProGuard)可以分析字节码并删除未使用的类,字段和方法。该程序在功能上保持等效,包括异常堆栈跟踪中给出的信息。

什么是混淆?

默认情况下,已编译的字节码仍包含许多调试信息:源文件名,行号,字段名,方法名,参数名,变量名等。此信息使直接编译字节码和对整个程序进行反向工程变得很简单。有时,这是不可取的。诸如ProGuard之类的混淆器可以删除调试信息,并以无意义的字符序列替换所有名称,这使得对代码进行反向工程变得更加困难。同时它进一步压缩了代码。该程序在功能上保持等效,除了在异常堆栈跟踪中给出的类名,方法名和行号。

反射

反射给代码的自动处理带来了特殊的问题。 在ProGuard中,必须将代码中动态创建或调用的类或类成员指定为入口点, 用keep选项保护起来。 例如,Class.forName()构造可以在运行时引用任何类。 通常无法预见必须保留哪些类(及其原始名称),比如可以从配置文件中读取类名称。 因此,必须在ProGuard配置中使用相同的简单-keep选项来指定它们。

此外,如果需要保留某些类或类成员,ProGuard将提供一些建议。 例如,ProGuard将注意类似“(SomeClass)Class.forName(variable).newInstance()”的结构。 这些可能表明该类或接口SomeClass或它的实现可能需要保留。 然后,我们可以相应地调整混淆配置。

混淆选项

一份混淆文件主要有一系列的keep选项及非keep选项构成。keep选项用来告诉ProGuard哪些类、类成员不被混淆;非keep选项包括输入、压缩、优化、 混淆、常规等选项,用来告诉ProGuard额外的配置。

非keep选项

  • 输入选项 -skipnonpubliclibraryclasses 指定在读取库jar时跳过非公共类,以加快处理速度并减少ProGuard的内存使用量。默认情况下,ProGuard会读取非公共和公共库类。但是,非公用类通常不相关,只要它们不影响输入jar中的实际程序代码即可。然后忽略它们可以加快ProGuard的速度,而不会影响输出。不幸的是,某些库,包括最近的JSE运行时库,都包含由公共库类扩展的非公共库类。如果由于设置了此选项而无法找到类,则ProGuard将打印警告。

    -dontskipnonpubliclibraryclasses 指定不忽略非公共库类。从4.5版开始为默认设置。

    -dontskipnonpubliclibraryclassmembers 指定不忽略包可见的库类成员(字段和方法)。默认情况下,ProGuard在解析库类时会跳过这些类成员,因为程序类通常不会引用它们。但是,有时程序类与库类位于同一包中,并且它们确实引用其包可见的类成员。在这种情况下,实际读取类成员可能很有用,以确保处理后的代码保持一致。

  • 压缩选项 默认开启压缩; 除各种-keep选项列出的类以及它们直接或间接依赖的类之外,所有类和类成员 都将被删除。 在每个优化(optimization )步骤之后,还会执行压缩步骤,因为优化后可能会再次暴露一些未被使用的类和成员。 关闭压缩:-dontshrink

  • 优化选项 默认开启优化。所有方法都在字节码级别进行了优化。但某些时候,优化可能导致程序执行异常,它可能会改变程序原有的逻辑。比如删除了某些特殊的注释,删除了它认为无意义的空loop。 关闭优化:-dontoptimize

    -optimizationpasss n 指定要执行的优化遍数。默认情况下,执行一次通过。多次通过可能会有进一步的改进。如果 在优化通过后未发现任何改进,则优化结束。仅在优化时适用。

    -allowaccessmodification 指定在处理过程中可以扩大类和类成员的访问修饰符。这样可以改善优化步骤的结果。

  • 混淆选项 默认开启混淆。除了各种-keep选项列出的名称外,类和类成员会收到新的简短随机名称。删除了对调试有用的内部属性,例如源文件名,变量名和行号。 关闭混淆:-dontobfuscate

    -printmapping [文件名] 指定为已重命名的类和类成员打印从旧名称到新名称的映射。映射将打印到标准输出或给定文件。

    -useuniqueclassmembernames 该选项将为需要混淆的类生成唯一的混淆名称。如果没有该选项,则将更多的类成员映射到相同的短名称,如“ a”,“ b”等。

    -dontusemixedcaseclassnames 指定在混淆时不生成大小写混合的类名,即全部小写。 默认情况下,混淆的类名可以包含大写字符和小写字符的混合。

    -keeppackagenames [package_filter] 指定不混淆指定的包名称。 可选的过滤器是包名称的逗号分隔列表。包名称可以包含?,*和**通配符,或在其前面加上!。 主工程不同的库工程时,不同的库工程混淆后的类名可能冲突,比如都是a.a.a.a。当主工程引用混淆后的库aar时就会编译出错:

#Duplicate class a.a.a.a found in modules classes.jar (:libA-release:) and classes.jar (:libB-release:)

这时可以keeppackagenames指定一个库的包名称不混淆来避免此问题。

-keepattributes [attribute_filter ] 指定要保留的可选属性。可以使用一个或多个-keepattributes指令指定属性。可选过滤器是用逗号分隔的属性名称列表。属性名称可以包含?,*和**通配符,或在其前面加上!。 典型的可选属性包括: Exceptions,Signature,InnerClasses,Deprecated,SourceFile,SourceDir,LineNumberTable,LocalVariableTable,LocalVariableTypeTable,Synthetic,EnclosingMethod,RuntimeVisibleAnnotations,RuntimeInvisibleAnnotations,RuntimeVisibleParameterAnnotations,RuntimeInvisibleParameterAnnotations和AnnotationDefault。

例如,在处理库时,至少应保留Exceptions,InnerClasses和Signature属性。还应该保留SourceFile和LineNumberTable属性,以产生有用的混淆堆栈跟踪。最后,如果您的代码依赖注释,则可能需要保留注释。

示例:

-keepattributes Exceptions,InnerClasses,Signature #保留内部接口或内部类、内部类、泛型签名类型
-renamesourcefileattribute SourceFile #将崩溃日志文件来源重命名为“SourceFile”
-keepattributes SourceFile,LineNumberTable #产生有用的混淆堆栈跟踪
-keepattributes *Annotation* #保留注释
  • 常规选项

    -verbose 指定在处理期间输出更多信息。如果程序因异常终止,则此选项将打印出整个堆栈跟踪,而不仅仅是异常消息。

    -dontnote [class_filter] 指定不打印有关配置中潜在错误或遗漏的注释,例如类名中的错字或缺少可能有用的选项。可选过滤器class_filter是一个正则表达式; ProGuard不会打印与可选名称匹配的类的注释。

    -dontwarn [class_filter] 指定不警告尚未解决的引用和其他重要问题。可选过滤器class_filter是一个正则表达式; ProGuard不会打印与可选名称匹配的类的警告。忽视警告可能很危险。例如,如果确实需要对未解析的类或类成员进行处理,则处理后的代码将无法正常运行。仅当知道自己在做什么时才使用此选项!

    -ignorewarnings 指定打印有关未解决引用和其他重要问题的任何警告,但在任何情况下都将继续处理。忽视警告可能很危险。例如,如果确实需要对未解析的类或类成员进行处理,则处理后的代码将无法正常运行。仅当知道自己在做什么时才使用此选项!

    文件过滤器 文件过滤器是逗号分隔的文件名列表,可以包含通配符。 支持以下通配符: ? 匹配名称中的任何单个字符。 *   匹配名称的不包含包分隔符“.”或目录分隔符"/"的任何部分。 **  匹配名称的任何部分,可能包含任意数量的包分隔符或目录分隔符。 例如,"java / **.class,javax / **.class" 匹配java和javax中的所有类文件。“ foo,*bar”匹配名称foo和所有以bar结尾的名称。

此外,名称前可以带有一个负号“!”。从匹配的文件名中排除该文件名。例如,

"!**.gif,images/** " # 匹配images目录中的所有文件,gif文件除外。
"!foobar,*bar" #匹配所有以bar结尾的名称,但foobar除外。

keep选项

keep选项用来在混淆规则中声明需要保留的类和类成员,防止它们被删除和重命名。一般的格式如下:

-keep选项   class_specification class_specification是类和成员的模板,用来指定应用keep规则的若干类及其成员.

根据能否在压缩阶段被删除和在混淆阶段被重命名,keep选项分为两类: 第一类,不带names,不能被删除、不能被重命名:-keep、-keepclassmembers、-keepclasseswithmembers,分别对应 同时保留类和类成员、只保留类成员、根类据成员找到满足条件的所有类而不用指定类名,保留类名和成员名。 第二类,带names,不能被重命名:-keepnames、-keepclassmembernames 、-keepclasseswithmembernames,分别对应 同时保留类和类成员不被重命名、只保留类成员不被重命名、根类据成员找到满足条件的所有类而不用指定类名,保留类名和成员名不被重命名。对于第二类,如果类没有被调用到,则在压缩阶段就会被删除。

如图:

在这里插入图片描述

class_specification

class_specification是类和成员的模板,用来指定应用keep规则的若干类及其成员。格式如下:

[@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关键字将匹配项限制为枚举类。在接口或枚举关键字之前加!将匹配分别限制为不是接口或枚举的类。

  • 每个类名必须完全合格,例如java.lang.String。可以将类名指定为包含以下通配符的正则表达式: ?   匹配类名称中的任何单个字符,但不匹配包分隔符"."。  *   匹配除包分隔符之外的类名的任何部分。

    "mypackage.*" 与mypackage中的所有类匹配,但与子包中的所有类都不匹配。
    

    ** 匹配类名的任何部分,可能包含任意数量的包分隔符。

    **.Test 匹配除根包以外的所有包中的所有Test类。
    "mypackage.**" 与mypackage及其子包中的所有类匹配。
    
  • @annotationtype 可用于将类和类成员限制为使用指定注释类型进行注释的成员。指定注释类型就像类名一样。字段和方法的指定与Java中的指定非常相似,注释类型的方法参数列表不包含参数名称。

  • 类名* 表示任何类,无论其包如何。

  • < init> 匹配任何构造函数 < fields> 匹配任何字段 < methods> 匹配任何方法  *  匹配任何字段或方法。 请注意,上述通配符没有返回类型。仅< init>通配符具有参数列表

  • 字段和方法名称可以包含以下通配符: ?匹配方法名称中的任何单个字符。 * 匹配方法名称的任何部分。

  • 描述符中的类型可以包含以下通配符: % 匹配任何原始类型(“ boolean”,“ int”等,但不匹配“ void”)。 ? 匹配名称中的任何单个字符。 *  与不包含包分隔符的名称的任何部分匹配。 ** 匹配类名的任何部分,可能包含任意数量的包分隔符。 *** 匹配任何类型(原始或非原始,数组或非数组)。 ... 匹配任何类型的任意数量的参数。

1.  请注意,?,*和** 通配符永远与基本类型不匹配。
2. 此外,只有*** 通配符可以匹配任何维度的数组类型。
   例如,"**get*()"匹配"java.lang.Object getObject()",但不匹配" float getFloat()",也不匹配“”java.lang.Object [] getObjects()"。
3. 也可以使用构造函数的短类名(不带包)或完整的类名来指定构造函数。与Java语言一样,构造函数规范具有参数列表,但没有返回类型。
 4. 允许组合多个类成员访问修饰符标志(例如public static)。

ProGuard其他需要注意的事项

  • 保留native方法 对于native方法,则需要保留它们的名称和类的名称,以便它们可以链接到本地库:
-keepclasseswithmembernames class * {
    native <methods>;
}
  • 保留枚举 如果程序代码中包含枚举类,则必须保留一些特殊方法。 Java 5中引入了枚举。java编译器将枚举转换为具有特殊结构的类。 值得注意的是,这些类包含一些静态方法的实现,运行时环境可以通过内省访问。必须明确指定这些内容,以确保它们不会被删除或混淆:
-keepclassmembers enum * {
    public static **[] values();
    public static ** valueOf(java.lang.String);
}

ProGuard的一些技术问题

使用ProGuard时,您应该注意一些技术问题,可以轻松避免或解决所有这些问题。ProGuard在处理代码时,可能会打印出一些注意事项和非致命警告:

  • Note: ... calls '(...)Class.forName(variable).newInstance()'

ProGuard会列出动态创建的类实例的所有类强制转换,例如“(MyClass)Class.forName(variable).newInstance()”。我们可能需要使用“ -keep class MyClass”之类的选项来保留所提及的类,或者使用“ -keep class * implements MyClass”之类的选项来保留其实现。

  • Note: ... accesses a field/method '...' dynamically

ProGuard列出了许多构造,例如“ .getField(“ myField”)“。我们可能需要弄清楚所提到的类成员的定义位置,并使用“ -keep class MyClass {MyFieldType myField;}”之类的选项来保留它们。否则,ProGuard可能会删除或混淆类成员。

  • 优化导致的意外错误 通常是在优化步骤中ProGuard遇到了意外,可能不会恢复。可以使用-dontoptimize选项来避免这种情况

  • 保留注释类 如果要基于注释保留类,则可能避免在压缩步骤中删除注释类本身。您可以使用“ -keep @interface *”之类的选项将所有注释类明确保留在程序代码中。

  • ClassNotFoundException 代码可能正在调用Class.forName,试图动态创建缺少的类。 ProGuard只能检测常量名称参数,例如Class.forName(“ mypackage.MyClass”)。对于像Class.forName(someClass)这样的变量名参数,您必须使用适当的-keep选项来保留所有可能的类,例如:

"-keep class mypackage.MyClass" 
"-keep class * implements mypackage.MyInterface".
  • NoSuchMethodException 代码可能正在调用诸如myClass.getMethod之类的内容,试图动态查找某些方法。而这些方法已经被混淆了。因此必须使用适当的-keep选项:
"-keep class mypackage.MyClass { void myMethod(); }"

更具体地说,如果报告为丢失的方法是value或valueOf,则可能必须保留一些与枚举有关的方法。

  • Disappearing annotations 默认情况下,混淆步骤将删除所有注释。如果您的应用程序依赖注释来正常运行,则应使用"-keepattributes * Annotation *" 明确保留它们。

  • Disappearing loops 如果您的代码包含空的繁忙等待循环,则ProGuard的优化步骤可能会将其删除。如果与实际逻辑冲突,则必须使用-dontoptimize选项关闭优化。

  • ClassCastException: class not an enum, or IllegalArgumentException: class not an enum type 应确保保留枚举类型的特殊方法,运行时环境通过自省调用该方法。

  • ArrayStoreException: sun.reflect.annotation.EnumConstantNotPresentExceptionProxy
    可能正在处理涉及枚举的注释。 同样,您应该确保保留枚举类型的特殊方法。

  • 对于dex编译器和Dalvik VM,预验证是无关紧要的,因此我们可以使用-dontpreverify选项将其关闭。

  • -optimizations选项禁用Dalvik 1.0和1.5无法处理的某些算术简化。Dalvik VM也无法处理(静态字段)过度的过载。

总结一下,就是:

  • 需要动态访问的类或类成员,需要保留
  • 慎用优化,甚至直接使用-dontoptimize禁用
  • Android中预验证无效,使用-dontpreverify将其关闭
  • 根据需要保留注释和枚举

一份通用的ProGuard混淆文件

最后,提供一份较通用的ProGuard混淆文件参考。 我们保留了应用程序的AndroidManifest.xml文件可能引用的所有基本类。如果清单文件包含其他类和方法,可能还必须指定它们。

我们保留注释,因为它们可能由自定义RemoteView使用。

我们将使用典型的构造函数保留所有自定义View扩展和其他类,因为它们可能是从XML布局文件引用的。

我们还将所需的静态字段保留在Parcelable或Serializable实现中,因为可以通过自省访问它们。

最后,我们保留了自动生成的R类的引用内部类的静态字段,以使调用代码通过自省访问这些字段。

如果您使用的是Google的可选许可证验证库,则可以将其代码与自己的代码混淆。 您必须保留其ILicensingService接口以使库正常工作:

-keep public interface com.android.vending.licensing.ILicensingService

如果您使用的是Android兼容性库,则应添加以下行,以使ProGuard知道该库引用了并非所有版本的API都可用的某些类:

-dontwarn android.support.**

“Exceptions”属性必须保留,以使编译器知道哪些方法可能引发异常。

仅当动态调用了其他任何非公共类或方法时,才应使用附加的-keep选项来指定它们。

对于可以从库外部引用的任何内部类,也必须保留“ InnerClasses”属性。否则,javac编译器将无法找到内部类。

在JDK 5.0及更高版本中进行编译时,必须具有“Signature”属性才能访问泛型。 最后,我们保留“ Deprecated”属性和用于生成有用的堆栈跟踪的属性。

-keepattributes Exceptions,InnerClasses,Signature,Deprecated

此外,正规的第三方库一般都会在接入文档中写好所需混淆规则,使用第三方库时注意添加。 WebView中JavaScript调用的方法时,也需要保留。 Layout布局使用的View构造函数、android:onClick等,也需要保留。

#指定要执行的优化遍数
-optimizationpasses 5

#混淆时不生成大小写混合的类名,即全部小写
-dontusemixedcaseclassnames

#指定不忽略非公共的库的类
-dontskipnonpubliclibraryclasses

#指定不忽略包可见的库类成员(字段和方法)。
-dontskipnonpubliclibraryclassmembers

#把混淆类中的方法名也混淆了
#为需要混淆的类生成唯一的混淆名称
-useuniqueclassmembernames

#关闭预验证
-dontpreverify

# 打印过程日志,在处理期间输出更多信息
-verbose

#-dontshrink #禁用压缩

#指定优化算法
-optimizations !code/simplification/arithmetic,!field/*,!class/merging/*

#关闭优化
-dontoptimize

#扩大类和类成员的访问权限,使优化时允许访问并修改有修饰符的类和类的成员
-allowaccessmodification

#四大组件和Application的子类不被混淆
-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.app.backup.BackupAgentHelper
-keep public class * extends android.preference.Preference

#如果使用的是Google的可选许可证验证库,则可以将其代码与自己的代码混淆。 必须保留其ILicensingService接口以使库正常工作
-keep public interface com.android.vending.licensing.ILicensingService

#将混淆堆栈跟踪文件来源重命名为“SourceFile”
-renamesourcefileattribute SourceFile

#保护注解。如果代码依赖注释,则可能需要保留注释,典型应用EventBus的事件接收回调
-keepattributes *Annotation*

#保留源文件名,变量名和行号,以产生有用的混淆堆栈跟踪
-keepattributes SourceFile,LineNumberTable

#保留异常,内部类/接口,泛型,Deprecated不推荐的方法
-keepattributes Exceptions,InnerClasses,Signature,Deprecated,EnclosingMethod

#如果引用了v4或者v7包, 不报警告,使ProGuard知道该库引用了并非所有版本的API都可用的某些类
-dontwarn android.support.**

#保留native方法
-keepclasseswithmembernames class * {
   native <methods>;
}

#保留自定义View的类及构造函数,以使它们可以被XML布局文件引用
-keep 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);
}


#保留自定义View的get和set相关方法
-keepclassmembers public class * extends android.view.View {
  void set*(***);
  *** get*();
}

#保持Activity中View及其子类为入参的方法,比如android:onClick
-keepclassmembers class * extends android.app.Activity {
  public void *(android.view.View);
}


#保留符合指定构造函数类型的自定义控件类,如果和下面的写在一起,那么只有同时有这两类构造函数的类才满足
-keepclasseswithmembers class * {
   public <init>(android.content.Context, android.util.AttributeSet);
}

-keepclasseswithmembers class * {
   public <init>(android.content.Context, android.util.AttributeSet, int);
}


#保留R文件的静态成员,以使调用代码通过自省访问这些字段
-keepclassmembers class **.R$* {
   public static <fields>;
}

#保留枚举
-keepclassmembers enum * {
   public static **[] values();
   public static ** valueOf(java.lang.String);
}

#保留实现了Parcelable接口的类中的静态成员
-keep class * implements android.os.Parcelable {
 public static final android.os.Parcelable$Creator *;
}

#保持所有实现Serializable接口的类成员
-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();
}

#Fragment不需要在AndroidManifest.xml中注册,需要额外保护下
-keep public class * extends android.support.v4.app.Fragment
-keep public class * extends android.app.Fragment

#指定不混淆指定的包名称
-keeppackagenames com.milanac007.*

#指定包名下的文件都保留
-keep class com.milanac007..blecommsdk.**{*;}