ProGuard 初探,新手入门必知必会

1. Start up

下载并安装proguard,当前是5.3.3,解压并将bin所在目录设置到系统环境变量中,接着你就可以使用命令行工具开始ProGuard之旅了。
1.1 @myconfig.pro / -include myconfig.pro
指定使用myconfig.pro作为ProGuard文件

>proguard @mycofnig.pro
>proguard -include myconfig.pro

1.2 -injars
指定要进行proguard的类文件集合(jar包、aar包、war包、zip包、apk报或者目录都可以)

>proguard -injars in.jar
>proguard -injars in.zip

1.3 -outjars
指定proguard后要输出的类文件集合,可以包含的类型同上

>proguard -outjars out.jar
>proguard -outjars out.zip

1.4 -libraryjars
指定所使用的类依赖的文件集合,可以包含的类型同上 ,注意

>proguard -libraryjars lib.jar

1.5 target
指定要使用的jdk版本,可选项是:1.0、1.1、1.2、1.3、1.4、1.5(或5)、1.6(或6)、1.7(或7)

>proguard -target 8

1.6 总之就是这一套技能打下去就好了。

proguard -include myconfig.pro -injars in.jar -libraryjars rt.jar -outjars out.jar -verbose

2. Keep options(保留选项)

为了方便学习,本着实践出真知的想法,我们开始定义一些类。
// MainClass.java

public class MainClass{

    public static void main(String[] args){
        private User user = new User(23333,"Shaoxia",new Clothes("uniqlo","jeans","straw hat"));
        System.out.println(user.intro());
    }
}

//User.java

public class User{
    private String name;
    private int id;
    private Clothes clothes;
    
    public User(int id,String name, Clothes clothes){
        this.id = id;
        this.name = name;
        this.clothes = clothes;
    }
    
    public String intro(){
        return this.toString();
    }
    
    public void setName(String name){
        this.name = name;
    }
    
    public String getName(){
        return this.name;
    }
    
    public void setId(int id){
        this.id =id;
    }
    
    public int getId(){
        return this.id;
    }
    
    @Override
    public String toString(){
        return String.format("[name: %s,id: %d]",name,id);
    }
}

// Clothes.java

public class Clothes{
    private String shirt;
    private String trousers;
    private String hat;
    public Clothes(String shirt, String trousers, String hat){
        this.shirt = shirt;
        this.trousers = trousers;
        this.hat = hat;
    }
    
    public String getShirt(){
        return this.shirt;
    }
    
    public String getTrousers(){
        return this.trousers;
    }
    
    public String getHat(){
        return this.hat;
    }
}

2.1 -keep [,modifier,...] specification
指定不混淆指定的类,包括类名和成员名。
//myconfig.pro

-keep class MainClass{*;}
-keep class User{*;}

运行proguard后再使用jd-gui进行反编译,结果如下:
// MainClass

import java.io.PrintStream;

public class MainClass
{
  public static void main(String[] paramArrayOfString)
  {
    User localUser = new User(23333, "Shaoxia", new a("uniqlo", "jeans", "straw hat"));
    System.out.println(localUser.intro());
  }
}

// User

public class User
{
  private String name;
  private int id;
  private a clothes;
  
  public User(int paramInt, String paramString, a parama)
  {
    this.id = paramInt;
    this.name = paramString;
    this.clothes = parama;
  }
  
  public String intro()
  {
    return toString();
  }
  
  public void setName(String paramString)
  {
    this.name = paramString;
  }
  
  public String getName()
  {
    return this.name;
  }
  
  public void setId(int paramInt)
  {
    this.id = paramInt;
  }
  
  public int getId()
  {
    return this.id;
  }
  
  public String toString()
  {
    return String.format("[name: %s,id: %d]", new Object[] { this.name, Integer.valueOf(this.id) });
  }
}

// Clothes

public class a
{
  private String a;
  private String b;
  private String c;
  
  public a(String paramString1, String paramString2, String paramString3)
  {
    this.a = paramString1;
    this.b = paramString2;
    this.c = paramString3;
  }
}

2.2 -keepclassmembers [, mobifier,...] class spcification
指定保留指定类的的成员,包括成员变量和成员方法。
//myconfig.pro

-keepclassmembers class Clothes{*;}

于是乎,Clothes的所有成员就都被保留了。

public class a
{
  private String shirt;
  private String trousers;
  private String hat;
  
  public a(String paramString1, String paramString2, String paramString3)
  {
    this.shirt = paramString1;
    this.trousers = paramString2;
    this.hat = paramString3;
  }
  
  public String getShirt()
  {
    return this.shirt;
  }
  
  public String getTrousers()
  {
    return this.trousers;
  }
  
  public String getHat()
  {
    return this.hat;
  }
}

2.3 keepclasseswithmembers [, modifier,...] class specification
指定不混淆类和成员。这时Clothes就回到原有的模样,除了方法参数变了之外。

2.4 keepnames
保留类名,是 -keep, allowshrinking class specification的简写,意思就是如果该类没有在shrink阶段被移除的话,就不混淆其类名。

-keepnames class Clothes{*;}

混淆结果:

public class Clothes
{
  private String shirt;
  private String trousers;
  private String hat;
  
  public Clothes(String paramString1, String paramString2, String paramString3)
  {
    this.shirt = paramString1;
    this.trousers = paramString2;
    this.hat = paramString3;
  }
}

2.5 -keepclassmembernames class specification
指定保留类的成员名,是-keepclassmembers, allowshrinking class specification 的简写。

-keepclassmembernames class Clothes{*;}

混淆结果是:

public class a
{
  private String shirt;
  private String trousers;
  private String hat;
  
  public a(String paramString1, String paramString2, String paramString3)
  {
    this.shirt = paramString1;
    this.trousers = paramString2;
    this.hat = paramString3;
  }
}

2.6 -keepclasseswithmembernames class specification
-keepclasseswithmembers, allowshrinking class specification的简写,指定保留类名和类成员名,除非被shrink掉了。

-keepclasseswithmembernames class User{*;}

混淆结果是:

public class User
{
  private String name;
  private int id;
  private a clothes;
  
  public User(int paramInt, String paramString, a parama)
  {
    this.id = paramInt;
    this.name = paramString;
    this.clothes = parama;
  }
  
  public String intro()
  {
    return toString();
  }
  
  public String toString()
  {
    return String.format("[name: %s,id: %d]", new Object[] { this.name, Integer.valueOf(this.id) });
  }
}

2.7 -printseeds [filename]
打印出那些被keep住的类和成员,结果输出到指定文件里。

-keep class MainClass{*;}
-keepclasseswithmembernames class User{*;}
-printseeds whatkeeped.txt

输出结果是:

MainClass
MainClass: MainClass()
MainClass: void main(java.lang.String[])
User
User: java.lang.String name
User: int id
User: Clothes clothes
User: User(int,java.lang.String,Clothes)
User: java.lang.String intro()
User: void setName(java.lang.String)
User: java.lang.String getName()
User: void setId(int)
User: int getId()
User: java.lang.String toString()

3. Shrink Options(压缩选项)

3.1 -dontshrink
不压缩。压缩功能会把除了被keep住和直接或间接依赖的类和成员会保留外,其他的类或成员都会被移除。
3.2 -printusage [filename]
指定输出那些没用的代码(dead code)到文件中。

Clothes:
    12:12:public java.lang.String getShirt()
    16:16:public java.lang.String getTrousers()
    20:20:public java.lang.String getHat()
User:
    17:18:public void setName(java.lang.String)
    21:21:public java.lang.String getName()
    25:26:public void setId(int)
    29:29:public int getId()

3.3 -whyareyoukeeping class specification
指定打印一些日志告诉你为什么特定的类或者类成员在压缩阶段被保留了。

-whyareyoukeeping class User{ 
    public java.lang.String intro(...);
}

控制台输出:

User
  is invoked by    MainClass: void main(java.lang.String[]) (4:6)
  is kept by a directive in the configuration.

User: java.lang.String intro() (13:13)
  is invoked by    MainClass: void main(java.lang.String[]) (4:6)
  is kept by a directive in the configuration.

4. Optimization options(优化选项)

4.1 -dontoptimize
不优化。默认优化是开启的,所有的方法都会在字节码级别进行优化。
4.2 -optimizations optimization filter
这是一个高级选项,会进行细粒度的优化。
4.3 optimizationpasses n
指定进行优化的通道,默认是单通道。如果一个优化通道后没有提升,那么优化就结束了。
4.4 -assumenosideeffeccts class specification
假定优化对指定的类没有副作用。意思就是说开启该选项,ProGuard会移除对程序功能没有影响的代码,比如打印日志的代码,这个选项很危险,要慎重使用。

-assumenosideeffects class android.util.Log { 
    public static boolean isLoggable(java.lang.String, int); 
    public static int v(...); 
    public static int i(...); 
    public static int w(...); 
    public static int d(...); 
    public static int e(...); 
} 

4.5 -allowaccessmodification
允许修改限定符,类以及类成员的修饰符在优化步骤会被加宽。这个选项不适合那些类库,因为会导致本来为是no public的变成public,导致API暴露。
4.6 -mergeinterfacesaggressively
这个选项允许接口被合并,能减少类的数量,从而达到优化的目的。当然这并不总是很用,在JIT编译器中,反而会降低性能,因为JIT倾向于多接口少实现。

5. Obfuscation option(混淆选项)

5.1 -dontobfuscate
不混淆。默认启用混淆,类和类成员名会变成短小且随机的名字。可通过keep选项来保留一些。
5.2 -printmapping [filename]
打印混淆前后的名称对应关系到文件中,以便crash后进行对照。例中打印的结果是:

Clothes -> a:
    void <init>(java.lang.String,java.lang.String,java.lang.String) -> <init>
MainClass -> MainClass:
    void <init>() -> <init>
    void main(java.lang.String[]) -> main
User -> User:
    java.lang.String name -> name
    int id -> id
    Clothes clothes -> clothes
    void <init>(int,java.lang.String,Clothes) -> <init>
    java.lang.String intro() -> intro
    java.lang.String toString() -> toString

5.3 -applymapping filename
指定重用之前生成的mapping文件,这样原先混淆的类名和成员名就不会再变了,这个对于那些增量式打补丁式的混淆很有用。当然如果项目结构从根本上就变动了,那么用这种方式就很有风险了,这时候可以使用-useuniqueclasssmembernames来降低风险。
5.4 obfuscationdictionary filename
混淆字典。默认混淆使用的是a、b、c之类的作为类名和文件名。使用这个选项可以自己指定一套字典供混淆使用。不过空白符、 标点符号、重复的单词和#号后面的注释会被忽略。我试了下:

-obfuscationdictionary dict.txt

果然:

public final class b
{
  private String Fuck;
  private int Fck = 23333;
  
  public b(int paramInt, String paramString, a parama)
  {
    this.Fuck = paramString;
  }
  
  public final String toString()
  {
    return String.format("[name: %s,id: %d]", new Object[] { this.Fuck, Integer.valueOf(this.Fck) });
  }
}

5.5 -packageobfuscationdictionary filename
将指定文件里的所有合法的单词作为混淆的包名使用。
5.6 -overloadaggressively
强制进行方法重载,多个不用的成员变量或者方法会被混淆成同一名字,只要它们的参数和返回值不用即可。
5.7 -useuniqueclassmembernames
指定原本一样的成员名,混淆后还是一样;原本不一样的成员名,混淆后还是不同。这样对于后面的mapping和增量更新很有用。
5.8 -dontusemixedcaseclassnames
不要生成大小写混合的类名,这样就不至于在一些大小写不敏感的系统上软件包不可用。注意:默认情况下,混淆会生成大小写混合的类名的。
5.9 -keeppackagenames [package filter]
指定不要对过滤器中的包名进行混淆。包名过滤器是一个可以包含?、等通配符在内的使用逗号分隔的包名列表。
5.10 -flattenpackagehierarchy [package_name]
指定重打包,将所有的包移动到单个指定的父级包名。如果没有指定或者指定了空字符串,那么会被移动到根包。
5.11 -repackageclasses [package_name]
指定重打包所有被混淆的类文件,将所有的类移动到单个指定的包。该选项重写自-flattenpackagehierarchy
5.12 -keepattributes [attribute-filter]
指定保留列表中的属性,列表仍然是可以使用?、
*的用逗号分隔的属性。

-keepattributes Exceptions,InnerClasses,Signature,Deprecated,
                SourceFile,LineNumberTable,*Annotation*,EnclosingMethod

5.13 -keepparameternames
保留参数名。这对于类库来说很有用,因为可以展示有意义的参数名供调用者调用。
5.14 -renamesourcefileattribute [string]
指定一个字符串常量作为类文件的源文件属性的前缀。
5.15 -adaptclassstrings [class filter]
更改类字符串。开启该选项会将跟类名相符的字符串常量也一并混淆了。-adaptresourcefilenames [file filter]-adaptresourcefilecontents [file filter]见名知意。

6. Preverification options(预校验)

6.1 -dontpreverify
不要预校验。预校验在Java Micro Edition 或者Java 6以上可用。在Java 6和Android上是可选项,在Java Micro Edition 和Java 7以上是必选的。关闭预校验可以减少处理时间。
6.2 -microedition
指定处理的类文件是面向Java Micro Edition的。

7. 通用选项

7.1 -verbose
控制台输出详细的Proguard日志。
7.2 -dontnote [class filter]
不提示过滤器中的类名。
7.3 -dontwarn [class filter]
不要警告过滤器中的类,忽略警告继续proguard。这个对于忽略第三方类库中的类的警告很有用。
7.4 -ignorewarnings
控制台输出所有未解析的应用和其他重要的问题,但是忽略,继续执行proguard。
7.5 -printconfiguration [filename]
指定输出所有的配置到指定文件中。这些配置包括混淆文件和替换的变量。
7.6 -dump [filename]
写出处理后的类文件的结构。

-injars in.jar 

-dontshrink 
-dontoptimize 
-dontobfuscate 
-dontpreverify 

-dump 

参考

推荐阅读更多精彩内容

  • 混淆(Proguard)用法 最近项目中遇到一些混淆相关的问题,由于之前对proguard了解不多,所以每次都是面...
    于晓飞93阅读 42,535评论 41 215
  • 说明:本文参考(翻译)自Android SDK根目录下的proguard目录下的说明文档,是其中的一篇。,文中除了...
    一件小毛衣阅读 49,595评论 6 66
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 89,041评论 13 123
  • 什么是代码混淆 代码混淆就是将代码中的各种元素,如变量,方法,类和包的名字改写成无意义的名字,增加项目反编译后被读...
    蜗牛家族史阅读 1,799评论 1 3
  • 当前是有些工具比如apktool,dextojar等是可以对我们android安装包进行反编译,获得源码的。为了减...
    码农明明桑阅读 27,858评论 2 38