Android Proguard工具使用和配置详解

Android开发中的Proguard

Proguard是Android开发时经常会用到的一个工具,在Android SDK中已经集成了一个免费的Proguard版本,位于<sdk>/tools/proguard目录中。

在Android项目中,通过修改module下面的build.gradle文件来开启使用Proguard选项,当开启了此选项后,Android Studio在编译该module时就会使用指定的配置来对编译之后的Java字节码进行处理,得到一个优化后的jar包。

在最新的Android Studio 2.1.2版本创建的Android工程中,module中的build.gradle有如下一段配置。这里的minifyEnabled即用来控制在编译时是否需要启用Proguard,将minifyEnabled修改为true,即表示启用Proguard。proguardFiles配置的是Proguard对此module进行处理时使用的配置文件,’proguard-android.txt’是Android SDK中自带的一个基本Progurad配置文件,它同样位于<sdk>/tools/proguard目录中,’proguard-rules.pro’则是当前module所在目录中的一个配置文件,默认是空白的,需要由开发者自行实现,当启用了Proguard之后需要编辑这个文件,向其中添加适合当前项目的Proguard配置。本文后面会对Proguard的配置进行详细解析。

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

除了’proguard-android.txt’,Android SDK中还自带了另外一个Pruguard配置文件’proguard-android-optimize.txt’,它同样位于<sdk>/tools/proguard目录中,和’proguard-android.txt’ 的区别在于,’proguard-android-optimize.txt’中开启了Proguard optimize的选项(optimize是Proguard的一项功能),而’proguard-android.txt’ 中没有开启optimize选项。如果需要开启optimize,可以将这里的’proguard-android.txt’修改为’proguard-android-optimize.txt’。当然,如果熟悉Proguard的配置,也可以直接编辑module中的’proguard-rules.pro’,向其中添加optimize的配置。

这里有几点需要注意的地方:

  1. 在较早的Android Studio 版本中,这里的一些选项名字可能会有所不同,可能会看到runProguard这样的配置。这是因为Android Plugin for Gradle 在0.14.0版本中修改了DSL中的几个名字。虽然在实际项目中不会再使用这么老的版本了,但是网络上很多文章仍然是在老版本基础上写的。参考: http://tools.android.com/tech-docs/new-build-system

  2. Proguard虽然被集成到了Android SDK中,但是Proguard并非是Google开发的,他最早是个人开发者Eric Lafortune在业余时间做的一个开源项目,后来Eric Lafortune加入了GuardSquare(一家总部在比利时的公司)担任CTO,Proguard也被GuardSquare当做公司的产品来宣传,不过仍然是开源且免费的。 GuardSquare还有另外一款基于Proguard的产品Dexguard,这款产品是收费的,当然功能上也比Proguard要强大。从名字上就可以看出这款产品是专门针对Android APK优化的。

  3. 在Android Plugin for Gradle 2.0版本中集成了一个实验性质的工具,可以用来对代码进行shrinker,也就是可以去掉代码中没有用到的那些变量,方法和类。目前这个工具仍然是experimental,由于没有混淆和优化的功能,和Proguard相比没有任何优势。也许Google会继续开发,未来可能会像Android Studio取代Eclipse + ADT 一样,取代掉Proguard,当然也可能会废弃掉。
    要开启这个工具,可以参考 http://tools.android.com/tech-docs/new-build-system/built-in-shrinker

Proguard功能介绍

Proguard经常被看做Android平台的代码混淆工具,这种看法是比较片面的。Proguard项目诞生于2002年,而Android 1.0是2008年才发布的,也就是说早在Android发布之前Proguard就已经存在很久了。Proguard不仅适用于Android项目,也适合对其他使用Java开发项目的优化。此外,Proguard也不仅仅是一个代码混淆工具,代码混淆只是Proguard四项功能中的其中一项。它之所以被认为是Android平台的代码混淆工具,是因为Google将其集成到了Android SDK和Android项目的编译过程中,成为Android开发默认的代码优化工具,从而被广大的Android开发者所熟悉。此外,开发者对代码混淆功能的需求比其他功能要迫切的多,Proguard代码混淆功能成为开发者必选的一项功能。

Proguard帮助文档中是这样描述的

ProGuard is a Java class file shrinker, optimizer, obfuscator, and preverifier.

从这段话中可以看出,Proguard可以对Java class文件执行shrink,optimize,obfuscate和preverify四项优化。这四项优化也代表了Proguard的四项功能。

shrink

shrink功能的作用是移除代码中没有使用到的类,方法和成员变量,从而减少文件大小。

这些没有使用到的类,方法和成员变量的产生主要有两种情况,一种是自身代码中由于功能,需求的变更,或者代码的重构,导致原先的一些代码不再被使用,这些代码有时会忘记删除,有些则是故意保留下来,以备以后使用,这些被废弃或故意存留的代码对程序的运行没有任何用处。另一种是项目中经常会包含一些开源代码或第三方SDK,而项目通常只会使用到开源代码或第三方SDK中的部分功能,那些没有用到的功能对应的代码同样是可以去掉的。

所有这些没有用处的类,方法和成员变量,对应用的运行没有任何用处,但是它们都会被编译到jar文件中,不仅会增大jar文件的大小,而且在Android平台上可能还会导致方法数超过64k,引起一些不必要的问题。所以最好能够将这些没有用处的类,方法和成员变量都从jar文件中删除。但是,如果靠开发者手动删除是很费事费力的。一方面,如果第三方SDK不是开源的,或者项目没有将源码包含进来,是无法手动删除的。另一方面,手动从项目中删除代码是有副作用的,如果后面又需要用到这部分代码,又得费力加回来。因此,最好是可以有工具自动完成这项工作。

Proguard的shrink功能就是用来执行这项操作的,它会自动分析jar包中各个类,方法之类的调用和依赖关系,对那些没有用到的类,方法和成员变量进行剔除。

optimize

optimize过程会在Java字节码层面上进行优化,剔除方法中一些冗余的调用。帮助文档中列出了一些当前Proguard支持的优化。

对常量表达式进行计算
删除不必要的字段访问和方法调用
删除不必要的代码分支
删除不必要的比较和instanceof测试
删除未使用的代码块
合并相同的代码块
减少变量的分配
删除只赋值但没有使用的成员变量,以及未使用的方法参数
内联常量字段,方法参数和返回值
内联很短的方法和只被调用一次的方法
简化尾递归调用
合并类和接口
尽可能的将方法修饰为private,static和final
尽可能的将类修饰为static和final
移除只被一个类实现的接口
其他200多项细节优化,比如用 <<1 代替 *2 运算
删除Log的相关代码(可选)

obfuscator

obfuscator也就是Proguard最常被提到的代码混淆功能。由于Java代码编译之后的class文件中仍然包含了调试信息,源文件名,行号,类名,方法名,成员变量名,参数名,局部变量名等信息,通过反编译可以很容易的将这些信息还原出来。通过obfuscator,可以将jar包中不需要对外暴露的类名、方法名和变量名替换成一些简短的,对人来说没有意义的名字。

以下是一段代码混淆前和混淆后的对比。可以看到类名,成员变量名,方法名,参数名和局部变量名都被替换成了一些简单无意义的名字,从混淆后的代码中就很难理解原先代码的逻辑。然而这两段代码对计算机来说是完全等价的。此外,由于混淆后jar包中原先很长的名字被替换成了简短的名字,这使得jar包的体积更小了。这也是混淆带来的另一个附加的好处。

public enum Edge {
    private float mCoordinate;

    public static float getWidth() {
        return Edge.RIGHT.getCoordinate() - Edge.LEFT.getCoordinate();
    }

    public static float getHeight() {
        return Edge.BOTTOM.getCoordinate() - Edge.TOP.getCoordinate();
    }

    private static float adjustLeft(float x, Rect imageRect, float imageSnapRadius, float aspectRatio) {

        float resultX = x;

        if (x - imageRect.left < imageSnapRadius)
            resultX = imageRect.left;
        else {
            float resultXHoriz = Float.POSITIVE_INFINITY;
            float resultXVert = Float.POSITIVE_INFINITY;

            if (x >= Edge.RIGHT.getCoordinate() - MIN_CROP_LENGTH_PX)
                resultXHoriz = Edge.RIGHT.getCoordinate() - MIN_CROP_LENGTH_PX;

            if (((Edge.RIGHT.getCoordinate() - x) / aspectRatio) <= MIN_CROP_LENGTH_PX)
                resultXVert = Edge.RIGHT.getCoordinate() - (MIN_CROP_LENGTH_PX * aspectRatio);

            resultX = Math.min(resultX, Math.min(resultXHoriz, resultXVert));
        }
        return resultX;
    }
}
public enum a {
  private float e;

  public static float a() {
    return c.c() - a.c();
  }

  public static float b() {
    return d.c() - b.c();
  }

  private static float a(float paramFloat1, Rect paramRect, float paramFloat2, float paramFloat3) {
    float f1 = paramFloat1;
    if (paramFloat1 - paramRect.left < paramFloat2)
      f1 = paramRect.left;
    else {
      float f2 = Float.POSITIVE_INFINITY;
      float f3 = Float.POSITIVE_INFINITY;
      if (paramFloat1 >= c.c() - 40.0F)
        f2 = c.c() - 40.0F;

      if ((c.c() - paramFloat1) / paramFloat3 <= 40.0F)
        f3 = c.c() - 40.0F * paramFloat3;

      f1 = Math.min(f1, Math.min(f2, f3));
    }
    return f1;
  }

preverifier

preverifier用来对Java class进行预验证。预验证主要是针对Java ME开发来说的,Android中没有预验证过程,所以不需要用到这项功能。

Proguard工作过程

总体工作过程

在Proguard帮助文档中给出了一个Proguard工作流程图

这里写图片描述

可以看到,Proguard会对输入的jar文件按照shrink - optimize - obfuscate - perverify的顺序依次进行处理,最后得到输出jar文件。Proguard使用library jars来辅助对input jars类之间的依赖关系进行解析, library jars自身不会被处理,也不会被包含到output jars中。

Entry points(进入点)

设想我们手动对一个jar文件进行shrink处理,为了决定这个jar文件中哪些类和方法应该被保留,我们需要分析jar文件中方法之间的调用过程,得到一张方法之间的依赖关系图。然而这时仍然不知道哪些类和方法需要保留,不能因为一个类的某个方法被另外一个类的某个方法调用了,就认为这两个类和这两个方法就应该被保留,它们可能都没有被其他代码所调用。仔细分析这个过程,由于jar文件是一个孤立的个体,无论其内部有怎样复杂的调用和依赖关系,如果我们不指定一个搜索的起点的话,那么整个jar包中所有的类和方法都是可以被shrink的。这个搜索的起点就是jar文件对外提供的一个入口。这个入口可能是Java中的main方法,可能是Android中的四大组件,可能是一个SDK对外提供的APIs…总之,要执行shrink之前必须先指定一个或若干个入口。

在Proguard中,将jar文件的入口称为Entry points。在Proguard的四项功能中,只有preverifier不需要用到Entry points,其他三项功能都必须在配置中指定Entry points后才可以执行。

Proguard在执行shrink时会将Entry points作为起点,递归的搜索整个jar文件中类和方法之间的调用关系图,只有通过Entry points直接或间接调用的那些方法和类才会被保留,其他的类和方法都将被删除。

在optimize过程中,没有被指定为Entry points的类和方法可能会被修饰为private, static以及final,方法中没有用到的参数可能会被移除,有些方法还可能会被内联(整个方法被删除,方法的代码直接拷贝到调用处,替代原先的方法调用)。这些优化的目的是为了提高执行的效率,并附带的减少一些包体大小。但如果jar包一个类和方法需要被外部使用,则显然不能执行这类优化,否则外部将不能通过原先约定的方式来使用这些类和方法。所以,Proguard对指定Entry points的类和方法不会执行这些优化。

在obfuscator过程中,如果一个类和方法没有被指定为Entry points,则这个类和方法的名字将会被重命名为无意义的名字。同样,如果jar包一个类和方法需要被外部使用,则显然不能执行这类修改,否则外部将不能通过原先约定的名字来使用这些类和方法。所以,Proguard对指定Entry points的类和方法不会执行混淆操作。

Entry points和反射机制

在jar文件内部,可能会有一部分类和方法是通过Java反射方式来调用的,Proguard在分析jar包中类和方法之间的调用关系时,会考虑到反射方式的调用。如下反射方式调用的类和方法能够被Proguard正确的分析,其中”SomeClass”,”someField”,”someMethod”指的是某个编译时的字符串常量,SomeClass是某个明确的类型。

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")

例如,在某段代码中执行了Class.forName(“MyClass”),说明这里需要引用MyClass类,因此MyClass类不能被shrink(假设这段代码所在的方法本身不会被shrink),同时在执行obfuscate时,如果MyClass的类名可以被混淆(没有指定为Entry points),则在重命名MyClass类名的同时会修改Class.forName()参数中的字符串。

然而不幸的是,很多时候Proguard无法准确的判断出所有通过反射方式来调用的类和方法,这主要是那些在运行时才能确定的名字的反射调用。例如,在某段代码中执行了Class.forName(getPackageName()+ “.R”),Proguard在分析该段代码时,并不知道这里的getPackageName()+ “.R”指的是哪个类,所以也就无从知道是否要保持该类不被shrink和obfuscate。在这种情况下,虽然这个类并不会被jar包外部所使用,也需要将该类指定为Entry points。不然的话,如果这个类没有被代码中其他地方使用,那么它会在shrink时被删除,即使没有被删除,它也会在obfuscate时被重命名为其他的名字,这样当代码执行到Class.forName(getPackageName()+ “.R”)时还是无法找到这个类。

所以,需要将代码中那些通过反射方式调用,又不能自动推断出调用关系的类和方法手动添加到Entry points的配置中。

Proguard使用

Proguard工具目录结构

Proguard工具目录结构如图所示。

这里写图片描述

  • lib目录
    lib目录中包含了Proguard工具对应的jar文件,其中又包含三个文件:proguard.jar,proguardgui.jar和retrace.jar。
    Proguard四项核心功能shrink,optimize,obfuscate和preverify的执行都是由proguard.jar来完成的,不过proguard.jar只能通过命令行方式来使用。
    proguardgui.jar是Proguard提供的一个图形界面工具,通过proguardgui.jar可以方便的查看和编辑Proguard配置,以及调用proguard.jar来执行一次优化过程。
    retrace.jar主要在debug时使用。混淆之后的jar文件执行过程如果出现异常,生成的异常信息将很难被解读,方法调用的堆栈都是一些混淆之后的名字,通过retrace.jar可以将异常的堆栈信息中的方法名还原成混淆前的名字,方便程序解决bug。

  • bin目录
    bin目录中包含了几个bat和shell脚本,通过这些脚本可以直接执行proguard.jar,proguardgui.jar和retrace.jar。如果将bin目录添加到环境变量中,就可以直接在命令行中执行proguard,proguardgui和retrace命令了,避免每次都要输入java -jar + <proguard路径>/lib/<jar文件名> + <参数>这一长串信息来调用proguard 。不过proguard.bat,proguard.sh,retrace.bat和retrace.sh在执行时都需要在后面加上参数,所以它们还是只能通过命令行的方式来执行。proguardgui.bat和proguardgui.sh则可以不用参数,可以直接双击打开proguardgui.jar的图形界面。

  • src目录
    Proguard是开源的,src目录中是Proguard源码,都是Java代码。

  • buildscripts目录
    buildscripts目录中是Proguard的编译脚本,提供了shell,makefile,Ant和Maven四种方式来编译Proguard源码。

  • docs目录
    docs目录是Proguard提供的帮助文档,通过index.html来查看。

  • examples目录
    examples目录提供了一些针对不同平台的Proguard配置文件示例,例如android.pro中包含了对Android平台Proguard配置的说明。

proguard.jar的使用

使用proguard.jar有几种方式。

  1. 通过命令行执行”java -jar + <proguard路径>/lib/proguard.jar + <参数>”
  2. 通过命令行执行”<proguard路径>/bin/proguard.bat(Windows) + <参数>” 或者 “<proguard路径>/bin/proguard.sh+ <参数>”(Linux)。如果已经将”<proguard路径>/bin”路径添加到环境变量中,则可以直接执行proguard.bat(Windows) + <参数>” 或者 “proguard.sh+ <参数>”(Linux)

这里的<参数>可以是Proguard支持的任意配置选项,Proguard的配置选项均以-开头。不过由于Proguard一次执行过程中通常都需要很多配置选项,所以一般都会将所有需要的配置选项保存为配置文件的形式,以便重复使用和修改,proguard.jar也支持用配置文件作为参数,不同的是配置文件以@开头。此外,proguard.jar还可以支持配置文件和配置选项的混合形式。如下的写法是正确的。

java -jar proguard.jar -injars myapp. jar -outjars myapp_out.jar -libraryjars 'D:\android-sdk\platforms\android-23\android.jar' // 只使用配置选项
java -jar proguard.jar @myconfig.pro                // 只使用配置文件
java -jar proguard.jar @myconfig.pro -verbose       // 混合使用配置文件和配置选项

proguardgui.jar的使用

使用proguardgui.jar有几种方式。

  1. 通过命令行执行”java -jar + <proguard路径>/lib/proguardgui.jar + <参数>”
  2. 通过命令行执行”<proguard路径>/bin/proguardgui.bat(Windows) + <参数>” 或者 “<proguard路径>/bin/proguardgui.sh+ <参数>”(Linux)。如果已经将”<proguard路径>/bin”路径添加到环境变量中,则可以直接执行proguardgui.bat(Windows) + <参数>” 或者 “proguardgui.sh+ <参数>”(Linux)

这里的<参数>不是必须的,如果没有参数,只只打开图形界面。如果包含了参数,参数必须是Proguard配置文件,proguardgui.jar不支持用配置选项作为参数。对作为参数的配置文件,直接用文件名即可,不需要在文件名前加@等符号。如下的写法是正确的。

java -jar proguardgui.jar                   // 不使用配置文件
java -jar proguardgui.jar @myconfig.pro     // 使用配置文件

retrace.jar的使用

使用retrace.jar有几种方式。

  1. 通过命令行执行”java -jar + <proguard路径>/lib/retrace.jar + <参数>”
  2. 通过命令行执行”<proguard路径>/bin/retrace.bat(Windows) + <参数>” 或者 “<proguard路径>/bin/retrace.sh+ <参数>”(Linux)。如果已经将”<proguard路径>/bin”路径添加到环境变量中,则可以直接执行retrace.bat(Windows) + <参数>” 或者 “retrace.sh+ <参数>”(Linux)

这里的<参数>包含三个部分:retrace选项,mapping文件和包含异常堆栈信息的文件,其中mapping文件是必须的,其他两个部分是可选的。mapping文件是在执行proguard.jar时通过-printmapping选项生成的文本文件,此文件中包含了jar文件混淆前的类,方法和成员变量名,以及它们混淆后对应的名字。包含异常堆栈信息的文件是Java中Exception对象的printStackTrace()方法打印出来的堆栈信息保存的文本文件,如果没有在参数中指定这个文件,则会从标准输入中读取。retrace支持-verbose和-regex两个配置选项。-verbose表示输出结果中不仅包括堆栈信息中的方法名,还包括返回值和参数。-regex用来指定一个正则表达式,指定的正则表达式会被用来匹配堆栈信息中的类名,方法名等,一般来说不需要指定这个选项,除非堆栈信息比较特殊,retrace默认的规则无法解析。如下的写法是正确的。

java -jar retrace.jar mapping_file
java -jar retrace.jar mapping_file exception_statck_file.txt
java -jar retrace.jar -verbose mapping_file exception_statck_file.txt

Proguard配置选项

这里的Proguard配置选项指的是proguard.jar所支持的配置选项。

配置文件规则

由于大多数时候都是将Proguard配置选项写到配置文件中来使用,所以在介绍Proguard配置选项前,先介绍Proguard配置文件的一些规则。

  1. 配置文件中不能包含配置选项和注释之外的其他字符。
  2. 配置文件中可以添加注释,注释以#开头,从#到该行末尾的文字都会看做注释。不支持多行注释。
  3. 在配置文件中空格和换行有相同的效果,能用空格的地方都可以用换行,反之亦然。不过一般来说,空格用来分割配置选项和对应的值,换行用来分割不同的配置选项。此外,多余的空格和换行会被忽略。
  4. 每条配置规则均以-开头,-和后面的选项名之间不能有空格,例如’-verbose’写成’- verbose’是不能被识别的。
  5. 有些配置规则中需要指定文件或文件夹的路径,如果路径中包含空格,则需要将路径用单引号或双引号括起来。如果没有空格,可以不需要引号。
  6. 配置规则中除-injars,-outjars之间有先后顺序上的要求外,其他的不同配置选项在配置文件中可以按照任意的顺序来配置。

-injars,-outjars和-libraryjars

配置选项-injars表示输入的文件,也就是需要被Proguard处理的文件。-outjars表示输出文件,也就是处理完毕后的文件,-libraryjars表示-injars文件运行时所依赖的jar文件,例如Java运行时rt.java,Android运行时android.jar等。这几个选项的配置规则如下。

  1. -injars,-outjars和-libraryjars不仅支持jar文件,还支持 aar, war, ear, zip和apk格式的文件,还可以指定一个包含这些类型文件的文件夹。Proguard会读取-injars中指定的文件(为了简化描述,这里所说的文件包含了文件夹的情况,对文件夹就是遍历文件夹中所有支持的上述类型的文件,下同)中的Java class文件,对其进行处理,得到目标class文件,最后将其打包到-outjars指定的文件中。
  2. -injars文件和-libraryjars文件可以有多个,有几个文件,就需要写几条配置,不能在一条配置中写多个文件。以下是正确的写法。

    -injars 'libs\in1.jar'
    -injars 'libs\in2.jar'
    -libraryjars 'android.jar'
    -libraryjars 'android-support-v4.jar'

    -injars文件和-libraryjars文件有多个时,配置的顺序没有影响。例如上面的写法和下面这种写法是等价的。

    -injars 'libs\in2.jar'
    -injars 'libs\in1.jar'
    -libraryjars 'android-support-v4.jar'
    -libraryjars 'android.jar'

    -injars文件有多个时,要保证多个injars中不能包含相同名字的类,例如,上述in1.jar和in2.jar中都包含com.ccpat.test类,则无法处理。-libraryjars没有这个限制。

  3. -injars,-outjars和-libraryjars都支持filter,filter是一个用来匹配类名的可以包含通配符的字符串。对-injars和-libraryjars来说,filter用来匹配文件中需要处理的class的范围,不在范围内的class不会被解析和处理,也不会被包含到-outjars文件中,对-outjars来说,filter用来指定需要保存到此文件中的class的范围。如果需要在-injars,-outjars和-libraryjars配置中增加filter,可以在文件名后加上(),然后在()中写上符合filter语法的匹配字符串。例如

    -injars 'libs\in1.jar(com/ccpat/**.class)'   # 只处理in1.jar中com.ccpat包名下的类
    -injars 'libs\in2.jar(!rx/**.class)'         # 不处理in2.jar中rx包名下的类
    -outjars libs\out1.jar(com/**.class)         # 将对in1.jar和in2.jar处理完成后的结果中com包名下的类保存到out1.jar中
  4. -outjars也可以有多个,不过需要使用filter,没有filter的-outjars后面不能直接再跟其他的-outjars。例如。

    -injars 'libs\in1.jar(com/ccpat/**.class)'
    -injars 'libs\in2.jar(!rx/**.class)'
    -outjars libs\out1.jar(com/**.class)           # 将处理完成后的结果中com包名下的类保存到out1.jar中
    -outjars libs\out2.jar(org/**.class)           # 将处理完成后的结果中org包名下的类保存到out2.jar中
    -outjars libs\out3.jar                         # 将处理完成后的结果中除com和org之外的包名下的类保存到out3.jar中
    -outjars libs\out4.jar(de/**.class)            # 错误的用法,在上一条-outjars中没有包含filter,所以不能在后面直接加-outjars配置。
                                                   # 因为这时已经没有其他的类可以保存到这个文件中了。
  5. -injars和-outjars是有顺序要求的,-injars配置应该在-outjars配置之前。对-libraryjars则没有顺序要求,它可以配置在-injars和-outjars之前,也可以配置在后面。甚至可以配置在两条-injars或-outjars中间。以下是正确的写法。

    -injars 'libs\in1.jar'
    -libraryjars 'android.jar'
    -injars 'libs\in2.jar'
    -outjars libs\out1.jar(com/**.class)
    -libraryjars 'android-support-v4.jar'
    -outjars libs\out2.jar

    以下是错误的写法。

    -outjars libs\out1.jar
    -injars 'libs\in1.jar'
    -injars 'libs\in2.jar'
  6. -injars,-outjars和-libraryjars有多个时,不能出现相同的文件,如下配置均是错误的。

    -injars libs\in.jar
    -injars libs\in.jar
    -outjars libs\out.jar
    -outjars libs\out.jar
    -libraryjars 'android.jar'
    -libraryjars 'android.jar'

    -injars,-outjars和-libraryjars之间配置的文件也不能是同一个文件,如下配置均是错误的。

    -injars libs\in.jar
    -outjars libs\in.jar
    -injars 'android-support-v4.jar'
    -libraryjars 'android-support-v4.jar'
    -outjars libs\out.jar
    -libraryjars libs\out.jar
  7. 可以同时配置多组-injars和-outjars,例如,如下配置会将in1.jar和in2.jar处理结果保存到out1.jar中,in3.jar的处理结果保存到out2.jar中。

    -injars libs\in1.jar
    -injars libs\in2.jar
    -outjars libs\out1.jar
    -injars libs\in3.jar
    -outjars libs\out2.jar

    当配置多组-injars时,-libraryjars文件是可以被这多组-injars共用的,不存在配置多组-libraryjars文件的概念。

    当配置多组-injars时,每一组的-injars和-outjars需要遵循规则5,也就是每一组的-injars都需要配置在-outjars前面。

    当配置多组-injars和-outjars,在不同的组中的jar文件可以有相同的类,这点和同一组中是不一样的,例如,上述in1.jar和in3.jar中都包含com.ccpat.test类,是可以正常处理的。

    当配置多组-injars和-outjars,即使在不同的组,同一个文件也不能出现两次。如下配置是错误的。

    -injars libs\in1.jar
    -injars libs\in2.jar
    -outjars libs\out1.jar
    -injars libs\in1.jar
    -outjars libs\out2.jar

    结合第6条规则,可以得出这样一条规则:在一个配置文件中-injars,-outjars和-libraryjars文件都不能相同。

  8. 配置的-injars在被Proguard处理时可以当做-libraryjars来使用。例如有如下配置。

    -injars libs\in1.jar
    -injars libs\in2.jar
    -outjars libs\out1.jar

    假如in1.jar和in2.jar并非是完全独立的,它们之间有相互依赖关系,in1.jar中的代码会调用in2.jar中的代码,in2.jar中的代码也会调用in1.jar中的代码,这种情况是可以被Proguard正确处理的,不需要再将in1.jar和in2.jar声明为-libraryjars(事实上也无法将这两个文件声明为-libraryjars)

    这点对有多组-injars和-outjars配置的情况也是适用的。例如有如下配置。

    -injars libs\in1.jar
    -outjars libs\out1.jar
    -injars libs\in2.jar
    -outjars libs\out2.jar

    假如这里的in1.jar和in2.jar也同样存在相互依赖关系,in1.jar中的代码会调用in2.jar中的代码,in2.jar中的代码也会调用in1.jar中的代码,这种情况也是可以被Proguard正确处理的。

类的模板class specification

由于在之后的一系列 -keep 声明中需要使用到类的模板,这里先讲述下Proguard中类的模板的表示方法。

当需要在Proguard配置中表示一组类及其成员(包括成员变量和成员方法)的时候就需要用到类的模板,Proguard中类的模板看起来和Java中去掉一个类的方法实现部分后剩余部分很像(类似于接口的定义),其中类名,成员名等可以用一些特定的通配符来表示,以匹配多个不同的类和成员。

它的完整语法如下(来源于官方文档)。”[]”中的内容表示可选的, “…”表示可以附加任意多个该字段,”|”表示可以从一组选项中任意选择一个。可以看到整个模板由一组可选或必选的字段组合而成,其中只有interface|class|enum和classname 是必选的字段,其他字段都是可选的。

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

对这里的各个字段分别解析如下。

以下是模板中类名的表示

  1. @annotationtype
    这是一个可选的字段,用来修饰类,表示该类附加的annotation。例如。

    @Deprecated public class *     // 可以匹配所有被@Deprecated修饰的类
    
  2. public|final|abstract|@
    这是一个可选的字段,用来修饰类,public|final|abstract表示该类的修饰符,@表示需要annotation,可以任意组合使用,前面可以加上!表示不匹配该修饰符。需要注意的是这里没有private和protected修饰符。例如

    // 匹配所有是public,但不是final,不是abstract的类。
    public !final !abstract class *
    
    // 匹配所有是public,且有被注解修饰的类。
    public @class *
  3. interface|class|enum
    这是一个必选字段,interface表示这是一个接口类型,class既可以匹配接口,也可以匹配类,enum表示这是一个枚举类型,前面可以加上!表示不匹配。此字段只能从这三个中任意选取一个,不能组合使用。例如。

    // 匹配所有class和interface
    public class *
    
    // 匹配所有interface
    public interface*
    
    // 匹配不是interface的其他类型,也就是匹配classenum
    public !interface*
    
    // 匹配不是enum的其他类型,等同于public class *
    public !enum *
  4. classname
    classname是一个必选的字段,表示类的名字,这里必须使用完整的名字,也就是要带上包名,例如要表示String类需要用java.lang.String,而不是直接用String,如果要表示内部类,可以用$来分割外部类和内部类的名字,例如 android.view.View$OnClickListener。
    classname中还可以包含如下的通配符。

    ? : 用来匹配除包分隔符.之外的其他任意单个字符
    * : 用来匹配任意多个字符(包括0个),匹配不能跨越包分隔符.。
    ** : 用来匹配任意多个字符(包括0个),和*不同的是,**匹配可以跨越包分隔符。

    *使用时有一个特例,单独的*可以表示任意类,也就是可以跨包分隔符,和单独的** 等价。
    以下是一些例子

    模板 可以匹配示例 不能匹配示例
    my.test? my.test1; my.testx my.test ; my.test12 ; my.test.1
    my.*test* my.test ; my.thistest ; my.testapp ; my.thistestapp my.sub.test ; my.test.app
    my.* my.abc ; my.abc123 my.sub.abc
    * abc ; myabc, my.abc
    **.test my.test ; your.sub.test my.testabc
    my.** my.app ; my.sub.app your.app
    ** my.app ; your.sub.test
  5. extends|implements
    这是一个可选的字段,表示类的继承或对接口实现的约束。例如

    // 匹配所有继承自android.view.View,且是public的类。
    public class * extends android.view.View
    
    // 匹配所有实现了android.view.View$OnClickListener接口,且是public的类。
    public class * implements android.view.View$OnClickListener

    以下是模板中成员变量的表示

  6. @annotationtype
    用来修饰成员变量,表示该成员变量附加的annotation。例如。

    public class * {
        @Deprecated <fields>;       // 可以匹配一个类中所有被@Deprecated修饰的成员变量
    }
    
  7. public|private|protected|static|final|volatile|transient
    用来修饰成员变量,它们都是成员变量的修饰符,前面可以加上!表示不匹配该修饰符。例如,

    class * {
        public int mCount;       // 匹配一个类中被public 修饰的int类型名字为mCount的变量
    }
    
  8. <fields>
    <fields>表示任意类型任意名字的所有成员变量。例如

    class * {
        public static <fields>;       // 匹配一个类中被public static修饰的所有成员变量
    }
    
  9. fieldtype
    fieldtype表示成员变量的数据类型,它需要和fieldname组合一起使用。fieldtype中可以包含如下的通配符。

    % : 用来匹配任意Java基本数据类型(包括byte,short,int,long,float,double,char和boolean,不包括void)
    ? : 用来匹配成员变量类型中除包分隔符.之外的任意单个字符
    * : 用来匹配成员变量类型中除包分隔符.之外的任意多个字符(包括0个)
    ** : 用来匹配成员变量类型中任意多个字符(包括0个),包括包分隔符.
    ***: 用来匹配任意类型,包括基本数据类型和数组

    需要注意的是,?,*和**不会匹配Java的基本数据类型,如果要匹配基本数据类型,需要使用%或***。
    以下是一些示例

    模板 可以匹配示例 不能匹配示例
    % byte,short,int,long,float,double,char,boolean java.lang.String,void
    my.test? my.test1; my.testx my.test ; my.test12 ; my.test.1
    my.in? my.ins, my.ing my.int
    my.*test* my.test ; my.thistest ; my.testapp ; my.thistestapp my.sub.test ; my.test.app
    my.* my.abc ; my.abc123 my.sub.abc
    * abc ; myabc my.abc, int, boolean
    **.test my.test ; your.sub.test -
    my.** my.app ; my.sub.app -
    ** my.app ; your.sub.test int, boolean, my.app []
    *** my.app ; your.sub.test, int, boolean, java.lang.String[] -
  10. fieldname
    fieldname表示成员变量的名字。fieldname中可以包含如下的通配符。

    ? : 用来匹配成员变量名中任意单个字符
    * : 用来匹配成员变量名中任意多个字符(包括0个)

    以下是模板中成员方法的表示

  11. @annotationtype
    用来修饰成员方法,表示该成员方法附加的annotation。例如。

    public class * {
        @Deprecated <methods>;       // 匹配一个类中所有被@Deprecated修饰的成员方法
    }
    
  12. public|private|protected|static|final|synchronized|native|abstract|strictfp
    用来修饰成员方法,它们都是成员方法的修饰符,前面可以加上!表示不匹配该修饰符。部分修饰符可以组合使用。例如,

    class * {
        public static final int get*();     // 匹配一个类中被public static final修饰的,返回值为int类型,名字前面为get,且没有参数的成员方法
    }
    
  13. <methods>
    <methods>表示返回值为任意类型,任意名字,任意参数的所有成员方法。例如

    class * {
        public static <methods>;       // 匹配一个类中被public static修饰的所有成员方法
    }
    
  14. <init>
    <init>(argumenttype,…)表示构造方法。和<methods>不同的是,这里必须包含参数描述,也就是和argumenttype一起使用,例如

    class * {
        public <init>();       // 匹配一个类中public且没有参数的构造方法
    }
    class * {
        public <init>(int, java.lang.string);      // 匹配一个类中public且包含两个参数,第一个为int,第二个为String的构造方法
    }
  15. classname
    classname同样表示构造方法,这里的classname必须和第4项的classname完全相同。和<init>是等价的。例如

    public class abc {
        public abc (int);       // 匹配abc类中参数为int的构造方法
    }
  16. returntype
    returntype表示成员方法返回值的类型。它需要和methodname组合一起使用。returntype中通配符的用法和fieldtype完全相同。

  17. methodname
    methodname表示方法的名字。它需要和argumenttype一起使用。methodname中通配符的用法和fieldname完全相同。

  18. argumenttype
    argumenttype表示成员方法的参数列表,多个参数之间用逗号分割。argumenttype中通配符的用法和fieldtype基本相同,fieldtype可以使用的通配符在argumenttype中都可以使用,且有相同的含义,不同的是argumenttype中有一个…的通配符,它的含义是表示任意多个任意类型的参数。例如

    public class abc {
        public <init>(*);     // 匹配abc类中有一个参数,参数类型为除基本数据类型之外,且没有子包的任意名字的构造方法
    }
    public class abc {
        public <init>(**);    // 匹配abc类中有一个参数,参数类型为除基本数据类型之外任意类型的构造方法
    }
    public class abc {
        public <init>(***);   // 匹配abc类中有一个参数,参数类型为任意类型的构造方法
    }
    public class abc {
        public <init>(...);   // 匹配abc类任意的构造方法
    }
  19. *
    单独的*表示任意的成员变量和成员方法。例如

    public class abc {
        public *;     // 匹配abc类任意的public成员变量和成员方法
    }

注意:如果一个模板中没有包含某个字段,并不表示它只能匹配那些不包含该修饰字段的类和成员,而是表示不关心该字段,该字段可以不存在,也可以是任意的。例如,模板public class ; 中没有包含@annotationtype字段,它表示不关心类是否使用了注解,因此它既可以匹配那些没有使用注解的类,也可以匹配使用了任意注解的类;再比如模板class ;中没有包含public|final|abstract字段,这个模板可以匹配使用了任意注解,任意访问修饰符的类,当然也包括那些没有注解,没有访问修饰符的类。

keep修饰符(modifier)

指定为keep的类和类的成员默认既不会被shrink,也不会被混淆,也不会被优化。可以指定修饰符modifier来改变这一默认的过程。keep修饰符有四个,分别描述如下。

  • allowshrinking
    加上此修饰符后,可以让 -keep选项指定的类和类的成员可以被 shrink。也就是说,此entry points只对混淆和优化阶段有用(不会被混淆和优化),在shrink阶段不被考虑(可以被shrink)。注意,由于shrink是最先执行的,如果 -keep选项指定的类和类的成员被shrink了,那么显然它也无法再被混淆和优化了。

  • allowoptimization
    加上此修饰符后,可以让 -keep选项指定的类和类的成员可以被 优化。也就是说,此entry points只对shrink和混淆阶段有用(不会被shrink和混淆),在优化阶段不被考虑(可以被优化)。

  • allowobfuscation
    加上此修饰符后,可以让 -keep选项指定的类和类的成员可以被 混淆。也就是说,此entry points只对shrink和优化阶段有用(不会被shrink和优化),在混淆阶段不被考虑(可以被混淆)。

  • includedescriptorclasses
    加上此修饰符后,可以让 -keep选项指定的类的成员变量的类型,成员方法的参数类型,成员方法的返回值类型都加入到entry points中。此选项主要用来保护native方法。

keep选项

在Proguard中通过一系列的keep选项用来指定Entry points。

  • -keep [,modifier,…] class_specification
    指定需要保留的类和类的成员(包括成员变量和成员方法),modifier和class_specification的含义见上文。
  • -keepclassmembers [,modifier,…] class_specification
    指定需要保留的类的成员(包括成员变量和成员方法),modifier和class_specification的含义见上文。类本身是可以被shrink,混淆和优化处理的。
  • -keepclasseswithmembers [,modifier,…] class_specification
    指定需要保留的类和类的成员(包括成员变量和成员方法),modifier和class_specification的含义见上文。和-keep不同的是,需要指定的类的成员都存在才会被保留。
  • -keepnames class_specification
    指定需要不被混淆和优化的类和类的成员(包括成员变量和成员方法),指定的类和类的成员可以被shrink。等同于-keep,allowshrinking class_specification。
    -keepclassmembernames class_specification
    指定需要不被混淆和优化的类的成员(包括成员变量和成员方法),指定的类的成员可以被shrink。等同于-keepclassmembers,allowshrinking class_specification
    -keepclasseswithmembernames class_specification
    指定需要不被混淆和优化的类的成员(包括成员变量和成员方法),需要指定的类的成员都存在才会被处理。指定的类和类的成员可以被shrink。等同于-keepclasseswithmembers,allowshrinking class_specification
    -printseeds [filename]
    输出jar文件中和各类-keep选项匹配的类和成员,如果不指定filename,则输出到标准输出,指定filename后则输出到指定的文件。

—————未完待续—————-

阅读更多
版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/ccpat/article/details/52059344
个人分类: java Android
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页

关闭
关闭
关闭