前言
受《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開發基礎點進行整理,羅列下已經總結的文章,從中可以看到技術積累的過程。
1,
Android系統簡介
2,
ProGuard代碼混淆
6,
EventBus初理解
14,
Android 動畫的理解
16,
Android IPC 機制
17,
View 的事件體系
18,
View 的工作原理
20,
Activity 啟動過程分析
21,
Service 啟動過程分析
22,
Android 性能優化
23,
Android 消息機制
24,
Android Bitmap相關
25,
Android 線程和線程池
28,
Android 觸摸事件機制
29,
Android 事件機制應用
30,
Cordova 框架的一些理解
31,
有關 Android 插件化思考
32,
開發人員必備技能——單元測試