ProGuard技術詳解
目錄
一.簡介
ProGuard是一個開源的Java程式碼混淆器,在Android中一提起ProGuard,我們通常第一想到的是用來混淆程式碼的,其實它的功能並不僅限於此,有以下四個功能:
(1)壓縮(Shrink):偵測並移除程式碼中無用的類、欄位、方法和特性
(2)優化(Optimize):對位元組碼進行優化,移除無用的指令
(3)混淆(Obfuscate):使用a,b,c,d這樣簡短而無意義的名稱,對類、欄位和方法進行重新命名
(4)預檢(Preverify):在Java平臺上對處理後的程式碼進行預檢
總結:
ProGuard是一個Java類檔案收縮器,優化器,混淆器和預校驗器。 收縮步驟檢測並刪除未使用的類,欄位,方法和屬性。 優化步驟分析和優化方法的位元組碼。 混淆步驟使用短無意義的名稱重新命名剩餘的類,欄位和方法。
二.工作原理
ProGuard由Shrink、Optimize、Obfuscate、Preverify四個步驟組成,每個步驟都是可選的,我們可以通過配置指令碼決定執行其中的哪幾個步驟
官網上介紹為了能夠確定哪些程式碼必須被保留以及哪些程式碼可以被丟棄或模糊處理,就必須指定一個或多個程式碼入口點。
這個入口點就是Entry Point.Entry Point 是在ProGuard過程中不會被處理的類或方法。在壓縮的步驟中,ProGuard會從上述的Entry Point開始遞迴遍歷,搜尋哪些類和類的成員在使用。對於沒有被使用的類和類的成員,就會在壓縮階段丟棄。在優化階段,那些非EntryPoint的類、方法都會被設定為private、static或final,不使用的引數會被移除。在混淆的階段,ProGuard會對非EntryPoint的類和方法進行重新命名。
三.如何編寫ProGuard檔案
3.1 基本混淆
基本混淆是指任何APP都會使用,可以直接拿來做模板使用
3.1.1基本指令
# 程式碼混淆壓縮比,在0和7之間,預設為5,一般不需要改
-optimizationpasses 5
# 混淆時不使用大小寫混合,混淆後的類名為小寫,Windows使用者必須指定,否則當你的專案中有超過26個類的
#話,ProGuard就會預設混用大小寫檔名,而導致class檔案相互覆蓋。
-dontusemixedcaseclassnames
# 指定不去忽略非公共的庫和類,不要跳過對非公開類的處理,預設情況下是跳過的
-dontskipnonpubliclibraryclasses
# 指定不去忽略非公共的庫的類的成員
-dontskipnonpubliclibraryclassmembers
# 不做預校驗,preverify是proguard的4個步驟之一
# Android不需要preverify,去掉這一步可加快混淆速度
-dontpreverify
# 有了verbose這句話,混淆後就會生成對映檔案
# 包含有類名->混淆後類名的對映關係
# 然後使用printmapping指定對映檔案的名稱
-verbose
-printmapping proguardMapping.txt
# 指定混淆時採用的演算法,後面的引數是一個過濾器
# 這個過濾器是谷歌推薦的演算法,一般不改變
-optimizations !code/simplification/arithmetic,!field/*,!class/merging/*
# 保護程式碼中的Annotation不被混淆,這在JSON實體對映時非常重要,比如fastJson
-keepattributes *Annotation*
# 避免混淆泛型,這在JSON實體對映時非常重要,比如fastJson
-keepattributes Signature
//丟擲異常時保留程式碼行號,在異常分析中可以方便定位
-keepattributes SourceFile,LineNumberTable
3.1.2 需要保留的東西
# 保留所有的本地native方法不被混淆
-keepclasseswithmembernames class * {
native <methods>;
}
# 保留了繼承自Activity、Application這些類的子類
# 因為這些子類,都有可能被外部呼叫
# 比如說,第一行就保證了所有Activity的子類不要被混淆
-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
-keep public class * extends android.view.View
-keep public class com.android.vending.licensing.ILicensingService
# 如果有引用android-support-v4.jar包,可以新增下面這行
-keep public class com.xxxx.app.ui.fragment.** {*;}
# 保留在Activity中的方法引數是view的方法,
# 從而我們在layout裡面編寫onClick就不會被影響
-keepclassmembers class * extends android.app.Activity {
public void *(android.view.View);
}
# 列舉類不能被混淆
-keepclassmembers enum * {
public static **[] values();
public static ** valueOf(java.lang.String);
}
# 保留自定義控制元件(繼承自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);
}
# 保留Parcelable序列化的類不被混淆
-keep class * implements android.os.Parcelable {
public static final android.os.Parcelable$Creator *;
}
# 保留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();
}
# 對於R(資源)下的所有類及其方法,都不能被混淆
-keep class **.R$* {
*;
}
# 對於帶有回撥函式onXXEvent的,不能被混淆
-keepclassmembers class * {
void *(**On*Event);
}
3.2 針對App的量身定製
3.2.1 保留類和成員不被混淆
實體類要保留set 和get方法,boolean 型別的get方法,有的是isXXX方法,不要遺漏了
-keep public class com.xxxx.entity.** {
public void set*(***);
public *** get*();
public *** is*();
}
Tip:最好是把多有的實體都放在一個包下進行管理,這樣只寫一次混淆就可以了,避免在別的包中新增的實體而忘記保留,如果忘記保留就會因找不到相應的實體類而崩潰。
3.2.2 內嵌類
內嵌類經常被混淆,結果在呼叫的時候為空就崩潰了。最好是把這個內嵌類拿出來,單獨成為一個類。
如果一定要內建,那麼這個類就必須在混淆時進行保留,比如com.example.xxx包下的MainActivity,它有一些內嵌類,以下指令保留MainActivity的所有內嵌類:
-keep class com.example.xxx.MainActivity$* { *; }
$符號是用來分割內嵌類與其母體的標誌。
3.2.3 對WebView的處理
# 對WebView的處理
-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, java.lang.String)
}
3.2.4 對JavaScript的處理
-keepclassmembers class com.example.xxx.MainActivity$JSInterface {
<methods>;
}
其中JSInterface是MainActivity內嵌的js方法類
3.2.5 處理反射
在程式中使用SomeClass.class.method這樣的靜態方法,SomeClass會在壓縮過程中被保留。對於Class.forName(“SomeClass”) ,SomeClass不會在壓縮過程中被移除,ProGuard會檢查程式中使用的Class.forName方法,對引數SomeClass這樣的字串不會移除。但是在混淆階段,無論是Class.forName(“SomeClass”),還是SomeClass.class,就都不能矇混過關了。因此要在ProGuard.cfg檔案中保留這個類名稱。
Class.forName("SomeClass")
SomeClass.class
SomeClass.class.getField("someField")
SomeClass.class.getDeclaredField("someField")
SomeClass.class.getMethod("someMethod", new Class[] {})
SomeClass.class.getMethod("someMethod", new Class[] { A.class })
SomeClass.class.getMethod("someMethod", new Class[] { A.class, B.class })
SomeClass.class.getDeclaredMethod("someMethod", new Class[] {})
SomeClass.class.getDeclaredMethod("someMethod", new Class[] { A.class })
SomeClass.class.getDeclaredMethod("someMethod", new Class[] { A.class, B.class })
AtomicIntegerFieldUpdater.newUpdater(SomeClass.class, "someField")
AtomicLongFieldUpdater.newUpdater(SomeClass.class, "someField")
AtomicReferenceFieldUpdater.newUpdater(SomeClass.class, SomeType.class, "someField")
在混淆的時候,將專案中以上相應的類或方法的名稱進行保留而不被混淆
3.2.6 自定義view
對於在layout目錄下的xml佈局檔案中配置的自定義view,都不能進行混淆,要遍歷layout下所有的xml佈局檔案,找到自定義view,然後確認其是否在proguard檔案中保留了。
3.3 針對第三方jar包的解決方法
一般而言,第三方提供的sdk都是經過混淆的了,我們要做的是避免這些sdk的類和方法在我們的app中被混淆。
3.3.1 android-support-v4.jar
# 針對android-support-v4.jar的解決方案
-libraryjars libs/android-support-v4.jar
-dontwarn android.support.v4.**
-keep class android.support.v4.** { *; }
-keep interface android.support.v4.app.** { *; }
-keep public class * extends android.support.v4.**
-keep public class * extends android.app.Fragment
3.3.2 其他第三方jar包的解決方案
一般第三方的sdk中有關於混淆的說明文字。如支付寶相關混淆規則:
-libraryjars libs/alipaysdk.jar
-dontwarn com.alipay.android.app.**
-keep public class com.alipay.** { *; }
這裡不做過多描述,為了避免有SDK遺漏,沒有進行混淆處理,一個好的辦法是 開啟libs目錄,看看有多少個jar包,每個都進行類似的處理,需要注意的是,不是每個第三方SDK都需要 -dontwarn指令,這取決於混淆時第三方SDK是否會出現警告,需要的時候再加上。
3.4 其他注意事項
3.4.1 如何確保混淆不會對專案產生影響
如果在專案的一開始就進行了混淆工作,那麼
(1)測試工作要基於混淆包進行,才能儘早發現問題
(2)每天開發團隊的冒煙測試,也要基於混淆包
(3)釋出前,重點的功能和模組要額外的測試,包括推送,分享,打賞
3.4.2 打包時忽略警告
當在匯出時,發現很多could not reference class之類的warning資訊,如果確認APP在執行中和那些引用沒有什麼關係的話,可以新增-dontwarn 標籤,就不會再提示這些warning資訊了。如-dontwarn org.apache.**,不要使用-ignorewarnings語句,它會忽略所有警告,這會有很大的潛在風險。
3.4.3 對於自定義類庫的混淆處理
我們需要對自定義的類庫進行混淆,然後在主專案的混淆檔案中保留自定義類庫中的類和類的成員
3.4.4 使用annotation避免混淆
另一種避免類或者屬性被混淆的方式是使用annotation,在需要保留的類上加如下語法:
@keep
@keepPublicGetterSetters
public class Bean{
}
這種方式多出現在fastJSON的使用上。
3.4.5 在專案中指定混淆檔案
在專案中有一個project.properties檔案,其中有寫下面一句話,就可以確保每次手動打包生成的apk是混淆過的:proguard.config = proguard.cfg ,其中proguard.cfg是混淆檔案的名稱
四.總結
本文介紹了proguard的工作原理,基本設定及其他情況下的設定,有不足之處可以提出來,希望我們一起進步。