iiiiiiProGuard Awesome Sources

ProGuard系列 - ProGuard基础

2017-02-07  本文已影响245人  糊涂_糖

作者:Tong,欢迎交流。
邮箱:wangtotang@gmail.com
GitHub:https://github.com/wangtotang

导读:
  我在学习Android的时候,知道ProGuard是用来混淆和压缩代码的,但使用的时候都只是按网上的第三方库提供的ProGuard配置进行复制粘贴,并没有理解里面的原理,在这个系列文章里,将总结我从头学习ProGuard的知识点,让我们来一次有趣的ProGuard之旅吧!

这是这个系列的第一篇文章,主要来了解ProGuard基础知识,包括功能和基础语法。

1 ProGuard介绍

ProGuard是用于Java文件的压缩、优化、混淆和预校验的免费工具,利用它可以检查和移除无用代码减少程序包体积,优化和混淆并对代码进行预校验。
  ProGuard提供模板化配置,还有简单的命令行选项以及快速的处理过程的优点,广泛用于各种开发场景,特别是Android应用。

ProGuard的用处:
  ● 创建更紧凑的代码和更小的程序包,便于网络传输、减少内存占用以及更快的加载;
  ● 使得程序更难被反编译;      
  ● 检查出无用代码,并移除;      
  ● 重定向和预校验Java类文件。

1.1 ProGuard功能

完整的ProGuard由压缩、优化、混淆和预校验四个步骤组成,每个步骤都默认开启,我们可以在配置文件中决定是否取消这些步骤。

ProGuard按照上图的顺序依次执行,首先检查发现没有使用的类、方法、字段和特性并对其进行移除,减少程序包的体积大小;然后是对字节码进行优化,移除无用的指令(因为Android打包会进行优化,默认不启);再然后是混淆,通过使用简短的名称,如a、b、c,来命名类、方法和变量名,增加程序被反编译的难度(这里需要注意利用反射等动态加载的代码,以免混淆后报错);最后是代码预校验,确保加载的class文件是可执行的(同样因为Android自带代码校验,默认不开启),最后输出新程序包。

由于Java先被编译成.class字节码文件,然后在打成程序包,如war、jar和apk等,都很容易解压得到字节码文件,而字节码文件中包含几乎所有的程序信息,可以很容易被反编译出来得到完整的程序文件,被一些别有居心的人利用,不但无法保证版权,而且还会造成很大的损失,为了更好的保护应用程序运行,我们需要对编译好的class文件进行混淆。

上面四个步骤都是可选的,可以通过以下选项进行关闭功能:
 
  #不进行压缩
  -dontshrink
  #不进行优化
  -dontoptimize
  #不进行混淆
  -dontobfuscate
  #不进行预校验
  -dontpreverify

1.2 工作原理

在上面讲到,ProGuard检查出没有使用的代码,然后移除,它是如何发现没有使用的代码呢?这里就要引入一个入口点(Entry Point)的概念。入口点是指被保持的代码,来决定哪些代码不被移除或者混淆。

那入口点怎么来的呢?其实我们可以通过在ProGuard配置文件中使用Keep选项进行配置它。在接下来一节,我们就开始学习如何使用ProGuard。

1.3 小结

ProGuard处理代码有四个步骤,它们都是可选的,可以通过指令选项关闭;ProGuard通过入口点来递归处理代码,这入口点就是我们
配置的需要保持的代码。学习如何配置ProGuard,这是使用ProGuard的最主要的部分。

这一部分内容可以查看ProGuard介绍

2 ProGuard基础语法

ProGuard的语法很简单:

2.1 Keep选项

想要使用ProGuard,就要熟悉ProGuard指令选项。Android中我们常用的指令选项一般是Keep选项,这里着重了解一下Keep选项的用法。

# ------------------- 不被移除且不被混淆 ----------------------
#保持类及其成员
-keep [,modifier,...] class_specification

 #保持类成员
-keepclassmembers [,modifier,...] class_specification

#保持含有指定成员及其类
-keepclasseswithmembers [,modifier,...] class_specification

# ---------------------- 不被混淆 ----------------------------
#保持类及其成员的名称
#等同于 -keep,allowshrinking class_specification
-keepnames class_specification

#保持类成员的名称
#等同于 -keepclassmembernames,allowshrinking class_specification
-keepclassmembernames class_specification

#保持含有指定成员及其类的名称
#等同于 -keepclasseswithmembers,allowshrinking class_specification
-keepclasseswithmembernames class_specification

现在我们尝试使用一下Keep选项,

# 保持指定的类名,需要全路径类名
-keep public class mypackage.MyActivity
# 保持任意enum类中的static修饰values(),valueOf()方法
# 参数类型也需要全路径类名
-keepclassmembers enum * {
    public static **[] values();
    public static ** valueOf(java.lang.String);
}
# 保持class$()方法名称,class$()方法是Java编译器实现构造的,无需理会
-keepclassmembernames class * {
    java.lang.Class class$(java.lang.String);
    java.lang.Class class$(java.lang.String, boolean);
}
# 保持含有native修饰的方法的类名称和native修饰的方法名称
-keepclasseswithmembernames class * {
    native <methods>;
}

在我们单独使用*代表类名时,表示任意类名,否则,不单独使用仅匹配多个字符,例如Test*,表示Test1或TestWorld等,且不含有包分隔符,例如不能表示Test.World;

我们在使用的时候,如何具体地写保持的内容呢?下面是完整的模板定义:

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

Keep选项给我们一种很混乱的感觉,有时候可能不确定选择使用哪一个,其实我们大可不必纠结,直接使用-keep就好了,它能确保指定的类及其成员不被移除且不被混淆。例如:

# 保持model包及其子包下的所有类和所有成员
-keep class com.google.gson.examples.android.model.** { *; }

对于Keep选项,我们要记得两点:
1.只指定类但没有指定类成员,那么类成员可能会被移除、优化或者混淆;
2.只指定类成员,与其相关联的代码可能会被优化或者混淆。

2.2 指令选项

指令选项分成7种,这里只列出在Android中常用到的部分。

I/O选项
# 不忽略类库中非public的类
-dontskipnonpubliclibraryclasses
Keep选项
# 保持ILicensingService这个公共类,名称要使用全路径类名
-keep public class com.google.vending.licensing.ILicensingService

# 对于R(资源)下的所有类及其方法,都不能被混淆
# 这里的$是Java编译器自动编译时内部类产生的
-keep class **.R$* {
    *;
}

# 保持继承自View类的set()、get()方法
# 这里的class *表示任意类,不考虑包名
-keepclassmembers public class * extends android.view.View {
   void set*(***);
   *** get*();
}

# 保持使用@Keep注解的字段及其类
-keepclasseswithmembers class * {
    @android.support.annotation.Keep <fields>;
}

# 保持native方法及其类(移除后剩下的)
# 等同于 -keepclasseswithmembers,allowshrinking,这里的allowshrinking是Keep选项的修饰符
-keepclasseswithmembernames class * {
    native <methods>;
}
压缩选项
# 列出无用代码
-printusage
优化选项
# 关闭优化
-dontoptimize

# 优化控制选项,通过指定优化名称进行过滤操作
# 不对算术和类型转化操作符优化,'!'表示排除匹配到的优化
-optimizations !code/simplification/arithmetic,!code/simplification/cast

# 优化次数,如果没有发现可优化的,就会结束优化
# 在0和7之间,默认为5
-optimizationpasses 5

# 允许提升访问修饰符,扩大可访问范围
-allowaccessmodification
混淆选项
# 不使用大小写混合的类名(默认设置)
# 混淆后的类名为小写
-dontusemixedcaseclassnames

# 保持Annotations
-keepattributes *Annotation*

# 保持泛型
-keepattributes Signature

# 保持文件属性和代码行号
# 方便在异常分析中定位
# SourceFile可能保留了重要的文件信息,如无必要,最好去掉
-keepattributes SourceFile,LineNumberTable
预校验选项
# 关闭预校验
-dontpreverify
通用选项
# 打印处理代码的信息,出现异常时,会打印完整栈追踪信息
-verbose

# 生成映射文件
# 包含有类名->混淆后类名的映射关系
# 与-verbose一起使用
-printmapping proguardMapping.txt

# 不打印处理过程中的警告信息
-dontwarn android.support.**

# 不打印关于错误或缺省配置的说明信息
-dontnote rx.internal.util.PlatformDependent
优化名称清单(挺多的,简略写):
   ● class/marking/final  #尽量使用final修饰类
   ● class/merging/*      #尽量使类层次成vertical或者horizontal
   ● field/*              #字段优化相关
   ● method/*             #方法优化相关
   ● code/*               #代码优化相关

2.3 小结

ProGuard的配置文件类似脚本文件,指令选项就是它的命令行。指令选项虽然有7种,不过Keep选项是ProGuard指令选项的主要部分,学习掌握Keep选项和常用的指令选项可以让我们使用Android ProGuard事半功倍。

这一部分内容可以查看 使用指南

上一篇 下一篇

猜你喜欢

热点阅读