ProGuard程式碼混淆技術詳解

cryAllen發表於2016-05-27

前言   

    受《APP研發錄》啟發,裡面講到一名Android程式設計師,在工作一段時間後,會感覺到迷茫,想進階的話接下去是看Android系統原始碼呢,還是每天繼續做應用,畢竟每天都是畫UI和利用MobileAPI處理Json還是蠻無聊的,做著重複的事情,沒有技術的上提升空間的。所以,根據裡面提到的Android應用開發人員所需要精通的20個技術點,寫篇文章進行總結,一方面是梳理下基礎知識和鞏固知識,另一方面也是彌補自我不足之處。
    那麼,今天就來講講ProGuard程式碼混淆的相關技術知識點。

內容目錄

  • ProGuard簡介
  • ProGuard工作原理
  • 如何編寫一個ProGuard檔案
  • 其他注意事項
  • 小結

ProGuard簡介

因為Java程式碼是非常容易反編碼的,況且Android開發的應用程式是用Java程式碼寫的,為了很好的保護Java原始碼,我們需要對編譯好後的class檔案進行混淆。
ProGuard是一個混淆程式碼的開源專案,它的主要作用是混淆程式碼,殊不知ProGuard還包括以下4個功能。
  1. 壓縮(Shrink):檢測並移除程式碼中無用的類、欄位、方法和特性(Attribute)。
  2. 優化(Optimize):對位元組碼進行優化,移除無用的指令。
  3. 混淆(Obfuscate):使用a,b,c,d這樣簡短而無意義的名稱,對類、欄位和方法進行重新命名。
  4. 預檢(Preveirfy):在Java平臺上對處理後的程式碼進行預檢,確保載入的class檔案是可執行的。
總而言之,根據官網的翻譯:Proguard是一個Java類檔案壓縮器、優化器、混淆器、預校驗器。壓縮環節會檢測以及移除沒有用到的類、欄位、方法以及屬性。優化環節會分析以及優化方法的位元組碼。混淆環節會用無意義的短變數去重新命名類、變數、方法。這些步驟讓程式碼更精簡,更高效,也更難被逆向(破解)。
 

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.**  { *; }
值得注意的是,不是每個第三方SDK都需要-dontwarn 指令,這取決於混淆時第三方SDK是否出現警告,需要的時候再加上。

其他注意事項

當然在使用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開發基礎點進行整理,羅列下已經總結的文章,從中可以看到技術積累的過程。

相關文章