Android開發應該掌握的Proguard技巧
Proguard介紹
Proguard被人們熟知的是它的混淆功能,根據Proguard幫助文件的描述,Proguard可以對Java class 檔案進行shrink,optimize,obfuscate和preveirfy。obfuscate(混淆)只是其中之一。簡要的介紹下這四個功能:
壓縮(Shrink): 檢測和刪除沒有使用的類,欄位,方法和特性
優化(Optimize) : 分析和優化Java位元組碼
混淆(Obfuscate): 使用簡短的無意義的名稱,對類,欄位和方法進行重新命名
預檢(Preveirfy): 用來對Java class進行預驗證(預驗證主要是針對JME開發來說的,Android中沒有預驗證過程,預設是關閉)
補充說明:根據proguard-android-optimize.txt對optimize的描述,在Android中使用該功能是有潛在風險的,並不能保證在所有版本的Dalvik虛擬機器上正常執行,該選項預設是關閉的,如果開啟,請做好全面的測試。在Android專案中,我們在相應module下的build.gradle檔案中會看到
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile(`proguard-android.txt`), `proguard-rules.pro`
}
}
其中 minifyEnabled 為true是開啟Proguard的功能,false是關閉。
Proguard工作流程
Prouguard的工作流程如下圖所示:
可以看出, Proguard工作流程是對輸入的jars經過shrink->optimize->obfuscate->preveirfy依次處理,圖中library jars是input jars執行所依賴的包,比如Java執行時的rt.jar,Android執行時android.jar,這些jars在上述處理過程中不會有任何改變,僅是作為輸入jars的依賴。
大家可能會有一個疑問,Proguard是怎麼知道哪些類,方法,成員變數等是無用的呢,這就要說到Entry Point(入口點),我們在配置檔案(包括預設的proguard-android.txt)中寫入的一系列-keep選項,都會作為Entry Point,Proguard把這些Entry Points作為搜尋的入口,進行遞迴檢索,以此來確定哪些部分未使用到。類似於hotspot虛擬機器對可回收物件的判定,從GC Roots出發,進行可達性的判斷,不可達的為可回收物件。Entry Points非常重要,Proguard的壓縮,優化,混淆功能是以Entry Point作為依據的(預檢不需要以此為依據)。
在壓縮過程中,Proguard從Entry Points出發,遞迴檢索,刪除那些沒有使用到的類和類的成員,在接下來的優化過程中,那些非Entry Points的類和方法會被設定成private,static或final,沒有使用到的引數會被移除,有些方法可能會被標記為內聯的,在混淆過程中,會對非EntryPoint的類和類的成員進行重新命名,也就是用其它無意義的名稱代替。我們在配置檔案中用-keep保留的部分屬於Entry Point,所以不會被重新命名。
Proguard配置檔案的依據
說起重新命名,為什麼需要保留一些類和類的成員(方法和變數)不被重新命名呢 ? 原因是Proguard對class檔案經過一系列處理後,能保證功能上和原來是一樣的,但有些情況它卻不能良好的處理,比如我們程式碼中有些功能依賴於它們原來的名字,如反射功能,native呼叫(函式簽名)等,如果換成其它名字,會出現找不到,不對應的情況,可能引起程式崩潰,或者我們的對外提供了一些功能,必須保持原來的名字,才能保證其它依賴這些功能的模組能正確的執行等。
這就是我們為什麼要配置-keep選項的原因之一,還有一個原因是我們要用-keep告訴Proguard程式的入口(帶有-keep的選項都會作為Entry Point),以此來確定哪些是未被使用的類和類的成員,方法等,並刪除它們,因此,我們要針對我們的專案配置對應的選項。當然Proguard不僅提供了-keep選項,還有一些其它配置選項,比如-dontoptimize 對輸入的Java class 檔案不進行優化處理,-verbose 生成混淆後的對映檔案等。下面介紹一下app中proguard檔案的常用配置和專案中可能會用到的指令。更多詳細的用法,可以參考Proguard幫助文件。
編寫Proguard配置檔案
第1條是可以作為Android App的配置模板的(預設的proguard-android.txt檔案裡的配置沒有列舉出來),基本所有的app都會用到。
通用配置
#程式碼混淆壓縮比,在0~7之間,預設為5,一般不做修改
-optimizationpasses 5
#把混淆類中的方法名也混淆了
-useuniqueclassmembernames
#優化時允許訪問並修改有修飾符的類和類的成員
-allowaccessmodification
# 避免混淆內部類、泛型、匿名類
-keepattributes InnerClasses,Signature,EnclosingMethod
#丟擲異常時保留程式碼行號
-keepattributes SourceFile,LineNumberTable
#重新命名丟擲異常時的檔名稱為"SourceFile"
-renamesourcefileattribute SourceFile
#保持所有實現 Serializable 介面的類成員
-keepclassmembers class * implements java.io.Serializable {
static final long serialVersionUID;
private static final java.io.ObjectStreamField[] serialPersistentFields;
private void writeObject(java.io.ObjectOutputStream);
private void readObject(java.io.ObjectInputStream);
java.lang.Object writeReplace();
java.lang.Object readResolve();
}
#保留我們使用的四大元件,自定義的Application等等這些類不被混淆
#因為這些子類都有可能被外部呼叫
-keep public class * extends android.app.Activity
-keep public class * extends android.app.Appliction
-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
#保留support下的所有類及其內部類
-keep class android.support.** {*;}
# 保留繼承的support類
-keep public class * extends android.support.v4.**
-keep public class * extends android.support.v7.**
-keep public class * extends android.support.annotation.**
#保留我們自定義控制元件(繼承自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);
}
#Fragment不需要在AndroidManifest.xml中註冊,需要額外保護下
-keep public class * extends android.app.Fragment
# 保持測試相關的程式碼
-dontnote junit.framework.**
-dontnote junit.runner.**
-dontwarn android.test.**
-dontwarn android.support.test.**
-dontwarn org.junit.**
下面是針對我們App的配置。
1. 實體類需要保留
我們需要保留實體類的get和set方法(反射會用到),boolean型別的get方法是isXXX,不要忘記保留。
-keep public class com.dev.example.entity.** {
public void set*(***);
public *** get*();
public *** is*();
}
如果所有的實體類在一個包下的話,上面的配置只用寫一遍就可以了。可是實際中我們更多的是以業務來劃分包名的,於是我們還可以這樣配置(實體類的類名一定要含有”Model”)
<pre style=”margin: 0px; padding: 0px; border: 0px; font-style: inherit; font-variant: inherit; font-weight: inherit; font-stretch: inherit; font-size: inherit; line-height: inherit; font-family: inherit; vertical-align: baseline; word-break: break-word;”>
-keep public class **.*Model*.** {
public void set*(***);
public *** get*();
public *** is*();
}
2. 對內部類的處理
如果專案中使用了內部類,要對其進行保留。
- 保留寫在某個類裡面的所有內部類。下面表示寫在類A裡面的內部類都會被保留($符號是用來分割內部類與其母體的標誌),什麼意思呢,比如類A裡面有一個內部類B,而B裡面也有個內部類C,這時,B和C都會被保留,以此類推,對多重巢狀的情況,都會被保留(當然我們寫程式碼也不會寫出這麼深層級的內部類出來),這裡的內部類包含靜態內部類,非靜態內部類,不包含匿名內部類,如果是匿名內部類,只會保留其方法和成員變數(其繼承的類或實現的介面的名字會被混淆),另外如果對應的類被保留,在該類裡面定義的介面也會被保留,{*;}匹配該類裡面的所有部分。
-keep class com.dev.example.A$* { *; }
- 保留寫在某個內部類裡面所有的內部類。這話聽著有點繞口,舉個例子,類A裡面有個內部類B,下面表示寫在類B裡面的內部類都會被保留。此時,類B像上面第一點所舉得類A一樣,有點遞迴意思在裡面。還有就是此時類B的名字不會被混淆,但裡面的方法和成員變數會被混淆,如果其它地方沒有對類B的方法和成員變數進行保留的話。
-keep class com.dev.example.A$B$* { *; }
3. 對webView進行處理
-keepclassmembers class fqcn.of.javascript.interface.for.webview {
public *;
}
-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.webViewClient {
public void *(android.webkit.webView, jav.lang.String);
}
4. 保留js呼叫的原生方法
如果我們的app中涉及到和h5互動,需要保留js呼叫的原生方法。
# Keep JavascriptInterface
-keepclassmembers class ** {
@android.webkit.JavascriptInterface public *;
}
5. 對含有反射類的處理
有時候專案中有些類不是實體類,但仍然用到反射功能,如Class.forName(“xxx”),這是我們需要保留的。比如這些類在com.dev.example包下,可以通過下面的配置進行保留。
-keep class com.dev.example.* { *; }
另外上面只是保留了該包下的類,如果該包下還有子包,則子包的類仍然會被混淆,
如果想保留該包下子包的類,我們可以如下配置(**能匹配本包和所含子包,其中子包也可以含有子包)
-keep class com.dev.example.**{ *; }
6. 常見的自定義的配置
1.保留某個特定的類
#保留Test類
-keep public class com.dev.example.Test { *; }
2.保留某個類的子類
#保留繼承了AbstractClass的子類
-keep class * extends com.dev.example.AbstractClass{*;}
3.保留介面的實現類
#保留實現了Callable介面的類
-keep class * implements Callable{*;}
4.保留類的特定部分
保留TaskRepository類的所有構造方法,變數和普通方法。
-keep class com.dev.example.TaskRepository{
<init>; //匹配所有構造器
<fields>; //匹配所有域
<methods>; //匹配所有方法
}
還可以保留的更具體一點,如下所示
-keepclassmembers com.dev.example.TaskRepository{
// 保留該類的修飾符是public且有一個引數(型別是String)的構造方法
public <init>(java.lang.String);
// 保留該類的所有修飾符是public且返回型別void的方法
public void *(**);
// 保留該類的具體某一個方法
public String getUserName();
}
7. 對於第三方依賴庫的處理
下面給出幾個例子,用到具體第三發依賴庫的時候,對應的文件會給出相應配置的。
#okhttp
-dontwarn com.squareup.okhttp.**
-dontwarn com.squareup.okhttp3.**
-keep class com.squareup.okhttp3.** { *;}
-dontwarn okio.**
#retroift
-dontwarn retrofit2.**
-keep class retrofit2.** { *; }
-keepattributes Signature
-keepattributes Exceptions
# fresco SDK
-keep,allowobfuscation @interface com.facebook.common.internal.DoNotStrip
# Do not strip any method/class that is annotated with @DoNotStrip
-keep @com.facebook.common.internal.DoNotStrip class *
-keepclassmembers class * {
@com.facebook.common.internal.DoNotStrip *;
}
#rx
-dontwarn rx.**
-keep class rx.** { *;}
#keep GSON stuff
-keep class sun.misc.Unsafe { *; }
-keep class com.google.gson.** { *; }
#ButterKnife
-keep class butterknife.** { *; }
-dontwarn butterknife.internal.**
-keep class **$ViewBinder { *; }
-keepclasseswithmembernames class * {
@butterknife.* <fields>;
}
-keepclasseswithmembernames class * {
@butterknife.* <methods>;
}
#enventbus
-keep class org.greenrobot.eventbus.** { *;}
-dontwarn org.greenrobot.eventbus.**
-keepclassmembers class ** {另外說一下
public void onEvent*(**);
}
# Bugly
-dontwarn com.tencent.bugly.**
-keep public class com.tencent.bugly.**{*;}
# aliyun push
-keepclasseswithmembernames class ** {
native <methods>;
}
# QQ share SDK
-dontwarn com.tencent.**
-keepnames class com.tencent.** {*;}
# sina share SDK
-dontwarn com.sina.**
-keepnames class com.sina.** {*;}
# umeng SDK
-keep public class * extends com.umeng.**
-dontwarn com.umeng.**
-keep class com.umeng.** { *; }
其它
還有關於多module專案的配置,一種方法是關閉子module的Proguard功能,在我們主app的proguard-rules.pro檔案中配置所有module的配置選項。這樣會使主app的proguard配置檔案變得比較雜亂,如果業務發展過程中,某個子module的功能不需要了,還要在主app的配置檔案中找到對應子module的配置,並刪除它們,不建議使用。另一種方式是各個module配置好自己的配置檔案,要注意的是,子module中制定配置檔案的方式如下所示:
buildTypes {
release {
consumerProguardFiles `proguard-rules.pro`
}
}
子module是通過consumerProguardFiles來指定配置檔案的,而不是proguardFiles。
在匯出包時,如果發現有很多could not reference class之類的warning資訊,確認app在執行時和這些warning沒有任何關係,可以配置-dontwarn選項,就不會提示這些warning資訊了。
到這裡Proguard配置部分基本已經說完了。Proguard是對class位元組碼檔案進行操作的,有時我們還想對資原始檔進行混淆,比較成熟的是微信的資源混淆檔案方案,由於本次討論的重點不是這個,不再多說。
檢查混淆和追蹤異常
開啟Proguard功能,則每次構建時 ProGuard 都會輸出下列檔案:
- dump.txt
- 說明 APK 中所有類檔案的內部結構。
- mapping.txt
- 提供原始與混淆過的類、方法和欄位名稱之間的轉換。
- seeds.txt
- 列出未進行混淆的類和成員。
- usage.txt
- 列出從 APK 移除的程式碼。
這些檔案儲存在/build/outputs/mapping/release/ 中。我們可以檢視seeds.txt裡面是否是我們需要保留的,以及usage.txt裡檢視是否有誤刪除的程式碼。mapping.txt檔案很重要,由於我們的部分程式碼是經過重新命名的,如果該部分出現bug,對應的異常堆疊資訊裡的類或成員也是經過重新命名的,難以定位問題。我們可以用 retrace 指令碼(在 Windows 上為 retrace.bat;在 Mac/Linux 上為 retrace.sh)。它位於/tools/proguard/ 目錄中。該指令碼利用 mapping.txt檔案和你的異常堆疊檔案生成沒有經過混淆的異常堆疊檔案,這樣就可以看清是哪裡出問題了。使用 retrace 工具的語法如下:
retrace.bat|retrace.sh [-verbose] mapping.txt [<stacktrace_file>]
例如:
retrace.bat -verbose mapping.txt obfuscated_trace.txt
這篇文章參考了Proguard相關文件和幾篇寫的好的部落格,旨在介紹在Android中Proguard的使用,以及解釋大家在理解Proguard中可能會遇到的一些點,希望能有所幫助。
最後
更多Android進階技術,面試資料系統整理分享,職業生涯規劃,產品,思維,行業觀察,談天說地。可以加Android架構師群;701740775。
相關文章
- Android 開發應該掌握的 Proguard 技巧Android
- WWDC 2017:高階開發應該掌握的自動佈局技巧
- 後端開發應該掌握的Redis基礎後端Redis
- Java之列舉, 程式設計師應該掌握的開發技巧“簡潔易懂又安全的程式碼”Java程式設計師
- [譯] 優秀 JavaScript 開發人員應掌握的 9 個技巧JavaScript
- 面試開發崗位,你應該知道的回答技巧!面試
- 中級Android開發應該瞭解的Binder原理Android
- Android小技巧:Android開發究竟該如何學習,年薪超過80萬!Android
- 2019 年,React 開發人員應該掌握的 22 種神奇工具React
- 【Camera專題】Qcom-你應該掌握的Camera除錯技巧一除錯
- Android混淆(Proguard)詳解Android
- 運用Kotlin開發Android應用的一些技巧KotlinAndroid
- 一名合格的前端開發工程師應該掌握的8個技能前端工程師
- Gradle:你必須掌握的開發常見技巧Gradle
- android開發技巧雜談Android
- Android開發人員應該知道的一些技術Android
- 重要!每個開發者都應該掌握的9個核心演算法演算法
- 前端應該掌握的nginx知識前端Nginx
- Android開發 - 掌握ConstraintLayout(五)偏差(Bias)AndroidAI
- Android開發 - 掌握ConstraintLayout(二)介紹AndroidAI
- 【譯】2019年JavaScript開發者應該都在用的9個棒的技巧JavaScript
- Android外掛化、熱補丁中繞不開的Proguard的坑Android
- Android開發 - 掌握ConstraintLayout(六)鏈條(Chains)AndroidAI
- Android開發 - 掌握ConstraintLayout(三)編輯器AndroidAI
- Android開發 - 掌握ConstraintLayout(九)分組(Group)AndroidAI
- 作為一名Java開發者應該掌握的基礎知識彙總!Java
- Android工程常用配置和開發技巧Android
- Android Proguard混淆對抗之我見Android
- Android開發 - 掌握ConstraintLayout(七)輔助線(Guideline)AndroidAIGUIIDE
- Android開發 - 掌握ConstraintLayout(四)建立基本約束AndroidAI
- Android開發 - 掌握ConstraintLayout(八)障礙線(Barrier)AndroidAI
- 一個自信的前端應該掌握的CDN操作前端
- 解放雙手 - Android 開發應該嘗試的 UI 自動化測試AndroidUI
- Android開發-掌握ConstraintLayout(一)傳統佈局的問題AndroidAI
- Android開發 - 掌握ConstraintLayout(一)傳統佈局的問題AndroidAI
- CSS Tricks - 你應該知道的 CSS 技巧CSS
- 雷達學習者應該掌握的MATLAB ToolboxMatlab
- ORACLE DBA應該掌握的9個免費工具Oracle