ProGuard程式碼混淆技術詳解
阿新 • • 發佈:2018-12-31
前言
受《APP研發錄》啟發,裡面講到一名Android程式設計師,在工作一段時間後,會感覺到迷茫,想進階的話接下去是看Android系統原始碼呢,還是每天繼續做應用,畢竟每天都是畫UI和利用MobileAPI處理Json還是蠻無聊的,做著重複的事情,沒有技術的上提升空間的。所以,根據裡面提到的Android應用開發人員所需要精通的20個技術點,寫篇文章進行總結,一方面是梳理下基礎知識和鞏固知識,另一方面也是彌補自我不足之處。 那麼,今天就來講講ProGuard程式碼混淆的相關技術知識點。內容目錄
- ProGuard簡介
- ProGuard工作原理
- 如何編寫一個ProGuard檔案
- 其他注意事項
- 小結
ProGuard簡介
因為Java程式碼是非常容易反編碼的,況且Android開發的應用程式是用Java程式碼寫的,為了很好的保護Java原始碼,我們需要對編譯好後的class檔案進行混淆。 ProGuard是一個混淆程式碼的開源專案,它的主要作用是混淆程式碼,殊不知ProGuard還包括以下4個功能。- 壓縮(Shrink):檢測並移除程式碼中無用的類、欄位、方法和特性(Attribute)。
- 優化(Optimize):對位元組碼進行優化,移除無用的指令。
- 混淆(Obfuscate):使用a,b,c,d這樣簡短而無意義的名稱,對類、欄位和方法進行重新命名。
- 預檢(Preveirfy):在Java平臺上對處理後的程式碼進行預檢,確保載入的class檔案是可執行的。
ProGuard工作原理
ProGuar由shrink、optimize、obfuscate和preveirfy四個步驟組成,每個步驟都是可選的,我們可以通過配置指令碼來決定執行其中的哪幾個步驟。如何編寫一個ProGuard檔案
有個三步走的過程:- 基本混淆
- 針對APP的量身定製
- 針對第三方jar包的解決方案
基本混淆
混淆檔案的基本配置資訊,任何APP都要使用,可以作為模板使用,具體如下。 1,基本指令# 程式碼混淆壓縮比,在0和7之間,預設為5,一般不需要改 -optimizationpasses 5 # 混淆時不使用大小寫混合,混淆後的類名為小寫 -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 -dontskipnonpubliclibraryclasses用於告訴ProGuard,不要跳過對非公開類的處理。預設情況下是跳過的,因為程式中不會引用它們,有些情況下人們編寫的程式碼與類庫中的類在同一個包下,並且對包中內容加以引用,此時需要加入此條宣告。 -dontusemixedcaseclassnames,這個是給Microsoft Windows使用者的,因為ProGuard假定使用的作業系統是能區分兩個只是大小寫不同的檔名,但是Microsoft Windows不是這樣的作業系統,所以必須為ProGuard指定-dontusemixedcaseclassnames選項
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); }
針對APP的量身定製
1,保留實體類和成員被混淆 對於實體,保留它們的set和get方法,對於boolean型get方法,有人喜歡命名isXXX的方式,所以不要遺漏。如下:# 保留實體類和成員不被混淆 -keep public class com.xxxx.entity.** { public void set*(***); public *** get*(); public *** is*(); }
一種好的做法是把所有實體都放在一個包下進行管理,這樣只寫一次混淆就夠了,避免以後在別的包中新增的實體而忘記保留,程式碼在混淆後因為找不到相應的實體類而崩潰。
2,內嵌類
內嵌類經常會被混淆,結果在呼叫的時候為空就崩潰了,最好的解決方法就是把這個內嵌類拿出來,單獨成為一個類。如果一定要內建,那麼這個類就必須在混淆的時候保留,比如如下:
# 保留內嵌類不被混淆 -keep class com.example.xxx.MainActivity$* { *; }
這個$符號就是用來分割內嵌類與其母體的標誌。
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) }
4,對JavaScript的處理
# 保留JS方法不被混淆 -keepclassmembers class com.example.xxx.MainActivity$JSInterface1 { <methods>; }
其中JSInterface是MainActivity的子類
5,處理反射
在程式中使用SomeClass.class.method這樣的靜態方法,在ProGuard中是在壓縮過程中被保留的,那麼對於Class.forName("SomeClass")呢,SomeClass不會被壓縮過程中移除,它會檢查程式中使用的Class.forName方法,對引數SomeClass法外開恩,不會被移除。但是在混淆過程中,無論是Class.forName("SomeClass"),還是SomeClass.class,都不能矇混過關,SomeClass這個類名稱會被混淆,因此,我們要在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")
在混淆的時候,要在專案中搜索一下上述方法,將相應的類或者方法的名稱進行保留而不被混淆。
6,對於自定義View的解決方案 但凡在Layout目錄下的XML佈局檔案配置的自定義View,都不能進行混淆。為此要遍歷Layout下的所有的XML佈局檔案,找到那些自定義View,然後確認其是否在ProGuard檔案中保留。有一種思路是,在我們使用自定義View時,前面都必須加上我們的包名,比如com.a.b.customeview,我們可以遍歷所有Layout下的XML佈局檔案,查詢所有匹配com.a.b的標籤即可。針對第三方jar包的解決方案
我們在Android專案中不可避免要使用很多第三方提供的SDK,一般而言,這些SDK是經過ProGuard混淆的,而我們所需要做的就是避免這些SDK的類和方法在我們APP被混淆。 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
2,其他的第三方jar包的解決方案
這個就取決於第三方包的混淆策略了,一般都有在各自的SDK中有關於混淆的說明文字,比如支付寶如下:
# 對alipay的混淆處理 -libraryjars libs/alipaysdk.jar -dontwarn com.alipay.android.app.** -keep public class com.alipay.** { *; }值得注意的是,不是每個第三方SDK都需要-dontwarn 指令,這取決於混淆時第三方SDK是否出現警告,需要的時候再加上。
其他注意事項
當然在使用ProGuard過程中,還有一些注意的事項,如下。 1,如何確保混淆不會對專案產生影響- 測試工作要基於混淆包進行,才能儘早發現問題
- 每天開發團隊的冒煙測試,也要基於混淆包
- 發版前,重點的功能和模組要額外的測試,包括推送,分享,打賞