ProGuard代碼混淆技術(shù)詳解

前言

受《APP研發(fā)錄》啟發(fā),里面講到一名Android程序員,在工作一段時(shí)間后,會(huì)感覺到迷茫,想進(jìn)階的話接下去是看Android系統(tǒng)源碼呢,還是每天繼續(xù)做應(yīng)用,畢竟每天都是畫UI和利用MobileAPI處理Json還是蠻無聊的,做著重復(fù)的事情,沒有技術(shù)的上提升空間的。所以,根據(jù)里面提到的Android應(yīng)用開發(fā)人員所需要精通的20個(gè)技術(shù)點(diǎn),寫篇文章進(jìn)行總結(jié),一方面是梳理下基礎(chǔ)知識(shí)和鞏固知識(shí),另一方面也是彌補(bǔ)自我不足之處。
那么,今天就來講講ProGuard代碼混淆的相關(guān)技術(shù)知識(shí)點(diǎn)。

內(nèi)容目錄

  • ProGuard簡(jiǎn)介
  • ProGuard工作原理
  • 如何編寫一個(gè)ProGuard文件
  • 其他注意事項(xiàng)
  • 小結(jié)

ProGuard簡(jiǎn)介

因?yàn)镴ava代碼是非常容易反編碼的,況且Android開發(fā)的應(yīng)用程序是用Java代碼寫的,為了很好的保護(hù)Java源代碼,我們需要對(duì)編譯好后的class文件進(jìn)行混淆。
ProGuard是一個(gè)混淆代碼的開源項(xiàng)目,它的主要作用是混淆代碼,殊不知ProGuard還包括以下4個(gè)功能。

  • 壓縮(Shrink):檢測(cè)并移除代碼中無用的類、字段、方法和特性(Attribute)。
  • 優(yōu)化(Optimize):對(duì)字節(jié)碼進(jìn)行優(yōu)化,移除無用的指令。
  • 混淆(Obfuscate):使用a,b,c,d這樣簡(jiǎn)短而無意義的名稱,對(duì)類、字段和方法進(jìn)行重命名。
  • 預(yù)檢(Preveirfy):在Java平臺(tái)上對(duì)處理后的代碼進(jìn)行預(yù)檢,確保加載的class文件是可執(zhí)行的。

總而言之,根據(jù)官網(wǎng)的翻譯:Proguard是一個(gè)Java類文件壓縮器、優(yōu)化器、混淆器、預(yù)校驗(yàn)器。壓縮環(huán)節(jié)會(huì)檢測(cè)以及移除沒有用到的類、字段、方法以及屬性。優(yōu)化環(huán)節(jié)會(huì)分析以及優(yōu)化方法的字節(jié)碼?;煜h(huán)節(jié)會(huì)用無意義的短變量去重命名類、變量、方法。這些步驟讓代碼更精簡(jiǎn),更高效,也更難被逆向(破解)。

ProGuard工作原理

ProGuar由shrink、optimize、obfuscate和preveirfy四個(gè)步驟組成,每個(gè)步驟都是可選的,我們可以通過配置腳本來決定執(zhí)行其中的哪幾個(gè)步驟。


ProGuard執(zhí)行流程.png

混淆就是移除沒有用到的代碼,然后對(duì)代碼里面的類、變量、方法重命名為人可讀性很差的簡(jiǎn)短名字。
那么有一個(gè)問題,ProGuard怎么知道這個(gè)代碼沒有被用到呢?
這里引入一個(gè)Entry Point(入口點(diǎn))概念,Entry Point是在ProGuard過程中不會(huì)被處理的類或方法。在壓縮的步驟中,ProGuard會(huì)從上述的Entry Point開始遞歸遍歷,搜索哪些類和類的成員在使用,對(duì)于沒有被使用的類和類的成員,就會(huì)在壓縮段丟棄,在接下來的優(yōu)化過程中,那些非Entry Point的類、方法都會(huì)被設(shè)置為private、static或final,不使用的參數(shù)會(huì)被移除,此外,有些方法會(huì)被標(biāo)記為內(nèi)聯(lián)的,在混淆的步驟中,ProGuard會(huì)對(duì)非Entry Point的類和方法進(jìn)行重命名。
那么這個(gè)入口點(diǎn)怎么來呢?就是從ProGuard的配置文件來,只要這個(gè)配置了,那么就不會(huì)被移除。

如何編寫一個(gè)ProGuard文件

有個(gè)三步走的過程:

  • 基本混淆
  • 針對(duì)APP的量身定制
  • 針對(duì)第三方j(luò)ar包的解決方案

基本混淆

混淆文件的基本配置信息,任何APP都要使用,可以作為模板使用,具體如下。
1,基本指令

# 代碼混淆壓縮比,在0和7之間,默認(rèn)為5,一般不需要改
-optimizationpasses 5 
 
# 混淆時(shí)不使用大小寫混合,混淆后的類名為小寫
-dontusemixedcaseclassnames
 
# 指定不去忽略非公共的庫的類
-dontskipnonpubliclibraryclasses
 
# 指定不去忽略非公共的庫的類的成員
-dontskipnonpubliclibraryclassmembers
 
# 不做預(yù)校驗(yàn),preverify是proguard的4個(gè)步驟之一
# Android不需要preverify,去掉這一步可加快混淆速度
-dontpreverify
 
# 有了verbose這句話,混淆后就會(huì)生成映射文件
# 包含有類名->混淆后類名的映射關(guān)系
# 然后使用printmapping指定映射文件的名稱
-verbose
-printmapping proguardMapping.txt
 
# 指定混淆時(shí)采用的算法,后面的參數(shù)是一個(gè)過濾器
# 這個(gè)過濾器是谷歌推薦的算法,一般不改變
-optimizations !code/simplification/arithmetic,!field/*,!class/merging/*
 
# 保護(hù)代碼中的Annotation不被混淆,這在JSON實(shí)體映射時(shí)非常重要,比如fastJson
-keepattributes *Annotation*
 
# 避免混淆泛型,這在JSON實(shí)體映射時(shí)非常重要,比如fastJson
-keepattributes Signature
 
# 拋出異常時(shí)保留代碼行號(hào),在異常分析中可以方便定位
-keepattributes SourceFile,LineNumberTable

# 用于告訴ProGuard,不要跳過對(duì)非公開類的處理。默認(rèn)情況下是跳過的,因?yàn)槌绦蛑胁粫?huì)引用它們,有些情況下人們編寫的代碼與類庫中的類在同一個(gè)包下,并且對(duì)包中內(nèi)容加以引用,此時(shí)需要加入此條聲明。
-dontskipnonpubliclibraryclasses

# 這個(gè)是給Microsoft Windows用戶的,因?yàn)镻roGuard假定使用的操作系統(tǒng)是能區(qū)分兩個(gè)只是大小寫不同的文件名,但是Microsoft Windows不是這樣的操作系統(tǒng),所以必須為ProGuard指定-dontusemixedcaseclassnames選項(xiàng)
-dontusemixedcaseclassnames

2,需要保留的東西

# 保留所有的本地native方法不被混淆
-keepclasseswithmembernames class * {
    native <methods>;
}
 
# 保留了繼承自Activity、Application這些類的子類
# 因?yàn)檫@些子類,都有可能被外部調(diào)用
# 比如說,第一行就保證了所有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中的方法參數(shù)是view的方法,
# 從而我們?cè)趌ayout里面編寫onClick就不會(huì)被影響
-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();
}
 
# 對(duì)于R(資源)下的所有類及其方法,都不能被混淆
-keep class **.R$* {
    *;
}
 
# 對(duì)于帶有回調(diào)函數(shù)onXXEvent的,不能被混淆
-keepclassmembers class * {
    void *(**On*Event);
}

針對(duì)APP的量身定制

1,保留實(shí)體類和成員被混淆
對(duì)于實(shí)體,保留它們的set和get方法,對(duì)于boolean型get方法,有人喜歡命名isXXX的方式,所以不要遺漏。如下:

# 保留實(shí)體類和成員不被混淆
-keep public class com.xxxx.entity.** {
    public void set*(***);
    public *** get*();
    public *** is*();
}

一種好的做法是把所有實(shí)體都放在一個(gè)包下進(jìn)行管理,這樣只寫一次混淆就夠了,避免以后在別的包中新增的實(shí)體而忘記保留,代碼在混淆后因?yàn)檎也坏较鄳?yīng)的實(shí)體類而崩潰。

2,內(nèi)嵌類
內(nèi)嵌類經(jīng)常會(huì)被混淆,結(jié)果在調(diào)用的時(shí)候?yàn)榭站捅罎⒘?,最好的解決方法就是把這個(gè)內(nèi)嵌類拿出來,單獨(dú)成為一個(gè)類。如果一定要內(nèi)置,那么這個(gè)類就必須在混淆的時(shí)候保留,比如如下:

# 保留內(nèi)嵌類不被混淆
-keep class com.example.xxx.MainActivity$* { *; }

這個(gè)$符號(hào)就是用來分割內(nèi)嵌類與其母體的標(biāo)志。

3,對(duì)WebView的處理

# 對(duì)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,對(duì)JavaScript的處理

# 保留JS方法不被混淆
-keepclassmembers class com.example.xxx.MainActivity$JSInterface1 {
    <methods>;
}

其中JSInterface是MainActivity的子類

5,處理反射
在程序中使用SomeClass.class.method這樣的靜態(tài)方法,在ProGuard中是在壓縮過程中被保留的,那么對(duì)于Class.forName("SomeClass")呢,SomeClass不會(huì)被壓縮過程中移除,它會(huì)檢查程序中使用的Class.forName方法,對(duì)參數(shù)SomeClass法外開恩,不會(huì)被移除。但是在混淆過程中,無論是Class.forName("SomeClass"),還是SomeClass.class,都不能蒙混過關(guān),SomeClass這個(gè)類名稱會(huì)被混淆,因此,我們要在ProGuard.cfg文件中保留這個(gè)類名稱

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")

在混淆的時(shí)候,要在項(xiàng)目中搜索一下上述方法,將相應(yīng)的類或者方法的名稱進(jìn)行保留而不被混淆。

6,對(duì)于自定義View的解決方案
但凡在Layout目錄下的XML布局文件配置的自定義View,都不能進(jìn)行混淆。為此要遍歷Layout下的所有的XML布局文件,找到那些自定義View,然后確認(rèn)其是否在ProGuard文件中保留。有一種思路是,在我們使用自定義View時(shí),前面都必須加上我們的包名,比如com.a.b.customeview,我們可以遍歷所有Layout下的XML布局文件,查找所有匹配com.a.b的標(biāo)簽即可。

針對(duì)第三方j(luò)ar包的解決方案

我們?cè)贏ndroid項(xiàng)目中不可避免要使用很多第三方提供的SDK,一般而言,這些SDK是經(jīng)過ProGuard混淆的,而我們所需要做的就是避免這些SDK的類和方法在我們APP被混淆。
1,針對(duì)android-support-v4.jar的解決方案

# 針對(duì)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,其他的第三方j(luò)ar包的解決方案
這個(gè)就取決于第三方包的混淆策略了,一般都有在各自的SDK中有關(guān)于混淆的說明文字,比如支付寶如下:

# 對(duì)alipay的混淆處理
-libraryjars libs/alipaysdk.jar
-dontwarn com.alipay.android.app.**
-keep public class com.alipay.**  { *; }

值得注意的是,不是每個(gè)第三方SDK都需要-dontwarn 指令,這取決于混淆時(shí)第三方SDK是否出現(xiàn)警告,需要的時(shí)候再加上。

其他注意事項(xiàng)

當(dāng)然在使用ProGuard過程中,還有一些注意的事項(xiàng),如下。
1,如何確?;煜粫?huì)對(duì)項(xiàng)目產(chǎn)生影響

  • 測(cè)試工作要基于混淆包進(jìn)行,才能盡早發(fā)現(xiàn)問題
  • 每天開發(fā)團(tuán)隊(duì)的冒煙測(cè)試,也要基于混淆包
  • 發(fā)版前,重點(diǎn)的功能和模塊要額外的測(cè)試,包括推送,分享,打賞

2,打包時(shí)忽略警告
當(dāng)導(dǎo)出包的時(shí)候,發(fā)現(xiàn)很多could not reference class之類的warning信息,如果確認(rèn)App在運(yùn)行中和那些引用沒有什么關(guān)系,可以添加-dontwarn 標(biāo)簽,就不會(huì)提示這些警告信息了

3,對(duì)于自定義類庫的混淆處理
比如我們引用了一個(gè)叫做AndroidLib的類庫,我們需要對(duì)Lib也進(jìn)行混淆,然后在主項(xiàng)目的混淆文件中保留AndroidLib中的類和類的成員。

4,使用annotation避免混淆
另一種類或者屬性被混淆的方式是,使用annotation,比如這樣

@keep
@keepPublicGetterSetters
public class Bean{
    public  boolean booleanProperty;
    public  int intProperty;
    public  String stringProperty;
}

5,在項(xiàng)目中指定混淆文件
到最后,發(fā)現(xiàn)沒有介紹如何在項(xiàng)目中指定混淆文件。在項(xiàng)目中有一個(gè)project.properties文件,在其中寫這么一句話,就可以確保每次手動(dòng)打包生成的apk是混淆過的。

proguard.config=proguard.cfg

其中,proguard.cfg是混淆文件的名稱。

小結(jié)

總之ProGuard是一個(gè)比較枯燥的過程,但Android項(xiàng)目沒有了ProGuard就真不行了,這樣可以保證我們開發(fā)出的APK可以更健壯,畢竟很多核心代碼質(zhì)量也算是一個(gè)APK的核心競(jìng)爭(zhēng)力吧。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容