Android ProGuard 代碼混淆那些事兒

Android 開(kāi)發(fā)中為了代碼安全一般都會(huì)使用 ProGuard 進(jìn)行代碼混淆,它可以把類(lèi)名、屬性名和方法名變?yōu)楹翢o(wú)意義的 a, b, c 等,但是有些代碼是不需要混淆的,這時(shí)需要配置 proguard-rules.pro 文件。這是許多開(kāi)發(fā)者對(duì)代碼混淆的認(rèn)識(shí),但是 ProGuard 更深入的內(nèi)容呢,如何配置混淆規(guī)則呢,下面我就分享下 Android 中 ProGuard 那些事。

先敲敲黑板,劃下重點(diǎn):本文內(nèi)容重點(diǎn)在第三節(jié) -- 自定義要保留的代碼

ProGuard 簡(jiǎn)介

ProGuard 官網(wǎng)地址:https://www.guardsquare.com/en/proguard

ProGuard is 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 的功能有四個(gè):壓縮、優(yōu)化、混淆以及預(yù)校驗(yàn)。壓縮環(huán)節(jié)會(huì)檢測(cè)病移除無(wú)用的類(lèi)、字段、方法和屬性。優(yōu)化環(huán)節(jié)會(huì)分析并優(yōu)化方法的字節(jié)碼。混淆環(huán)節(jié)會(huì)用無(wú)意義的短變量去重命名其余的類(lèi)、字段以及方法。這些環(huán)節(jié)可以使得代碼更精簡(jiǎn),更高效,也跟難被逆向工程。預(yù)校驗(yàn)是針對(duì) J2ME 和 Java 6 以上的,在 Android 開(kāi)發(fā)中不需要。

Android Studio 中使用 ProGuard

本節(jié)內(nèi)容引用自 Android 官網(wǎng):https://developer.android.com/studio/build/shrink-code.html

啟用 ProGuard

要在 Android Studio 中使用 ProGuard,請(qǐng)?jiān)?build.gradle 文件內(nèi)相應(yīng)的構(gòu)建類(lèi)型中添加 minifyEnabled true。

例如,下面在 release 版本構(gòu)建時(shí)啟用代碼壓縮:

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

注:Android Studio 會(huì)在使用 Instant Run 時(shí)停用 ProGuard。如果您需要為增量式構(gòu)建壓縮代碼,請(qǐng)嘗試試用 Gradle 壓縮器。

每次啟用 ProGuard 構(gòu)建時(shí),都會(huì)在 <module-name>/build/outputs/mapping/release/ 下輸出下列文件:

  • dump.txt -- 說(shuō)明 APK 中所有類(lèi)文件的內(nèi)部結(jié)構(gòu)。

  • mapping.txt -- 提供原始與混淆過(guò)的類(lèi)、方法和字段名稱(chēng)之間的轉(zhuǎn)換。

  • seeds.txt -- 列出未進(jìn)行混淆的類(lèi)和成員。

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

配置 ProGuard 規(guī)則

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

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

提示:想要進(jìn)一步使用字節(jié)碼優(yōu)化功能,請(qǐng)使用位于同一位置的 proguard-android-optimize.txt 文件。它包含相同的 ProGuard 規(guī)則,但還包括其他在字節(jié)碼一級(jí)(方法內(nèi)和方法間)執(zhí)行分析的優(yōu)化,以進(jìn)一步減小 APK 大小和提高運(yùn)行速度。

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

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

要添加更多各構(gòu)建變體專(zhuān)用的 ProGuard 規(guī)則,請(qǐng)?jiān)谙鄳?yīng)的 productFlavor 代碼塊中再添加一個(gè) proguardFiles 屬性。例如,以下 Gradle 文件會(huì)向 flavor2 產(chǎn)品定制添加 flavor2-rules.pro。現(xiàn)在 flavor2 使用所有三個(gè) ProGuard 規(guī)則,因?yàn)檫€應(yīng)用了來(lái)自 release 代碼塊的規(guī)則。

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

轉(zhuǎn)換混淆過(guò)的堆棧跟蹤信息

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

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

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

如果不指定堆棧跟蹤文件,retrace 工具會(huì)從標(biāo)準(zhǔn)輸入讀取。

在 Instant Run 中啟動(dòng)代碼壓縮

如果代碼壓縮在增量構(gòu)建應(yīng)用時(shí)非常重要,可以嘗試使用 Anroid Gradle 插件內(nèi)置的試用代碼壓縮器。也可以使用過(guò)與 ProGuard 相同的配置文件來(lái)配置 Android 插件壓縮器。但是,Android 插件壓縮器不會(huì)對(duì)您的代碼進(jìn)行混淆處理或優(yōu)化,它只會(huì)刪除未使用的代碼。

要啟用 Android 插件壓縮器,只需在 "debug" 構(gòu)建類(lèi)型中將 useProguard 設(shè)置為 false(并保留 minifyEnabled 設(shè)置 true):

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

自定義要保留的代碼

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

  • Activity, Service, Receiver 等在 AndroidManifest.xml 文件中注冊(cè)的類(lèi)

  • JNI 中的 native 方法

  • 反射使用的類(lèi),屬性,方法

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

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

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

一般都會(huì)使用第一種方法,因?yàn)橐?guī)則都在這個(gè)文件中,方便查找和修改,不過(guò) ProGuard 規(guī)則的語(yǔ)法一開(kāi)始看可能難以理解,下面先看默認(rèn)的 proguard-android.txt 中是如何寫(xiě)的。

默認(rèn)的 ProGuard 規(guī)則

下面我列出的 <root_project>/build/intermediates/proguard-files/ 中的 proguard-android.txt-2.3.3,后面 2.3.3 是因?yàn)槲疫@里的 Gradle 插件的版本為 2.3.3。前面有提過(guò),Gradle 插件 2.2 以后,使用的是插件內(nèi)置的 ProGuard 規(guī)則。

下面文件中 # 開(kāi)頭的是源文件的注釋?zhuān)?code>// 開(kāi)頭的是我添加的說(shuō)明:

# 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 之后使用的是內(nèi)置的規(guī)則文件

-dontusemixedcaseclassnames     // 混淆時(shí)不使用大小寫(xiě)混寫(xiě)的類(lèi)名
-dontskipnonpubliclibraryclasses    // 不跳過(guò) libraray 中的非 public 類(lèi)
-verbose    // 打印處理過(guò)程的信息

# 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   // 關(guān)閉優(yōu)化功能,因?yàn)閮?yōu)化可能會(huì)造成一些潛在的風(fēng)險(xiǎn),無(wú)法保證在所有版本的 Dalvik 都正常運(yùn)行
-dontpreverify  // 關(guān)閉預(yù)校驗(yàn)功能,Android 平臺(tái)上不需要,所以默認(rèn)是關(guān)閉的
# 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     // 保留注解屬性、泛型、內(nèi)部類(lèi)、封閉方法,后面都是三個(gè)屬性都是為了反射正常運(yùn)行

-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 方法的類(lèi)名,如果 native 方法未使用,還是會(huì)被移除

# Keep setters in Views so that animations can still work.
-keepclassmembers public class * extends android.view.View {
    void set*(***);
    *** get*();
}
// 保留繼承自 View 的 setXx() 和 getXx() 方法,因?yàn)閷傩詣?dòng)畫(huà)會(huì)用到相應(yīng)的 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 中參數(shù)是 View 的方法,因?yàn)樵?XML 中配置 android:onClick="buttonClick" 屬性時(shí),點(diǎn)擊該按鈕時(shí)就會(huì)調(diào)用 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 中的靜態(tài) values() 和 valueOf 方法,因?yàn)檫@些靜態(tài)方法可能會(huì)在運(yùn)行時(shí)通過(guò)內(nèi)省調(diào)用

-keepclassmembers class * implements android.os.Parcelable {
    public static final ** CREATOR;
}
// 保留 Parcelable 子類(lèi)中的 CREATOR 字段

-keepclassmembers class **.R$* {
    public static <fields>;
}
// 保留 R 文件中的所有靜態(tài)字段,這些靜態(tài)字段是用來(lái)記錄資源 ID 的

# Preserve annotated Javascript interface methods.
-keepclassmembers class * {
    @android.webkit.JavascriptInterface <methods>;
}
// 保留 JavascriptInterface 注解標(biāo)記的方法,不然 js 調(diào)用時(shí)就會(huì)找不到方法

# 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.**
// 不對(duì) android.support 包下的代碼警告,因?yàn)?support 包中一些代碼是高版本才能使用,但是在低版本中有兼容性判斷,是安全,所以在低版本打包時(shí)也不要警告

# Understand the @Keep support annotation.
-keep class android.support.annotation.Keep     // 保留 Keep 注解

// 接下來(lái)的規(guī)則都是保留 Keep 注解標(biāo)記的類(lèi)型
-keep @android.support.annotation.Keep class * {*;}   // 標(biāo)記類(lèi)時(shí),保留類(lèi)及其所有成員

-keepclasseswithmembers class * {
    @android.support.annotation.Keep <methods>;
}
// 標(biāo)記方法時(shí),保留標(biāo)注的方法和包含它的類(lèi)名

-keepclasseswithmembers class * {
    @android.support.annotation.Keep <fields>;
}
// 標(biāo)記字段時(shí),保留標(biāo)記的字段和包含它的類(lèi)名

-keepclasseswithmembers class * {
    @android.support.annotation.Keep <init>(...);
}
// 標(biāo)記構(gòu)造函數(shù)時(shí),保留標(biāo)記的構(gòu)造函數(shù)和包含它的類(lèi)名

以上就是默認(rèn)的 proguard-android.txt 中的所有規(guī)則,包含了常見(jiàn)的一些情況,但是在日常開(kāi)發(fā)過(guò)程中,我們還是需要再自定義一些規(guī)則。不過(guò) ProGuard 語(yǔ)法還是有些難以理解,例如上面的 keepclasseswithmemberskeepclasseswithmembernames 有什么區(qū)別呢?keepclassmemberskeepclasseswithmembers 又有什么區(qū)別呢?

ProGuard 語(yǔ)法

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

ProGuard 語(yǔ)法中的基本符號(hào):

# 代表行注釋符
- 表示一條規(guī)則的開(kāi)始

一些常見(jiàn)選項(xiàng):

-dontnote   // 不打印潛在的錯(cuò)誤或疏漏的注釋
-dontwarn   // 不針對(duì)未處理的引用或其他重要問(wèn)題發(fā)出警告
-ignorewarning  // 忽略產(chǎn)生的警告繼續(xù)往下運(yùn)行
-dontskipnonpubliclibraryclassmembers // 不跳過(guò)非公開(kāi)庫(kù)的類(lèi)成員

ProGuard 規(guī)則中最常用的是 Keep 選項(xiàng),所以下面主要講 Keep 選項(xiàng)的語(yǔ)法,ProGuard 中有 6 組 Keep 選項(xiàng),以表格的方式顯示如下:

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

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

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

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

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

注意:"-keep class * extends android.view.View" ?表示保留 View 的子類(lèi),方法和字段還是會(huì)被混淆的,而 "-keep class * extends android.view.View { *; }" 才表示保留所有 View 的?子類(lèi)及其成員。

在了解 Keep 選項(xiàng)和通配符后,在回頭看默認(rèn)的 proguard-android.txt 文件就輕松多了。

常用的自定義 ProGuard 規(guī)則

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

保留源代碼行號(hào)

方便查看堆棧信息時(shí)對(duì)應(yīng)源代碼的位置。

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

Android 應(yīng)用中常用規(guī)則

Android 開(kāi)發(fā)中,一般需要保留 AndroidManifest.xml 定義的四大組件,自定義 View 的構(gòu)造函數(shù),參照自官網(wǎng)的 Android application sample。

# 一般可以加上?這兩個(gè)規(guī)則,不跳過(guò)非公開(kāi)庫(kù)的類(lèi)成員,保留方法上的異常屬性
-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 相關(guān)內(nèi)容

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

保留實(shí)體類(lèi)

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

項(xiàng)目中用的一些開(kāi)源庫(kù),只需要根據(jù)它們的建議添加相應(yīng)的規(guī)則就可以了,這里就不再重復(fù)了。

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容