Android 程式碼混淆規則

往事惘逝發表於2018-07-17

1. Proguard介紹

Android SDK自帶了混淆工具Proguard。它位於SDK根目錄\tools\proguard下面。 ProGuard是一個免費的Java類檔案收縮,優化,混淆和預校驗器。它可以檢測並刪除未使用的類,欄位,方法和屬性。它可以優化位元組碼,並刪除未使用的指令。它可以將類、欄位和方法使用短無意義的名稱進行重新命名。最後,預校驗的Java6或針對Java MicroEdition的所述處理後的碼。 如果開啟了混淆,Proguard預設情況下會對所有程式碼,包括第三方包都進行混淆,可是有些程式碼或者第三方包是不能混淆的,這就需要我們手動編寫混淆規則來保持不能被混淆的部分。

2. Proguard作用

Android中的“混淆”可以分為兩部分,一部分是 Java 程式碼的優化與混淆,依靠 proguard 混淆器來實現;另一部分是資源壓縮,將移除專案及依賴的庫中未被使用的資源(資源壓縮嚴格意義上跟混淆沒啥關係,但一般我們都會放一起講)。

2.1 程式碼混淆

壓縮(Shrinking):預設開啟,用以減小應用體積,移除未被使用的類和成員,並且會在優化動作執行之後再次執行(因為優化後可能會再次暴露一些未被使用的類和成員)。

    -dontshrink 關閉壓縮
複製程式碼

優化(Optimization):預設開啟,在位元組碼級別執行優化,讓應用執行的更快。

    -dontoptimize  關閉優化
    -optimizationpasses n 表示proguard對程式碼進行迭代優化的次數,Android一般為5
複製程式碼

混淆(Obfuscation):預設開啟,增大反編譯難度,類、函式、變數名會被隨機命名成無意義的代號形如:a,b,c...之類的,除非用keep保護。

    -dontobfuscate 關閉混淆
複製程式碼

上面這幾個功能都是預設開啟的,要關閉他們只需配置對應的規則即可。 混淆後預設會在工程目錄app/build/outputs/mapping/release下生成一個mapping.txt檔案,這就是混淆規則,我們可以根據這個檔案把混淆後的程式碼反推回源本的程式碼,所以這個檔案很重要,注意保護好。原則上,程式碼混淆後越亂越無規律越好,但有些地方我們是要避免混淆的,否則程式執行就會出錯。

2.2 資源壓縮

資源壓縮將移除專案及依賴的庫中未被使用的資源,這在減少 apk 包體積上會有不錯的效果,一般建議開啟。具體做法是在 build.grade 檔案中,將 shrinkResources 屬性設定為 true。需要注意的是,只有在用minifyEnabled true開啟了程式碼壓縮後,資源壓縮才會生效。 資源壓縮包含了“合併資源”和“移除資源”兩個流程。 “合併資源”流程中,名稱相同的資源被視為重複資源會被合併。需要注意的是,這一流程不受shrinkResources屬性控制,也無法被禁止, gradle 必然會做這項工作,因為假如不同專案中存在相同名稱的資源將導致錯誤。gradle 在四處地方尋找重複資源:

  • src/main/res/ 路徑
  • 不同的構建型別(debug、release等等)
  • 不同的構建渠道
  • 專案依賴的第三方庫 合併資源時按照如下優先順序順序:
    依賴 -> main -> 渠道 -> 構建型別
複製程式碼

舉個例子,假如重複資源同時存在於main資料夾和不同渠道中,gradle 會選擇保留渠道中的資源。 同時,如果重複資源在同一層次出現,比如src/main/res/src/main/res2/,則 gradle 無法完成資源合併,這時會報資源合併錯誤。 “移除資源”流程則見名知意,需要注意的是,類似程式碼,混淆資源移除也可以定義哪些資源需要被保留,這點在下文給出。

3. Proguard規則

3.1 基本指令

  • -ignorewarning:是否忽略警告
  • -optimizationpasses n:指定程式碼的壓縮級別(在0~7之間,預設為5)
  • -dontusemixedcaseclassnames:是否使用大小寫混合(windows大小寫不敏感,建議加入)
  • -dontskipnonpubliclibraryclasses:是否混淆非公共的庫的類
  • -dontskipnonpubliclibraryclassmembers:是否混淆非公共的庫的類的成員
  • -dontpreverify:混淆時是否做預校驗(Android不需要預校驗,去掉可以加快混淆速度)
  • -verbose:混淆時是否記錄日誌(混淆後會生成對映檔案)
  • -obfuscationdictionary dictionary_path:指定外部模糊字典
  • -classobfuscationdictionary dictionary_path:指定class模糊字典
  • -packageobfuscationdictionary dictionary_path:指定package模糊字典
  • -optimizations !code/simplification/arithmetic,!field/,!class/merging/,!code/allocation/variable:混淆時所採用的演算法(谷歌推薦演算法)
  • -libraryjars libs(*.jar;):新增支援的jar(引入libs下的所有jar包)
  • -renamesourcefileattribute SourceFile:將檔案來源重新命名為“SourceFile”字串
  • -keepattributes Annotation:保持註解不被混淆
  • -keep class * extends java.lang.annotation.Annotation {*;}:保持註解不被混淆
  • -keep interface * extends java.lang.annotation.Annotation { *; }:保持註解不被混淆
  • -keepattributes Signature:保持泛型不被混淆
  • -keepattributes EnclosingMethod:保持反射不被混淆
  • -keepattributes Exceptions:保持異常不被混淆
  • -keepattributes InnerClasses:保持內部類不被混淆
  • -keepattributes SourceFile,LineNumberTable:丟擲異常時保留程式碼行號

3.2 保留選項

  • -keep [,modifier,...] class_specification:指定需要保留的類和類成員(作為公共類庫,應該保留所有可公開訪問的public方法)
  • -keepclassmembers [,modifier,...] class_specification:指定需要保留的類成員:變數或者方法
  • -keepclasseswithmembers [,modifier,...] class_specification:指定保留的類和類成員,條件是所指定的類成員都存在(既在壓縮階段沒有被刪除的成員,效果和keep差不多)
  • -keepnames class_specification:指定要保留名稱的類和類成員,前提是在壓縮階段未被刪除,僅用於模糊處理。[-keep allowshrinking class_specification 的簡寫]
  • -keepclassmembernames class_specification:指定要保留名稱的類成員,前提是在壓縮階段未被刪除,僅用於模糊處理。[-keepclassmembers allowshrinking class_specification 的簡寫]
  • -keepclasseswithmembernames class_specification:指定要保留名稱的類成員,前提是在壓縮階段後所指定的類成員都存在,僅用於模糊處理。[-keepclasseswithmembers allowshrinking class_specification 的簡寫]
  • -printseeds [filename]:指定詳盡列出由各種-keep選項匹配的類和類成員。列表列印到標準輸出或給定檔案。 該列表可用於驗證是否真的找到了預期的類成員,特別是如果您使用萬用字元。

4. Keep命令說明

命令 作用
-keep 保持類和類成員,防止被移除或者被重新命名
-keepnames 保持類和類成員,防止被重新命名
-keepclassmembers 保持類成員,防止被移除或者被重新命名
-keepclassmembernames 保持類成員,防止被重新命名
-keepclasseswithmembers 保持擁有該成員的類和成員,防止被移除或者被重新命名
-keepclasseswithmembernames 保持擁有該成員的類和成員,防止被重新命名

保持元素不參與混淆的規則的命令格式:

[保持命令] [類] {
    [成員]
}
複製程式碼

“類”代表類相關的限定條件,它將最終定位到某些符合該限定條件的類。它的內容可以使用:

  • 具體的類
  • 訪問修飾符(public、protected、private
  • 萬用字元*,匹配任意長度字元,但不含包名分隔符(.)
  • 萬用字元**,匹配任意長度字元,並且包含包名分隔符(.)
  • extends,即可以指定類的基類
  • implement,匹配實現了某介面的類
  • $,內部類 “成員”代表類成員相關的限定條件,它將最終定位到某些符合該限定條件的類成員。它的內容可以使用:
  • <init> 匹配所有構造器
  • <fields> 匹配所有域
  • <methods> 匹配所有方法
  • 萬用字元*,匹配任意長度字元,但不含包名分隔符(.)
  • 萬用字元**,匹配任意長度字元,並且包含包名分隔符(.)
  • 萬用字元***,匹配任意引數型別
  • ,匹配任意長度的任意型別引數。比如void test(…)就能匹配任意 void test(String a) 或者是 void test(int a, String b) 這些方法。
  • 訪問修飾符(public、protected、private

4.1 不混淆某個類

    -keep public class com.android.proguard.example.Test { *; }
複製程式碼

4.2 不混淆某個包所有的類

    -keep class com.android.proguard.example.** { *; }
複製程式碼

4.3 不混淆某個類的子類

    -keep public class * extends com.android.proguard.example.Test { *; }
複製程式碼

4.4 不混淆所有類名中包含了“model”的類及其成員

    -keep public class **.*model*.** {*;}
複製程式碼

4.5 不混淆某個介面的實現

    -keep class * implements com.android.proguard.example.TestInterface { *; }
複製程式碼

4.6 不混淆某個類的構造方法

    -keepclassmembers class com.android.proguard.example.Test {
        public <init>();
    }
複製程式碼

4.7 不混淆某個類的特定的方法

    -keepclassmembers class com.android.proguard.example.Test {
        public void test(java.lang.String);
    }
複製程式碼

4.8 不混淆某個類的內部類

    -keep class com.android.proguard.example.Test$* {
            *;
     }
複製程式碼

5. Proguard注意事項

5.1 保持基本元件不被混淆

    -keep public class * extends android.app.Fragment
    -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
複製程式碼

5.2 保持 Google 原生服務需要的類不被混淆

    -keep public class com.google.vending.licensing.ILicensingService
    -keep public class com.android.vending.licensing.ILicensingService
複製程式碼

5.3 Support包規則

    -dontwarn android.support.**
    -keep public class * extends android.support.v4.**
    -keep public class * extends android.support.v7.**
    -keep public class * extends android.support.annotation.**
複製程式碼

5.4 保持 native 方法不被混淆

    -keepclasseswithmembernames class * { ####
        native <methods>;
    }
複製程式碼

5.5 保留自定義控制元件(繼承自View)不被混淆

    -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);
    }
複製程式碼

5.6 保留指定格式的構造方法不被混淆

    -keepclasseswithmembers class * {
        public <init>(android.content.Context, android.util.AttributeSet);
        public <init>(android.content.Context, android.util.AttributeSet, int);
    }
複製程式碼

5.7 保留在Activity中的方法引數是view的方法(避免佈局檔案裡面onClick被影響)

    -keepclassmembers class * extends android.app.Activity {
        public void *(android.view.View);
    }
複製程式碼

5.8 保持列舉 enum 類不被混淆

    -keepclassmembers enum * {
        public static **[] values();
        public static ** valueOf(java.lang.String);
    }
複製程式碼

5.9 保持R(資源)下的所有類及其方法不能被混淆

    -keep class **.R$* { *; }
複製程式碼

5.10 保持 Parcelable 序列化的類不被混淆(注:aidl檔案不能去混淆)

    -keep class * implements android.os.Parcelable {
        public static final android.os.Parcelable$Creator *;
    }
複製程式碼

5.11 需要序列化和反序列化的類不能被混淆(注:Java反射用到的類也不能被混淆)

    -keepnames class * implements java.io.Serializable
複製程式碼

5.12 保持 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();
    }
複製程式碼

5.13 保持 BaseAdapter 類不被混淆

    -keep public class * extends android.widget.BaseAdapter { *; }
複製程式碼

5.14 保持 CusorAdapter 類不被混淆

    -keep public class * extends android.widget.CusorAdapter{ *; }
複製程式碼

5.15 保持反射用到的類和與JavaScript進行互動的類不被混淆

6. 自定義資源保持規則

6.1 keep.xml

shrinkResources true開啟資源壓縮後,所有未被使用的資源預設被移除。假如你需要定義哪些資源必須被保留,在res/raw/路徑下建立一個xml檔案,例如keep.xml。 通過一些屬性的設定可以實現定義資源保持的需求,可配置的屬性有:

  • tools:keep 定義哪些資源需要被保留(資源之間用“,”隔開)
  • tools:discard 定義哪些資源需要被移除(資源之間用“,”隔開)
  • tools:shrinkMode 開啟嚴格模式 當程式碼中通過 Resources.getIdentifier() 用動態的字串來獲取並使用資源時,普通的資源引用檢查就可能會有問題。例如,如下程式碼會導致所有以“img_”開頭的資源都被標記為已使用。
    String name = String.format("img_%1d", angle + 1);
    res = getResources().getIdentifier(name, "drawable", getPackageName());
複製程式碼

我們可以設定 tools:shrinkModestrict 來開啟嚴格模式,使只有確實被使用的資源被保留。 以上就是自定義資源保持規則相關的配置,舉個例子:

    <?xml version="1.0" encoding="utf-8"?>
    <resources xmlns:tools="http://schemas.android.com/tools"
        tools:keep="@drawable/img_*,@drawable/ic_launcher,@layout/layout_used*"
        tools:discard="@layout/layout_unused"
        tools:shrinkMode="strict"/>
複製程式碼

6.2 移除替代資源

一些替代資源,例如多語言支援的 strings.xml,多解析度支援的 layout.xml 等,在我們不需要使用又不想刪除掉時,可以使用資源壓縮將它們移除。 我們使用 resConfig 屬性來指定需要支援的屬性,例如

    android {
        defaultConfig {
            ...
            resConfigs "en", "zh"
        }
    }
複製程式碼

其他未顯式宣告的語言資源將被移除。

7. Proguard使用

7.1 開啟混淆

在專案的可執行工程Module中開啟build.gradle檔案進行編輯:

android {
    ......
    defaultConfig {
        ......
    }
    buildTypes {
        release {
            minifyEnabled true      // 開啟程式碼混淆
            zipAlignEnabled true    // 開啟Zip壓縮優化
            shrinkResources true    // 移除未被使用的資源
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
    ......
}
複製程式碼
  • minifyEnabled:是否進行程式碼混淆
  • zipAlignEnabled:是否進行Zip壓縮優化
  • shrinkResources:是否移除未被使用的資源
  • proguardFiles:混淆規則配置檔案
    • proguard-android.txt:AndroidStudio預設自動匯入的規則,這個檔案位於Android SDK根目錄\tools\proguard\proguard-android.txt。這裡面是一些比較常規的不能被混淆的程式碼規則。
    • proguard-rules.pro:針對自己的專案需要特別定義的混淆規則,它位於專案每個Module的根目錄下面,裡面的內容需要我們自己編寫。

7.2 編寫混淆規則

# --------------------------------------------基本指令區--------------------------------------------#
-ignorewarning                                      # 是否忽略警告
-optimizationpasses 5                               # 指定程式碼的壓縮級別(在0~7之間,預設為5)
-dontusemixedcaseclassnames                         # 是否使用大小寫混合(windows大小寫不敏感,建議加入)
-dontskipnonpubliclibraryclasses                    # 是否混淆非公共的庫的類
-dontskipnonpubliclibraryclassmembers               # 是否混淆非公共的庫的類的成員
-dontpreverify                                      # 混淆時是否做預校驗(Android不需要預校驗,去掉可以加快混淆速度)
-verbose                                            # 混淆時是否記錄日誌(混淆後會生成對映檔案)

#指定外部模糊字典
-obfuscationdictionary dictionary1.txt
#指定class模糊字典
-classobfuscationdictionary dictionary1.txt
#指定package模糊字典
-packageobfuscationdictionary dictionary2.txt

# 混淆時所採用的演算法(谷歌推薦演算法)
-optimizations !code/simplification/arithmetic,!field/*,!class/merging/*,!code/allocation/variable

# 新增支援的jar(引入libs下的所有jar包)
-libraryjars libs(*.jar;)

# 將檔案來源重新命名為“SourceFile”字串
-renamesourcefileattribute SourceFile

# 保持註解不被混淆
-keepattributes *Annotation*
-keep class * extends java.lang.annotation.Annotation {*;}

# 保持泛型不被混淆
-keepattributes Signature
# 保持反射不被混淆
-keepattributes EnclosingMethod
# 保持異常不被混淆
-keepattributes Exceptions
# 保持內部類不被混淆
-keepattributes Exceptions,InnerClasses
# 丟擲異常時保留程式碼行號
-keepattributes SourceFile,LineNumberTable

# --------------------------------------------預設保留區--------------------------------------------#
# 保持基本元件不被混淆
-keep public class * extends android.app.Fragment
-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

# 保持 Google 原生服務需要的類不被混淆
-keep public class com.google.vending.licensing.ILicensingService
-keep public class com.android.vending.licensing.ILicensingService

# Support包規則
-dontwarn android.support.**
-keep public class * extends android.support.v4.**
-keep public class * extends android.support.v7.**
-keep public class * extends android.support.annotation.**

# 保持 native 方法不被混淆
-keepclasseswithmembernames class * {
    native <methods>;
}

# 保留自定義控制元件(繼承自View)不被混淆
-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);
}

# 保留在Activity中的方法引數是view的方法(避免佈局檔案裡面onClick被影響)
-keepclassmembers class * extends android.app.Activity {
    public void *(android.view.View);
}

# 保持列舉 enum 類不被混淆
-keepclassmembers enum * {
    public static **[] values();
    public static ** valueOf(java.lang.String);
}

# 保持R(資源)下的所有類及其方法不能被混淆
-keep class **.R$* { *; }

# 保持 Parcelable 序列化的類不被混淆(注:aidl檔案不能去混淆)
-keep class * implements android.os.Parcelable {
    public static final android.os.Parcelable$Creator *;
}

# 需要序列化和反序列化的類不能被混淆(注:Java反射用到的類也不能被混淆)
-keepnames class * implements java.io.Serializable

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

# 保持 BaseAdapter 類不被混淆
-keep public class * extends android.widget.BaseAdapter { *; }
# 保持 CusorAdapter 類不被混淆
-keep public class * extends android.widget.CusorAdapter{ *; }

# --------------------------------------------webView區--------------------------------------------#
# WebView處理,專案中沒有使用到webView忽略即可
# 保持Android與JavaScript進行互動的類不被混淆
-keep class **.AndroidJavaScript { *; }
-keepclassmembers class * extends android.webkit.WebViewClient {
     public void *(android.webkit.WebView,java.lang.String,android.graphics.Bitmap);
     public boolean *(android.webkit.WebView,java.lang.String);
}
-keepclassmembers class * extends android.webkit.WebChromeClient {
     public void *(android.webkit.WebView,java.lang.String);
}

# 網路請求相關
-keep public class android.net.http.SslError

# --------------------------------------------刪除程式碼區--------------------------------------------#
# 刪除程式碼中Log相關的程式碼
-assumenosideeffects class android.util.Log {
    public static boolean isLoggable(java.lang.String, int);
    public static int v(...);
    public static int i(...);
    public static int w(...);
    public static int d(...);
    public static int e(...);
}


# --------------------------------------------可定製化區--------------------------------------------#
#---------------------------------1.實體類---------------------------------



#--------------------------------------------------------------------------

#---------------------------------2.與JS互動的類-----------------------------



#--------------------------------------------------------------------------

#---------------------------------3.反射相關的類和方法-----------------------



#--------------------------------------------------------------------------

#---------------------------------2.第三方依賴--------------------------------



#--------------------------------------------------------------------------


複製程式碼

相關文章