(原创)ProGuard的介绍及使用

2015/05/23 Android

介绍

ProGuard是一个去冗余,优化,混淆,校验的java工具库。

  1. 在去冗余步骤中,它可以检测并移除不被使用的类,类成员,方法和属性。
  2. 在优化阶段,它可以分析和优化方法的字节码。
  3. 在混淆阶段它可以用简短无意义的名字重命名保留的类,类成员,和方法。

以上3步可以使代码更加精简,更加有效,更难反工程破解。最后校验可以给JAVA类增加校验信息。

以上每一步都是可选的。例如:ProGuard经常被用来把程序中多余的代码罗列出来,或者检验代码的使用效率。

p

ProGuard先读取输入jars包(包括aars,wars,ears,zips,apks,或者目录),然后它会按序执行去冗余,优化,混淆,和校验操作。另外,你可以多次执行优化操作来进一步改善代码。最终ProGuard会产生一个或者多个jars包(包括aars,wars,ears,zips,apks,或者目录),这些包包含混淆的映射文件。

ProGuard需要指定要处理的jars包(包括aars,wars,ears,zips,apks,或者目录),这些基本上都是编译的依赖包,ProGuard使用它们来重构类依赖关系,因为在程序运行中还需要依赖它们,所以这些依赖包本身需要保持不变。

入口

为了决定哪些代码必须保留,哪些可以丢弃,哪些可以混淆,你必须为你的代码指定一个或者多个入口。这些入口通常为方法类,程序,或者活动页面。

  • 在去冗余阶段,ProGuard从入口开始递归执行搜索哪些类和类成员会被使用,哪些类和类成员可以丢弃。
  • 在优化阶段,ProGuard除了进一步优化代码,还会有其他优化,比如将非外部访问的类和方法重命名为私有的,静态的,或者最终的。多余的参数会被移除。一些使用频率高的方位直接修饰为内联方法。
  • 在混淆阶段,ProGuard会对非入口类和类成员进行重命名,在此过程中,入口类和类成员会保持不变。
  • 检验阶段是唯一一个可用不需要知道入口的步骤。

反射

对于任何代码自动化工具来说,反射都是一大问题。在ProGuard里,你在代码里动态的创建或者调用类或方法也属于一个入口。例如,Class.forName()可以在任何运行时构建一个对象,通常来说,运用配置文件,你很难去计算哪些类需要保留。因此,你不得不在ProGuard配置文件中使用-keep选项来一一指定。

然而,ProGuard已经可以为你自动检测和处理以下关键使用场景:

  • 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”)

类名和类成员当然可以不一样,但ProGuard可以明确的识别构造方法,因此相关的类和类成员就可以在去冗余阶段得到保留,在混淆阶段字符串参数也会正确的得到更新。

此外,ProGuard可以对哪些需要保留给你提供一些建议,例如,ProGuard会提示一些构造如”(SomeClass)Class.forName(variable).newInstance()”,这可以指示哪些类或接口需要保留,你就可以在配置文件里更改适配它。

为了正确的结果,你至少要熟悉你要处理的代码,尤其是在代码里没有内在关系的的使用反射,需要不断混淆试验并排除错误。

限制

使用ProGuard,你可以意识到一些技术问题,基本上都是可以容易避免和解决的:

  • 为了最好的结果,ProGuard优化算法会假定被处理的代码从来不出现NullPointerExceptions,ArrayIndexOutOfBoundsExceptions,OutOfMemoryErrors,StackOverflowErrors异常。例如,它可以移除没有任何作用的myObject.myMethod(),因此可以避免myObject可能为null而带来的问题。在某种意义下,这是一个好的方式:优化代码可以抛出更少的异常。你可以使用-dontoptimize选项来关闭优化。
  • ProGuard优化算法也会假定被处理的代码从来不会出现忙等待循环,假如有,它会移除这些循环。你可以可以使用-dontoptimize选项来关闭优化。
  • 如果一个输入jar和库jar包含相同的包,混淆后的输出jar会和库jar包里的类名会重叠。这种情况在库jar已经混淆的情况下尤为常见。因此,应该避免在输入jar和库jar中出现相同的包名。
  • 混淆时,ProGuard会输出如”a.class”, “b.class”等的类名,如果一个包中包含大量的类,ProGuard也会输出”aux.class”类名,然而,在windows系统中,它会拒绝一个文件名使用保留名字。为了避免这种问题,最好还是配置下输出名。

命令

ProGuard命令大致可以分为以下几类:

  • Input/Output Options
  • Keep Options
  • Shrinking Options
  • Optimization Options
  • Obfuscation Options
  • Preverification Options
  • General Options
  • Class Paths
  • File Names
  • File Filters
  • Filters
  • Overview of Keep Options
  • Keep Option Modifiers
  • Class Specifications

由于命令繁多,在此就不全部解释了,随机举几个列子解释下:

保留选项

-keep [,modifier,…] class_specification

指定保留的类和类成员(字段和方法)。例如,为了保证一个应用程序正常运行,你要指定main类的main方法。为了保证一个库的正常使用,你要保留指定公开的可访问元素。 -keepclassmembers [,modifier,…] class_specification 指定要保留的类成员,

去冗余选项

-dontshrink

指定不对输入class文件去冗余。默认情况下,去冗余是开启的,除了直接或间接依赖的和用-keep选项罗列的类库,其他的全部会被移除。去冗余操作也可以在每次优化过后执行,因为优化之后可以移除更多不需要的类和类成员。

-printusage [filename]

指定罗列出输入文件中多余的代码。这些罗列可以标准输出,也可以指定文件进行保存。

-whyareyoukeeping class_specification

指定输出为什么输入的类和类成员在去冗余阶段之后还保存的细节原因。如果你惊讶为什么在输出文件中还有你不想要的类库时会非常有用。通常,这个原因会非常多,而这个选项可以为每个指定的类和类成员输出简短的引用关系链。当 -verbose选项被指定,那么就可以打印所有的字段和方法引用关系链。

优化选项

-dontoptimize

指定不对输入class文件优化。默认情况下,所有的方法都会在字节码层面优化。

使用

通用配置

#指定配置文件
#-printmapping proguardMapping.txt
#指定详细输出
-verbose
#取消校验,可以减少混淆时间
-dontpreverify
#不使用形形色色的命名类名
-dontusemixedcaseclassnames
#对代码的优化迭代次数
-optimizationpasses 5
#指定不去忽略包可见的库类的成员
-dontskipnonpubliclibraryclassmembers
#指定对代码优化算法
-optimizations !code/simplification/cast,!field/*,!class/merging/*
#保留异常,内部类,泛型,注解,源文件,代码行等属性
-keepattributes Exceptions,InnerClasses,Signature
-keepattributes *Annotation*
-keepattributes SourceFile,LineNumberTable

android通用配置

-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
-keep public class * extends android.view.View
-keep public class com.android.vending.licensing.ILicensingService
-keep class android.support.** {*;}

-keepclasseswithmembernames class * {
    native <methods>;
}
-keepclassmembers class * extends android.app.Activity{
    public void *(android.view.View);
}
-keepclassmembers enum * {
    public static **[] values();
    public static ** valueOf(java.lang.String);
}
-keep public class * extends android.view.View{
    *** get*();
    void set*(***);
    public <init>(android.content.Context);
    public <init>(android.content.Context, android.util.AttributeSet);
    public <init>(android.content.Context, android.util.AttributeSet, int);
}
-keepclasseswithmembers class * {
    public <init>(android.content.Context, android.util.AttributeSet);
    public <init>(android.content.Context, android.util.AttributeSet, int);
}
-keep class * implements android.os.Parcelable {
  public static final android.os.Parcelable$Creator *;
}
-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();
}
-keep class **.R$* {
 *;
}
-keepclassmembers class * {
    void *(**On*Event);
}

其它配置

android support库

-dontwarn android.support.**
-keep class android.support.**
-keep ,includedescriptorclasses class android.support.** { *;}

butterknife库

 -keep class butterknife.** { *; }
 -dontwarn butterknife.internal.**
 -keep class **$$ViewBinder { *; }
 -keepclasseswithmembernames class * {
  @butterknife.* <fields>;
 }
 -keepclasseswithmembernames class * {
 @butterknife.* <methods>;
 }

dlna库

-dontwarn org.teleal.**
-keep ,includedescriptorclasses class org.teleal.** { *;}

经验

之前在调试版本一切正常,发布版本已运行就崩溃,细查原因就是dlna中使用了内部类的反射技术,最后发现是因为proguard.txt中没有保留InnerClasses属性。另外SourceFile,LineNumberTable属性强烈建议保留,不然即使有崩溃日志也会让你抓狂,除非再去对比混淆映射,对于我这样的人来说还是更倾向看代码行数定位的更快些。


知识共享许可协议
本作品采用知识共享署名-非商业性使用 4.0 国际许可协议进行许可。

站内搜索

    撩我备注-博客

    joinee

    目录结构