Proguard被人們熟知的是它的混淆功能,根據Proguard幫助文檔的描述,Proguard能夠對Java class 文件進行shrink,optimize,obfuscate和preveirfy。obfuscate(混淆)只是其中之一。簡要的介紹下這四個功能:javascript
壓縮(Shrink): 檢測和刪除沒有使用的類,字段,方法和特性java
優化(Optimize) : 分析和優化Java字節碼android
混淆(Obfuscate): 使用簡短的無心義的名稱,對類,字段和方法進行重命名web
預檢(Preveirfy): 用來對Java class進行預驗證(預驗證主要是針對JME開發來講的,Android中沒有預驗證過程,默認是關閉)微信
補充說明:根據proguard-android-optimize.txt對optimize的描述,在Android中使用該功能是有潛在風險的,並不能保證在全部版本的Dalvik虛擬機上正常運行,該選項默認是關閉的,若是開啓,請作好全面的測試。在Android項目中,咱們在相應module下的build.gradle文件中會看到app
buildTypes { release { minifyEnabled true proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } }
其中 minifyEnabled 爲true是開啓Proguard的功能,false是關閉。ide
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對class文件通過一系列處理後,能保證功能上和原來是同樣的,但有些狀況它卻不能良好的處理,好比咱們代碼中有些功能依賴於它們原來的名字,如反射功能,native調用(函數簽名)等,若是換成其它名字,會出現找不到,不對應的狀況,可能引發程序崩潰,或者咱們的對外提供了一些功能,必須保持原來的名字,才能保證其它依賴這些功能的模塊能正確的運行等。
這就是咱們爲何要配置-keep選項的緣由之一,還有一個緣由是咱們要用-keep告訴Proguard程序的入口(帶有-keep的選項都會做爲Entry Point),以此來肯定哪些是未被使用的類和類的成員,方法等,並刪除它們,所以,咱們要針對咱們的項目配置對應的選項。固然Proguard不只提供了-keep選項,還有一些其它配置選項,好比-dontoptimize 對輸入的Java class 文件不進行優化處理,-verbose 生成混淆後的映射文件等。下面介紹一下app中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中可能會遇到的一些點,但願能有所幫助。