Android ProGuard 代码混淆那些事儿

hewi7453 7年前
   <p>Android 开发中为了代码安全一般都会使用 ProGuard 进行代码混淆,它可以把类名、属性名和方法名变为毫无意义的 a, b, c 等,但是有些代码是不需要混淆的,这时需要配置 proguard-rules.pro 文件。这是许多开发者对代码混淆的认识,但是 ProGuard 更深入的内容呢,如何配置混淆规则呢,下面我就分享下 Android 中 ProGuard 那些事。</p>    <p>先敲敲黑板,划下重点:本文内容重点在第三节 – 自定义要保留的代码</p>    <h2>ProGuard 简介</h2>    <p>ProGuard 官网地址: <a href="/misc/goto?guid=4959740914203112959" rel="nofollow,noindex">https://www.guardsquare.com/en/proguard</a></p>    <p>ProGuardis a Java class file shrinker, optimizer, obfuscator, and preverifier. The shrinking step detects and removes unused classes, fields, methods and attributes. The optimization step analyzes and optimizes the bytecode of the methods. The obfuscation step renames the remaining classes, fields, and methods using short meaningless names. These first steps make the code base smaller, more efficient, and harder to reverse-engineer. The final preverification step adds preverification information to the classes, which is required for Java Micro Edition and for Java 6 and higher.</p>    <p>ProGuard 的功能有四个:压缩、优化、混淆以及预校验。压缩环节会检测病移除无用的类、字段、方法和属性。优化环节会分析并优化方法的字节码。混淆环节会用无意义的短变量去重命名其余的类、字段以及方法。这些环节可以使得代码更精简,更高效,也跟难被逆向工程。预校验是针对 J2ME 和 Java 6 以上的,在 Android 开发中不需要。</p>    <h2>Android Studio 中使用 ProGuard</h2>    <p>本节内容引用自 Android 官网: <a href="/misc/goto?guid=4959721357009656615" rel="nofollow,noindex">https://developer.android.com/studio/build/shrink-code.html</a></p>    <h3>启用 ProGuard</h3>    <p>要在 Android Studio 中使用 ProGuard,请在 build.gradle 文件内相应的构建类型中添加 minifyEnabled true 。</p>    <p>例如,下面在 release 版本构建时启用代码压缩:</p>    <pre>  <code class="language-groovy">android {      buildTypes {          release {              minifyEnabled true              proguardFiles getDefaultProguardFile('proguard-android.txt'),                      'proguard-rules.pro'          }      }      ...  }  </code></pre>    <p>注:Android Studio 会在使用 Instant Run 时停用 ProGuard。如果您需要为增量式构建压缩代码,请尝试试用 Gradle 压缩器。</p>    <p>每次启用 ProGuard 构建时,都会在 <module-name> /build/outputs/mapping/release/ 下输出下列文件:</p>    <ul>     <li> <p>dump.txt – 说明 APK 中所有类文件的内部结构。</p> </li>     <li> <p>mapping.txt – 提供原始与混淆过的类、方法和字段名称之间的转换。</p> </li>     <li> <p>seeds.txt – 列出未进行混淆的类和成员。</p> </li>     <li> <p>usage.txt – 列出从 APK 移除的代码。</p> </li>    </ul>    <h3>配置 ProGuard 规则</h3>    <p>上面的例子中除了 minifyEnabled 属性外,还有用于定义 ProGuard 规则的 proguardFiles 属性:</p>    <ul>     <li>getDefaultProguardFile('proguard-android.txt') 方法会从 Android SDK 目录下的 tools/proguard/ 文件夹获取默认的 ProGuard 设置,默认只会使用压缩和混淆两个功能。</li>    </ul>    <p>提示:想要进一步使用字节码优化功能,请使用位于同一位置的 proguard-android-optimize.txt 文件。它包含相同的 ProGuard 规则,但还包括其他在字节码一级(方法内和方法间)执行分析的优化,以进一步减小 APK 大小和提高运行速度。</p>    <p>注意:从 Android Gradle 插件 2.2 开始,使用的是 Gradle 插件中内置的配置文件,可以在 <root_project> /build/intermediates/proguard-files/ 下看到,Android SDK 目录下的配置 ProGuard 配置文件不再维护了,也会被 Gradle 2.2 以上的插件忽略。</p>    <ul>     <li>proguard-rules.pro 文件用于添加自定义的 ProGuard 规则,例如自定义保留一些不需要移除或混淆的代码。默认情况下,该文件位于模块根目录(build.gradle 文件旁)。</li>    </ul>    <p>要添加更多各构建变体专用的 ProGuard 规则,请在相应的 productFlavor 代码块中再添加一个 proguardFiles 属性。例如,以下 Gradle 文件会向 flavor2 产品定制添加 flavor2-rules.pro。现在 flavor2 使用所有三个 ProGuard 规则,因为还应用了来自 release 代码块的规则。</p>    <pre>  <code class="language-groovy">android {      ...      buildTypes {          release {              minifyEnabled true              proguardFiles getDefaultProguardFile('proguard-android.txt'),                     'proguard-rules.pro'          }      }      productFlavors {          flavor1 {          }          flavor2 {              proguardFile 'flavor2-rules.pro'          }      }  }  </code></pre>    <h3>转换混淆过的堆栈跟踪信息</h3>    <p>在经过 ProGuard 混淆后,方法的名称都经过了混淆处理,堆栈跟踪信息的可读性变得很差。要将其转化成可读的堆栈信息,请使用 retrace 脚本(在 Windows 上为 retrace.bat;在 Mac/Linux 上为 retrace.sh)。它位于 <sdk-root> /tools/proguard/ 目录中。该脚本利用 mapping.txt 文件和您的堆叠追踪生成新的可读堆叠追踪。使用 retrace 工具的语法如下:</p>    <pre>  <code class="language-groovy">retrace.bat|retrace.sh [-verbose] mapping.txt [<stacktrace_file>]    // example  retrace.sh -verbose mapping.txt obfuscated_trace.txt  </code></pre>    <p>如果不指定堆栈跟踪文件,retrace 工具会从标准输入读取。</p>    <h3>在 Instant Run 中启动代码压缩</h3>    <p>如果代码压缩在增量构建应用时非常重要,可以尝试使用 Anroid Gradle 插件内置的试用代码压缩器。也可以使用过与 ProGuard 相同的配置文件来配置 Android 插件压缩器。但是, <strong>Android 插件压缩器不会对您的代码进行混淆处理或优化</strong> ,它只会删除未使用的代码。</p>    <p>要启用 Android 插件压缩器,只需在 “debug” 构建类型中将 useProguard 设置为 false(并保留 minifyEnabled 设置 true):</p>    <pre>  <code class="language-groovy">android {      buildTypes {          debug {              minifyEnabled true              useProguard false              proguardFiles getDefaultProguardFile('proguard-android.txt'),                      'proguard-rules.pro'          }      }      ...  </code></pre>    <h2>自定义要保留的代码</h2>    <p>一般情况下,默认的 ProGuard 配置文件(proguard-android.txt)是不够,这样会移除所有未使用的代码,并且会混淆几乎所有(除了默认配置中保留的除外)的代码。但是有些情况下,我们是不想移除或混淆部分代码,例如:</p>    <ul>     <li> <p>Activity, Service, Receiver 等在 AndroidManifest.xml 文件中注册的类</p> </li>     <li> <p>JNI 中的 native 方法</p> </li>     <li> <p>反射使用的类,属性,方法</p> </li>    </ul>    <p>要保留不想被移除或混淆的代码,有两种方法:</p>    <p>一是在 proguard-rules.pro 文件中添加规则;</p>    <p>二是向需要保留的代码添加 <a href="/misc/goto?guid=4959751213802851568" rel="nofollow,noindex"> keep </a> 注解,需要引入 support-annotation 包。在类上添加 @Keep 可原样保留整个类。在方法或字段上添加它可完整保留方法 / 字段(及其名称)以及类名称。</p>    <p>一般都会使用第一种方法,因为规则都在这个文件中,方便查找和修改,不过 ProGuard 规则的语法一开始看可能难以理解,下面先看默认的 proguard-android.txt 中是如何写的。</p>    <h3>默认的 ProGuard 规则</h3>    <p>下面我列出的 <root_project> /build/intermediates/proguard-files/ 中的 proguard-android.txt-2.3.3,后面 2.3.3 是因为我这里的 Gradle 插件的版本为 2.3.3。前面有提过,Gradle 插件 2.2 以后,使用的是插件内置的 ProGuard 规则。</p>    <p>下面文件中 # 开头的是源文件的注释, // 开头的是我添加的说明:</p>    <pre>  <code class="language-groovy"># This is a configuration file for ProGuard.  # http://proguard.sourceforge.net/index.html#manual/usage.html  #  # Starting with version 2.2 of the Android plugin for Gradle, this file is distributed together with  # the plugin and unpacked at build-time. The files in $ANDROID_HOME are no longer maintained and  # will be ignored by new version of the Android plugin for Gradle.  // 上面三行注释可以看出 Gradle 2.2 之后使用的是内置的规则文件    -dontusemixedcaseclassnames     // 混淆时不使用大小写混写的类名  -dontskipnonpubliclibraryclasses    // 不跳过 libraray 中的非 public 类  -verbose    // 打印处理过程的信息    # Optimization is turned off by default. Dex does not like code run  # through the ProGuard optimize and preverify steps (and performs some  # of these optimizations on its own).  -dontoptimize   // 关闭优化功能,因为优化可能会造成一些潜在的风险,无法保证在所有版本的 Dalvik 都正常运行  -dontpreverify  // 关闭预校验功能,Android 平台上不需要,所以默认是关闭的  # Note that if you want to enable optimization, you cannot just  # include optimization flags in your own project configuration file;  # instead you will need to point to the  # "proguard-android-optimize.txt" file instead of this one from your  # project.properties file.    # Preserve some attributes that may be required for reflection.  -keepattributes *Annotation*,Signature,InnerClasses,EnclosingMethod     // 保留注解属性、泛型、内部类、封闭方法,后面都是三个属性都是为了反射正常运行    -keep public class com.google.vending.licensing.ILicensingService  -keep public class com.android.vending.licensing.ILicensingService  -keep public class com.google.android.vending.licensing.ILicensingService  -dontnote com.android.vending.licensing.ILicensingService  -dontnote com.google.vending.licensing.ILicensingService  -dontnote com.google.android.vending.licensing.ILicensingService    # For native methods, see http://proguard.sourceforge.net/manual/examples.html#native  -keepclasseswithmembernames class * {      native <methods>;  }  // 不混淆 native 方法名和包含 native 方法的类名,如果 native 方法未使用,还是会被移除    # Keep setters in Views so that animations can still work.  -keepclassmembers public class * extends android.view.View {      void set*(***);      *** get*();  }  // 保留继承自 View 的 setXx() 和 getXx() 方法,因为属性动画会用到相应的 setter 和 getter    # We want to keep methods in Activity that could be used in the XML attribute onClick.  -keepclassmembers class * extends android.app.Activity {      public void *(android.view.View);  }  // 保留 Activity 中参数是 View 的方法,因为在 XML 中配置 android:onClick="buttonClick" 属性时,点击该按钮时就会调用 Activity 中的 buttonClick(View view) 方法    # For enumeration classes, see http://proguard.sourceforge.net/manual/examples.html#enumerations  -keepclassmembers enum * {      public static **[] values();      public static ** valueOf(java.lang.String);  }  // 保留 enum 中的静态 values() 和 valueOf 方法,因为这些静态方法可能会在运行时通过内省调用    -keepclassmembers class * implements android.os.Parcelable {      public static final ** CREATOR;  }  // 保留 Parcelable 子类中的 CREATOR 字段    -keepclassmembers class **.R$* {      public static <fields>;  }  // 保留 R 文件中的所有静态字段,这些静态字段是用来记录资源 ID 的    # Preserve annotated Javascript interface methods.  -keepclassmembers class * {      @android.webkit.JavascriptInterface <methods>;  }  // 保留 JavascriptInterface 注解标记的方法,不然 js 调用时就会找不到方法    # The support libraries contains references to newer platform versions.  # Don't warn about those in case this app is linking against an older  # platform version. We know about them, and they are safe.  -dontnote android.support.**  -dontwarn android.support.**  // 不对 android.support 包下的代码警告,因为 support 包中一些代码是高版本才能使用,但是在低版本中有兼容性判断,是安全,所以在低版本打包时也不要警告    # Understand the @Keep support annotation.  -keep class android.support.annotation.Keep     // 保留 Keep 注解    // 接下来的规则都是保留 Keep 注解标记的类型  -keep @android.support.annotation.Keep class * {*;}   // 标记类时,保留类及其所有成员    -keepclasseswithmembers class * {      @android.support.annotation.Keep <methods>;  }  // 标记方法时,保留标注的方法和包含它的类名    -keepclasseswithmembers class * {      @android.support.annotation.Keep <fields>;  }  // 标记字段时,保留标记的字段和包含它的类名    -keepclasseswithmembers class * {      @android.support.annotation.Keep <init>(...);  }  // 标记构造函数时,保留标记的构造函数和包含它的类名  </code></pre>    <p>以上就是默认的 proguard-android.txt 中的所有规则,包含了常见的一些情况,但是在日常开发过程中,我们还是需要再自定义一些规则。不过 ProGuard 语法还是有些难以理解,例如上面的 keepclasseswithmembers 和 keepclasseswithmembernames 有什么区别呢? keepclassmembers 和 keepclasseswithmembers 又有什么区别呢?</p>    <h3>ProGuard 语法</h3>    <p>ProGuard 语法的官方文档: <a href="/misc/goto?guid=4959751213892491038" rel="nofollow,noindex">https://www.guardsquare.com/en/proguard/manual/usage</a></p>    <p>ProGuard 语法中的基本符号:</p>    <pre>  <code class="language-groovy"># 代表行注释符  - 表示一条规则的开始  </code></pre>    <p>一些常见选项:</p>    <pre>  <code class="language-groovy">-dontnote   // 不打印潜在的错误或疏漏的注释  -dontwarn   // 不针对未处理的引用或其他重要问题发出警告  -ignorewarning  // 忽略产生的警告继续往下运行  -dontskipnonpubliclibraryclassmembers // 不跳过非公开库的类成员  </code></pre>    <p>ProGuard 规则中最常用的是 Keep 选项,所以下面主要讲 Keep 选项的语法,ProGuard 中有 6 组 Keep 选项,以表格的方式显示如下:</p>    <table>     <thead>      <tr>       <th>Keep 选项</th>       <th>描述</th>       <th>压缩</th>       <th>混淆</th>      </tr>     </thead>     <tbody>      <tr>       <td>-keep</td>       <td>保留类和类中的成员,防止被移除或混淆</td>       <td>×</td>       <td>×</td>      </tr>      <tr>       <td>-keepnames</td>       <td>不混淆类和类中的成员</td>       <td>√</td>       <td>×</td>      </tr>      <tr>       <td>-keepclassmembers</td>       <td>保留类中的成员,防止被移除或混淆</td>       <td>×</td>       <td>×</td>      </tr>      <tr>       <td>-keepclasseswithmembers</td>       <td>保留类中成员及包含它的类,防止被移除或混淆</td>       <td>×</td>       <td>×</td>      </tr>      <tr>       <td>-keepclassmembernames</td>       <td>不混淆类中的成员</td>       <td>√</td>       <td>×</td>      </tr>      <tr>       <td>-keepclasseswithmembernames</td>       <td>不混淆类中的成员及包含它的类</td>       <td>√</td>       <td>×</td>      </tr>     </tbody>    </table>    <p>这六组选项看起来很容易搞混,其实只要掌握其中的关键差别就好区分了:(一)带 names 后缀的表示只是防止被混淆,还是可被移除,而没有 names 后缀则表示防止被混淆或移除;(二) classmembers 表示只对类中成员生效,而 classeswithmembers 表示不仅对类中成员生效,还对包含它的类生效;(三) keep 表示对类和类中匹配的成员生效,而 keepclasseswithmembers 表示对匹配的类成员及包含它的类生效,例如 -keep class * { native <methods>; } 表示保留所有的类和类中的 native 方法, -keepclasseswithmembers class * { native <methods>; } 表示保留所有的 native 方法及包含 native 方法的类。</p>    <p>Keep 选项后面的匹配条件中,经常需要用到通配符,例如上面默认 proguard-android.txt 规则中的 void set*(***); ,下面看看这些通配符的含义:</p>    <table>     <thead>      <tr>       <th>通配符</th>       <th>描述</th>      </tr>     </thead>     <tbody>      <tr>       <td><init></td>       <td>匹配所有构造函数</td>      </tr>      <tr>       <td><fields></td>       <td>匹配所有字段</td>      </tr>      <tr>       <td><methods></td>       <td>匹配所有方法,不包括构造函数</td>      </tr>      <tr>       <td>?</td>       <td>匹配任意单个字符</td>      </tr>      <tr>       <td>%</td>       <td>匹配任意原始数据类型,例如 boolean、int,但是不包括 void</td>      </tr>      <tr>       <td>*</td>       <td>匹配任意长度字符,但是不包括包名分隔符( . ),例如 android.support.* 不匹配 android.support.annotation.Keep</td>      </tr>      <tr>       <td>**</td>       <td>匹配任意长度字符,包括包名分隔符( . ),例如 android.support.** 匹配 support 包下的所有类</td>      </tr>      <tr>       <td>***</td>       <td>匹配任意类型,包括原始数据类型、数组</td>      </tr>      <tr>       <td>...</td>       <td>匹配任意数量的任意参数类型</td>      </tr>     </tbody>    </table>    <p>注意: ? , * 和 ** 都是不匹配原始数据类型和数组的,例如 void set*(**) 匹配 “void setObject(java.lang.Object obj)”,但是不匹配 “void setInt()” 和 “void setObject(java.lang.Object[] objs)”。</p>    <p>注意:”-keep class * extends android.view.View” 表示保留 View 的子类,方法和字段还是会被混淆的,而 “-keep class * extends android.view.View { *; }” 才表示保留所有 View 的子类及其成员。</p>    <p>在了解 Keep 选项和通配符后,在回头看默认的 proguard-android.txt 文件就轻松多了。</p>    <h3>常用的自定义 ProGuard 规则</h3>    <p>除了默认的 proguard-android.txt 文件的规则外,一般还需要在 proguard-rules.pro 中添加一些自定义的规则,下面是我总结的一些常用的自定义规则。</p>    <p>保留源代码行号</p>    <p>方便查看堆栈信息时对应源代码的位置。</p>    <pre>  <code class="language-groovy"># Keep source file name and the line numbers of methods  -keepattributes SourceFile,LineNumberTable  -renamesourcefileattribute SourceFile  </code></pre>    <p>Android 应用中常用规则</p>    <p>Android 开发中,一般需要保留 AndroidManifest.xml 定义的四大组件,自定义 View 的构造函数,参照自官网的 <a href="/misc/goto?guid=4959751213986790629" rel="nofollow,noindex">Android application sample</a> 。</p>    <pre>  <code class="language-groovy"># 一般可以加上这两个规则,不跳过非公开库的类成员,保留方法上的异常属性  -dontskipnonpubliclibraryclassmembers  -keepattributes Exceptions    # Keep android classes in AndroidManifest.xml  -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.view.View {         public <init>(android.content.Context);         public <init>(android.content.Context, android.util.AttributeSet);         public <init>(android.content.Context, android.util.AttributeSet, int);         public void set*(...);   }    -keepclasseswithmembers class * {       public <init>(android.content.Context, android.util.AttributeSet);   }     -keepclasseswithmembers class * {       public <init>(android.content.Context, android.util.AttributeSet, int);   }     -keepclassmembers class * extends android.content.Context {       public void *(android.view.View);       public void *(android.view.MenuItem);   }  </code></pre>    <p>混淆包名</p>    <pre>  <code class="language-groovy">-repackageclasses ''   -allowaccessmodification  </code></pre>    <p>不混淆 Serializable 相关内容</p>    <pre>  <code class="language-groovy">-keepnames class * implements java.io.Serializable  -keepclassmembers class * implements java.io.Serializable {       static final long serialVersionUID;       private static final java.io.ObjectStreamField[] serialPersistentFields;       !static !transient <fields>;       !private <fields>;       !private <methods>;       private void writeObject(java.io.ObjectOutputStream);       private void readObject(java.io.ObjectInputStream);       java.lang.Object writeReplace();       java.lang.Object readResolve();   }  </code></pre>    <p>保留实体类</p>    <pre>  <code class="language-groovy">-keep class com.xxx.entity.** {      void set*(***);       void set*(int, ***);       boolean is*();       boolean is*(int);       *** get*();       *** get*(int);   }  </code></pre>    <p>项目中用的一些开源库,只需要根据它们的建议添加相应的规则就可以了,这里就不再重复了。</p>    <p>这篇文章的内容就到这里了,感谢你耐心看到最后,希望对大家有所帮助,对于 ProGuard 还有疑问的欢迎在下面留言。</p>    <p> </p>    <p>来自:http://johnnyshieh.me/posts/android-proguard/</p>    <p> </p>