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:shrinkMode
為 strict
來開啟嚴格模式,使只有確實被使用的資源被保留。
以上就是自定義資源保持規則相關的配置,舉個例子:
<?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.第三方依賴--------------------------------
#--------------------------------------------------------------------------
複製程式碼