Android ProGuard 代碼混淆那些事兒

hewi7453 7年前發布 | 17K 次閱讀 安卓開發 Android開發 移動開發

Android 開發中為了代碼安全一般都會使用 ProGuard 進行代碼混淆,它可以把類名、屬性名和方法名變為毫無意義的 a, b, c 等,但是有些代碼是不需要混淆的,這時需要配置 proguard-rules.pro 文件。這是許多開發者對代碼混淆的認識,但是 ProGuard 更深入的內容呢,如何配置混淆規則呢,下面我就分享下 Android 中 ProGuard 那些事。

先敲敲黑板,劃下重點:本文內容重點在第三節 – 自定義要保留的代碼

ProGuard 簡介

ProGuard 官網地址: https://www.guardsquare.com/en/proguard

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.

ProGuard 的功能有四個:壓縮、優化、混淆以及預校驗。壓縮環節會檢測病移除無用的類、字段、方法和屬性。優化環節會分析并優化方法的字節碼。混淆環節會用無意義的短變量去重命名其余的類、字段以及方法。這些環節可以使得代碼更精簡,更高效,也跟難被逆向工程。預校驗是針對 J2ME 和 Java 6 以上的,在 Android 開發中不需要。

Android Studio 中使用 ProGuard

本節內容引用自 Android 官網: https://developer.android.com/studio/build/shrink-code.html

啟用 ProGuard

要在 Android Studio 中使用 ProGuard,請在 build.gradle 文件內相應的構建類型中添加 minifyEnabled true 。

例如,下面在 release 版本構建時啟用代碼壓縮:

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

注:Android Studio 會在使用 Instant Run 時停用 ProGuard。如果您需要為增量式構建壓縮代碼,請嘗試試用 Gradle 壓縮器。

每次啟用 ProGuard 構建時,都會在 <module-name> /build/outputs/mapping/release/ 下輸出下列文件:

  • dump.txt – 說明 APK 中所有類文件的內部結構。

  • mapping.txt – 提供原始與混淆過的類、方法和字段名稱之間的轉換。

  • seeds.txt – 列出未進行混淆的類和成員。

  • usage.txt – 列出從 APK 移除的代碼。

配置 ProGuard 規則

上面的例子中除了 minifyEnabled 屬性外,還有用于定義 ProGuard 規則的 proguardFiles 屬性:

  • getDefaultProguardFile('proguard-android.txt') 方法會從 Android SDK 目錄下的 tools/proguard/ 文件夾獲取默認的 ProGuard 設置,默認只會使用壓縮和混淆兩個功能。

提示:想要進一步使用字節碼優化功能,請使用位于同一位置的 proguard-android-optimize.txt 文件。它包含相同的 ProGuard 規則,但還包括其他在字節碼一級(方法內和方法間)執行分析的優化,以進一步減小 APK 大小和提高運行速度。

注意:從 Android Gradle 插件 2.2 開始,使用的是 Gradle 插件中內置的配置文件,可以在 <root_project> /build/intermediates/proguard-files/ 下看到,Android SDK 目錄下的配置 ProGuard 配置文件不再維護了,也會被 Gradle 2.2 以上的插件忽略。

  • proguard-rules.pro 文件用于添加自定義的 ProGuard 規則,例如自定義保留一些不需要移除或混淆的代碼。默認情況下,該文件位于模塊根目錄(build.gradle 文件旁)。

要添加更多各構建變體專用的 ProGuard 規則,請在相應的 productFlavor 代碼塊中再添加一個 proguardFiles 屬性。例如,以下 Gradle 文件會向 flavor2 產品定制添加 flavor2-rules.pro。現在 flavor2 使用所有三個 ProGuard 規則,因為還應用了來自 release 代碼塊的規則。

android {
    ...
    buildTypes {
        release {
            minifyEnabled true
            proguardFiles getDefaultProguardFile('proguard-android.txt'),
                   'proguard-rules.pro'
        }
    }
    productFlavors {
        flavor1 {
        }
        flavor2 {
            proguardFile 'flavor2-rules.pro'
        }
    }
}

轉換混淆過的堆棧跟蹤信息

在經過 ProGuard 混淆后,方法的名稱都經過了混淆處理,堆棧跟蹤信息的可讀性變得很差。要將其轉化成可讀的堆棧信息,請使用 retrace 腳本(在 Windows 上為 retrace.bat;在 Mac/Linux 上為 retrace.sh)。它位于 <sdk-root> /tools/proguard/ 目錄中。該腳本利用 mapping.txt 文件和您的堆疊追蹤生成新的可讀堆疊追蹤。使用 retrace 工具的語法如下:

retrace.bat|retrace.sh [-verbose] mapping.txt [<stacktrace_file>]

// example
retrace.sh -verbose mapping.txt obfuscated_trace.txt

如果不指定堆棧跟蹤文件,retrace 工具會從標準輸入讀取。

在 Instant Run 中啟動代碼壓縮

如果代碼壓縮在增量構建應用時非常重要,可以嘗試使用 Anroid Gradle 插件內置的試用代碼壓縮器。也可以使用過與 ProGuard 相同的配置文件來配置 Android 插件壓縮器。但是, Android 插件壓縮器不會對您的代碼進行混淆處理或優化 ,它只會刪除未使用的代碼。

要啟用 Android 插件壓縮器,只需在 “debug” 構建類型中將 useProguard 設置為 false(并保留 minifyEnabled 設置 true):

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

自定義要保留的代碼

一般情況下,默認的 ProGuard 配置文件(proguard-android.txt)是不夠,這樣會移除所有未使用的代碼,并且會混淆幾乎所有(除了默認配置中保留的除外)的代碼。但是有些情況下,我們是不想移除或混淆部分代碼,例如:

  • Activity, Service, Receiver 等在 AndroidManifest.xml 文件中注冊的類

  • JNI 中的 native 方法

  • 反射使用的類,屬性,方法

要保留不想被移除或混淆的代碼,有兩種方法:

一是在 proguard-rules.pro 文件中添加規則;

二是向需要保留的代碼添加 keep 注解,需要引入 support-annotation 包。在類上添加 @Keep 可原樣保留整個類。在方法或字段上添加它可完整保留方法 / 字段(及其名稱)以及類名稱。

一般都會使用第一種方法,因為規則都在這個文件中,方便查找和修改,不過 ProGuard 規則的語法一開始看可能難以理解,下面先看默認的 proguard-android.txt 中是如何寫的。

默認的 ProGuard 規則

下面我列出的 <root_project> /build/intermediates/proguard-files/ 中的 proguard-android.txt-2.3.3,后面 2.3.3 是因為我這里的 Gradle 插件的版本為 2.3.3。前面有提過,Gradle 插件 2.2 以后,使用的是插件內置的 ProGuard 規則。

下面文件中 # 開頭的是源文件的注釋, // 開頭的是我添加的說明:

# 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>(...);
}
// 標記構造函數時,保留標記的構造函數和包含它的類名

以上就是默認的 proguard-android.txt 中的所有規則,包含了常見的一些情況,但是在日常開發過程中,我們還是需要再自定義一些規則。不過 ProGuard 語法還是有些難以理解,例如上面的 keepclasseswithmembers 和 keepclasseswithmembernames 有什么區別呢? keepclassmembers 和 keepclasseswithmembers 又有什么區別呢?

ProGuard 語法

ProGuard 語法的官方文檔: https://www.guardsquare.com/en/proguard/manual/usage

ProGuard 語法中的基本符號:

# 代表行注釋符
- 表示一條規則的開始

一些常見選項:

-dontnote   // 不打印潛在的錯誤或疏漏的注釋
-dontwarn   // 不針對未處理的引用或其他重要問題發出警告
-ignorewarning  // 忽略產生的警告繼續往下運行
-dontskipnonpubliclibraryclassmembers // 不跳過非公開庫的類成員

ProGuard 規則中最常用的是 Keep 選項,所以下面主要講 Keep 選項的語法,ProGuard 中有 6 組 Keep 選項,以表格的方式顯示如下:

Keep 選項 描述 壓縮 混淆
-keep 保留類和類中的成員,防止被移除或混淆 × ×
-keepnames 不混淆類和類中的成員 ×
-keepclassmembers 保留類中的成員,防止被移除或混淆 × ×
-keepclasseswithmembers 保留類中成員及包含它的類,防止被移除或混淆 × ×
-keepclassmembernames 不混淆類中的成員 ×
-keepclasseswithmembernames 不混淆類中的成員及包含它的類 ×

這六組選項看起來很容易搞混,其實只要掌握其中的關鍵差別就好區分了:(一)帶 names 后綴的表示只是防止被混淆,還是可被移除,而沒有 names 后綴則表示防止被混淆或移除;(二) classmembers 表示只對類中成員生效,而 classeswithmembers 表示不僅對類中成員生效,還對包含它的類生效;(三) keep 表示對類和類中匹配的成員生效,而 keepclasseswithmembers 表示對匹配的類成員及包含它的類生效,例如 -keep class * { native <methods>; } 表示保留所有的類和類中的 native 方法, -keepclasseswithmembers class * { native <methods>; } 表示保留所有的 native 方法及包含 native 方法的類。

Keep 選項后面的匹配條件中,經常需要用到通配符,例如上面默認 proguard-android.txt 規則中的 void set*(***); ,下面看看這些通配符的含義:

通配符 描述
<init> 匹配所有構造函數
<fields> 匹配所有字段
<methods> 匹配所有方法,不包括構造函數
? 匹配任意單個字符
% 匹配任意原始數據類型,例如 boolean、int,但是不包括 void
* 匹配任意長度字符,但是不包括包名分隔符( . ),例如 android.support.* 不匹配 android.support.annotation.Keep
** 匹配任意長度字符,包括包名分隔符( . ),例如 android.support.** 匹配 support 包下的所有類
*** 匹配任意類型,包括原始數據類型、數組
... 匹配任意數量的任意參數類型

注意: ? , * 和 ** 都是不匹配原始數據類型和數組的,例如 void set*(**) 匹配 “void setObject(java.lang.Object obj)”,但是不匹配 “void setInt()” 和 “void setObject(java.lang.Object[] objs)”。

注意:”-keep class * extends android.view.View” 表示保留 View 的子類,方法和字段還是會被混淆的,而 “-keep class * extends android.view.View { *; }” 才表示保留所有 View 的子類及其成員。

在了解 Keep 選項和通配符后,在回頭看默認的 proguard-android.txt 文件就輕松多了。

常用的自定義 ProGuard 規則

除了默認的 proguard-android.txt 文件的規則外,一般還需要在 proguard-rules.pro 中添加一些自定義的規則,下面是我總結的一些常用的自定義規則。

保留源代碼行號

方便查看堆棧信息時對應源代碼的位置。

# Keep source file name and the line numbers of methods
-keepattributes SourceFile,LineNumberTable
-renamesourcefileattribute SourceFile

Android 應用中常用規則

Android 開發中,一般需要保留 AndroidManifest.xml 定義的四大組件,自定義 View 的構造函數,參照自官網的 Android application sample

# 一般可以加上這兩個規則,不跳過非公開庫的類成員,保留方法上的異常屬性
-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); 
}

混淆包名

-repackageclasses '' 
-allowaccessmodification

不混淆 Serializable 相關內容

-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(); 
}

保留實體類

-keep class com.xxx.entity.** {
    void set*(***); 
    void set*(int, ***); 
    boolean is*(); 
    boolean is*(int); 
    *** get*(); 
    *** get*(int); 
}

項目中用的一些開源庫,只需要根據它們的建議添加相應的規則就可以了,這里就不再重復了。

這篇文章的內容就到這里了,感謝你耐心看到最后,希望對大家有所幫助,對于 ProGuard 還有疑問的歡迎在下面留言。

 

來自:http://johnnyshieh.me/posts/android-proguard/

 

 本文由用戶 hewi7453 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!