前言
受《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是一個Java類檔案壓縮器、優化器、混淆器、預校驗器。壓縮環節會檢測以及移除沒有用到的類、欄位、方法以及屬性。優化環節會分析以及優化方法的位元組碼。混淆環節會用無意義的短變數去重新命名類、變數、方法。這些步驟讓程式碼更精簡,更高效,也更難被逆向(破解)。
值得注意的是,不是每個第三方SDK都需要-dontwarn 指令,這取決於混淆時第三方SDK是否出現警告,需要的時候再加上。
ProGuard工作原理
ProGuar由shrink、optimize、obfuscate和preveirfy四個步驟組成,每個步驟都是可選的,我們可以通過配置指令碼來決定執行其中的哪幾個步驟。
混淆就是移除沒有用到的程式碼,然後對程式碼裡面的類、變數、方法重新命名為人可讀性很差的簡短名字。
那麼有一個問題,ProGuard怎麼知道這個程式碼沒有被用到呢?
這裡引入一個Entry Point(入口點)概念,Entry Point是在ProGuard過程中不會被處理的類或方法。在壓縮的步驟中,ProGuard會從上述的Entry Point開始遞迴遍歷,搜尋哪些類和類的成員在使用,對於沒有被使用的類和類的成員,就會在壓縮段丟棄,在接下來的優化過程中,那些非Entry Point的類、方法都會被設定為private、static或final,不使用的引數會被移除,此外,有些方法會被標記為內聯的,在混淆的步驟中,ProGuard會對非Entry Point的類和方法進行重新命名。
那麼這個入口點怎麼來呢?就是從ProGuard的配置檔案來,只要這個配置了,那麼就不會被移除。
如何編寫一個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.** { *; }
其他注意事項
當然在使用ProGuard過程中,還有一些注意的事項,如下。
1,如何確保混淆不會對專案產生影響
- 測試工作要基於混淆包進行,才能儘早發現問題
- 每天開發團隊的冒煙測試,也要基於混淆包
- 發版前,重點的功能和模組要額外的測試,包括推送,分享,打賞
2,打包時忽略警告
當匯出包的時候,發現很多could not reference class之類的warning資訊,如果確認App在執行中和那些引用沒有什麼關係,可以新增-dontwarn 標籤,就不會提示這些警告資訊了
3,對於自定義類庫的混淆處理
比如我們引用了一個叫做AndroidLib的類庫,我們需要對Lib也進行混淆,然後在主專案的混淆檔案中保留AndroidLib中的類和類的成員。
4,使用annotation避免混淆
另一種類或者屬性被混淆的方式是,使用annotation,比如這樣:
@keep @keepPublicGetterSetters public class Bean{ public boolean booleanProperty; public int intProperty; public String stringProperty; }
5,在專案中指定混淆檔案
到最後,發現沒有介紹如何在專案中指定混淆檔案。在專案中有一個project.properties檔案,在其中寫這麼一句話,就可以確保每次手動打包生成的apk是混淆過的。
proguard.config=proguard.cfg
其中,proguard.cfg是混淆檔案的名稱。
小結
總之ProGuard是一個比較枯燥的過程,但Android專案沒有了ProGuard就真不行了,這樣可以保證我們開發出的APK可以更健壯,畢竟很多核心程式碼質量也算是一個APK的核心競爭力吧。
閱讀擴充套件
源於對掌握的Android開發基礎點進行整理,羅列下已經總結的文章,從中可以看到技術積累的過程。
17,View 的事件體系
18,View 的工作原理
22,Android 效能優化
23,Android 訊息機制