Android 開發應該掌握的 Proguard 技巧
本文由
玉剛說寫作平台
提供寫作贊助原作者:Sen
版權聲明:本文版權歸微信公眾號玉剛說
所有,未經許可,不得以任何形式轉載
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,一般不做修改 5
#把混淆類中的方法名也混淆了
-useuniqueclassmembernames#優化時允許訪問並修改有修飾符的類和類的成員
-allowaccessmodification# 避免混淆內部類、泛型、匿名類
-keepattributes InnerClasses,Signature,EnclosingMethod#拋出異常時保留代碼行號
-keepattributes SourceFile,LineNumberTable#重命名拋出異常時的文件名稱為"SourceFile"
-renamesourcefileattribute SourceFile#保持所有實現 Serializable 介面的類成員
-keepclassmembersclass
*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 publicclass
*extends
android
.app
.Activity
-keep public
class
*extends
android
.app
.Appliction
-keep publicclass
*extends
android
.app
.
Service
-keep publicclass
*extends
android
.content
.BroadcastReceiver
-keep publicclass
*extends
android
.content
.ContentProvider
-keep publicclass
*extends
android
.app
.backup
.BackupAgentHelper
-keep public
class
*extends
android
.preference
.Preference
#保留support下的所有類及其內部類
-keepclass
android
.support
.** {*;}# 保留繼承的support類
-keep publicclass
*extends
android
.support
.v4
.**-keep publicclass
*extends
android
.support
.v7
.**-keep publicclass
*extends
android
.support
.annotation
.**#保留我們自定義控制項(繼承自View)不被混淆
-keep publicclass
*extends
android
.view
.View
{ *** get*(); void set*(***); public#Fragment不需要在AndroidManifest.xml中註冊,需要額外保護下
-keep publicclass
*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,不要忘記保留。
public class com dev example entity -keep
public
voidset
*(***);public
***get
*();public
***is
*(); }如果所有的實體類在一個包下的話,上面的配置只用寫一遍就可以了。可是實際中我們更多的是以業務來劃分包名的,於是我們還可以這樣配置(實體類的類名一定要含有"Model")
public class Model-keep
public
voidset
*(***);public
***get
*();public
***is
*();}2. 對內部類的處理
如果項目中使用了內部類,要對其進行保留。
1.
保留寫在某個類裡面的所有內部類
。下面表示寫在類A裡面的內部類都會被保留($符號是用來分割內部類與其母體的標誌),什麼意思呢,比如類A裡面有一個內部類B,而B裡面也有個內部類C,這時,B和C都會被保留,以此類推,對多重嵌套的情況,都會被保留(當然我們寫代碼也不會寫出這麼深層級的內部類出來),這裡的內部類包含靜態內部類,非靜態內部類,不包含匿名內部類,如果是匿名內部類,只會保留其方法和成員變數(其繼承的類或實現的介面的名字會被混淆),另外如果對應的類被保留,在該類裡面定義的介面也會被保留,{*;}匹配該類裡面的所有部分。class com dev example A$-keep
2.
保留寫在某個內部類裡面所有的內部類
。這話聽著有點繞口,舉個例子,類A裡面有個內部類B,下面表示寫在類B裡面的內部類都會被保留。此時,類B像上面第一點所舉得類A一樣,有點遞歸意思在裡面。還有就是此時類B的名字不會被混淆,但裡面的方法和成員變數會被混淆,如果其它地方沒有對類B的方法和成員變數進行保留的話。class com dev example A$B$-keep
3. 對webView進行處理
class fqcn of javascript interface for webview -keepclassmembers
public
*;}-keepclassmembersclass
*extends
android
.webkit
.webViewClient
{public
void
*(android.webkit.WebView, java.lang.String, android.graphics.Bitmap);public
boolean
*(android.webkit.WebView, java.lang.String);}-keepclassmembersclass
*extends
android
.webkit
.webViewClient
{public
void
*(android.webkit.webView, jav.lang.String);}4. 保留js調用的原生方法
如果我們的app中涉及到和h5交互,需要保留js調用的原生方法。
# Keep JavascriptInterface class
5. 對含有反射類的處理
有時候項目中有些類不是實體類,但仍然用到反射功能,如Class.forName("xxx"),這是我們需要保留的。比如這些類在com.dev.example包下,可以通過下面的配置進行保留。
class com dev example-keep
另外上面只是保留了該包下的類,如果該包下還有子包,則子包的類仍然會被混淆,如果想保留該包下子包的類,我們可以如下配置(**能匹配本包和所含子包,其中子包也可以含有子包)
class com dev example-keep
6. 常見的自定義的配置
1.保留某個特定的類
#保留Test類 public class com dev example Test
2.保留某個類的子類
#保留繼承了AbstractClass的子類 class extends com dev example AbstractClass
3.保留介面的實現類
#保留實現了Callable介面的類 class implements Callable
4.保留類的特定部分保留TaskRepository類的所有構造方法,變數和普通方法。
class com dev example TaskRepository-keep
//匹配所有構造器
//匹配所有域
//匹配所有方法
}還可以保留的更具體一點,如下所示
// 保留該類的修飾符是public且有一個參數(類型是String)的構造方法 public // 保留該類的所有修飾符是public且返回類型void的方法 public void // 保留該類的具體某一個方法 public getUserName ()-keepclassmembers com.dev.example.TaskRepository{
7. 對於第三方依賴庫的處理
下面給出幾個例子,用到具體第三發依賴庫的時候,對應的文檔會給出相應配置的。
#okhttp class com squareup okhttp3
#retroift
-dontwarn retrofit2.**-keepclass
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.DoNotStripclass
*-keepclassmembersclass
* { @com.facebook.common.internal.DoNotStrip *;}#rx
-dontwarn rx.**-keepclass
rx
.** { *;}#keep GSON stuff
-keepclass
sun
.misc
.Unsafe
{ *; }-keepclass
com
.gson
.** { *; }#ButterKnife
-keepclass
butterknife
.** { *; }-dontwarn butterknife.internal.**-keepclass
**$$ViewBinder
{ *; }-keepclasseswithmembernamesclass
* { @butterknife.*class
* { @butterknife.*#enventbus
-keepclass
org
.greenrobot
.eventbus
.** { *;}-dontwarn org.greenrobot.eventbus.**-keepclassmembersclass
** {另外說一下 public void onEvent*(**);}# Bugly
-dontwarn com.tencent.bugly.**-keep publicclass
com
.tencent
.bugly
.**{*;}# aliyun push
-keepclasseswithmembernamesclass
** { native# QQ share SDK
-dontwarn com.tencent.**-keepnamesclass
com
.tencent
.** {*;}# sina share SDK
-dontwarn com.sina.**-keepnamesclass
com
.sina
.** {*;}# umeng SDK
-keep publicclass
*extends
com
.umeng
.**-dontwarn com.umeng.**-keepclass
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位元組碼文件進行操作的,有時我們還想對資源文件進行混淆,比較成熟的是微信的資源混淆文件方案,由於本次討論的重點不是這個,不再多說。附上該項目的鏈接:https://github.com/shwenzhang/AndResGuard
檢查混淆和追蹤異常
開啟Proguard功能,則每次構建時 ProGuard 都會輸出下列文件:
dump.txt說明 APK 中所有類文件的內部結構。
mapping.txt提供原始與混淆過的類、方法和欄位名稱之間的轉換。
seeds.txt列出未進行混淆的類和成員。
usage.txt列出從 APK 移除的代碼。
這些文件保存在
retrace [-verbose] mapping.txt [
例如:
retrace .bat -verbose mapping .txt obfuscated_trace .txt
這篇文章參考了Proguard相關文檔和幾篇寫的好的博客,旨在介紹在Android中Proguard的使用,以及解釋大家在理解Proguard中可能會遇到的一些點,希望能有所幫助。
— — — END — — —
近期文章回顧
一念天堂 一念地獄Android 組件化最佳實踐Android 開發應該嘗試的 UI 自動化測試
推薦閱讀:
※書單丨十年發展,這5本書帶你開啟Android系統探索之旅
※安卓越用越卡是通病嗎?細數安卓的固有成見
※能讓機器搞定的事情,就別浪費力氣啦 – Automate #Android
※Android滲透測試學習手冊(一)Android 安全入門
※【搞機錄】努比亞Nubia Z17安裝谷歌框架服務Google Service-L2/E17