Android開發混淆的那些事

混淆想必大家都不陌生,android上用的這一套混淆規則和java混淆幾乎是一樣的。爲何需要混淆呢?簡單的說,就是將原本正常的項目文件,對其類,方法,字段,重新命名,a,b,c,d,e,f…之類的字母,達到混淆代碼的目的,這樣反編譯出來,結構亂糟糟的,給反編譯者製造一些代碼閱讀的麻煩。

ProGuard簡介

ProGuard是2002年由比利時程序員Eric Lafortune發佈的一款優秀的開源代碼優化、混淆工具,適用於Java和Android應用,目標是讓程序更小,運行更快,在Java界處於壟斷地位。主要分爲四個模塊:Shrinker(壓縮器)、Optimizer(優化器)、Obfuscator(混淆器)、Retrace(堆棧反混淆)。
ProGuard工作過程

  1. 壓縮(shrink)通過引用標記算法,移除未使用的類、方法、字段等
  2. 優化(optimize)優化字節碼,簡化代碼等操作
  3. 混淆(obfuscate)使用簡短的,無意義的名稱重命名類名,方法名,參數字段等
  4. 預校驗(perverify)爲class添加預校驗消息

Android啓用壓縮、混淆、優化

在 Android 中,我們平常所說的"混淆"其實有兩層意思:

  1. 是 Java 代碼的混淆
  2. 是資源的壓縮

其實這兩者之間並沒有什麼關聯,只不過習慣性地放在一起來使用。那麼,說了這麼多,Android平臺上到底該如何開啓混淆呢?

android {
    buildTypes {
        release {
            minifyEnabled true
            shrinkResources true
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

以上就是開啓混淆的基本操作了,通過 minifyEnabled 設置爲 true 來開啓混淆。同時,可以設置 shrinkResources 爲 true 來開啓資源的壓縮。

不難看出,我們一般在打 release 包時才啓用混淆,因爲混淆會增加額外的編譯時間,所以不建議在 debug 模式下啓用。此外,需要注意的是:只有在啓用混淆的前提下開啓資源壓縮纔會有效!

以上代碼中的 proguard-android.txt 表示 Android 系統爲我們提供的默認混淆規則文件,而 proguard-rules.pro 則是我們想要自定義的混淆規則。

如果你不對proguard-rules.pro文件做定製化,默認是整個工程全開啓混淆的,但是由於一些三方代碼、反射、自定義view等,一旦這些都混淆了編譯器在運行時找不到具體的成員,從而會導致錯誤,所以我們也要忽略一些類或者成員的混淆。

Android混淆配置關鍵字

系統混淆配置

#混淆時不使用大小寫混合類名
-dontusemixedcaseclassnames 
#不跳過library中的非public的類
-dontskipnonpubliclibraryclasses 
#打印混淆的詳細信息
-verbose 
#不進行優化,建議使用此選項
-dontoptimize 
#不進行預校驗,Android不需要,可加快混淆速度
-dontpreverify
#忽略警告
-ignorewarnings 
#指定代碼的壓縮級別
-optimizationpasses 5  

Proguard關鍵字

Proguard關鍵字

如果你不確定你需要使用哪一個選項,那麼你應該儘量使用 -keep 。它會確保指定的類和成員在 壓縮階段(shrinking step)不會被刪除,並且在 混淆階段(obfuscation step)不會被混淆。

Proguard通配符

Proguard通配符

上邊的通配符沒有返回類型。僅僅通配符有一個參數列表。

字段和方法的名稱可以包含如下通配符:

? 匹配名稱中的任意單個字符
* 匹配名稱中的任意一部分

類型描述可以包含如下通配符:

% 匹配任意基本類型(‘boolean’,'int'以及其他,但是不包括‘void’).
? 匹配類名中的任意單個字符
* 匹配類名中的任意部分,但是不包含包分隔符 。
** 匹配類名中的任意部分,可以包含任意個數的包分隔符。
*** 匹配任意類型(基本類型或者非基本類型,數組或者非數組)
... 匹配任意個數任意類型的參數。

注意:? , * 以及 ** 通配符永遠都不會匹配基本類型。此外,僅僅 *** 通配符會匹配任意長度任意類型的數組。

哪些不應該進行混淆

我們在瞭解了混淆的基本命令之後,很多人應該還是一頭霧水:到底哪些內容該混淆?其實,我們在使用代碼混淆時,ProGuard 對我們項目中大部分代碼進行了混淆操作,爲了防止編譯時出錯,我們應該通過 keep 命令保留一些元素不被混淆。所以,我們只需要知道哪些元素不應該被混淆:

枚舉

項目中難免可能會用到枚舉類型,然而它不能參與到混淆當中去。原因是:枚舉類內部存在 values 方法,混淆後該方法會被重新命名,並拋出 NoSuchMethodException。慶幸的是,Android 系統默認的混淆規則中已經添加了對於枚舉類的處理,我們無需再去做額外工作。

被反射的元素

被反射使用的類、變量、方法、包名等不應該被混淆處理。原因在於:代碼混淆過程中,被反射使用的元素會被重命名,然而反射依舊是按照先前的名稱去尋找元素,所以會經常發生 NoSuchMethodException 和 NoSuchFiledException 問題。

實體類

實體類即我們常說的"數據類",當然經常伴隨着序列化與反序列化操作。很多人也應該都想到了,混淆是將原本有特定含義的"元素"轉變爲無意義的名稱,所以,經過混淆的"洗禮"之後,序列化之後的 value 對應的 key 已然變爲沒有意義的字段,這肯定是我們不希望的。同時,反序列化的過程創建對象從根本上來說還是藉助於反射,混淆之後 key 會被改變,所以也會違揹我們預期的效果。

四大組件

Android 中的四大組件同樣不應該被混淆。原因在於:
四大組件使用前都需要在 AndroidManifest.xml 文件中進行註冊聲明,然而混淆處理之後,四大組件的類名就會被篡改,實際使用的類與 manifest 中註冊的類並不匹配,故而出錯。其他應用程序訪問組件時可能會用到類的包名加類名,如果經過混淆,可能會無法找到對應組件或者產生異常。

JNI 調用的Java 方法

當 JNI 調用的 Java 方法被混淆後,方法名會變成無意義的名稱,這就與 C++ 中原本的 Java 方法名不匹配,因而會無法找到所調用的方法。
其他不應該被混淆的

自定義控件不需要被混淆

JavaScript 調用 Java 的方法不應混淆

Java 的 native 方法不應該被混淆

項目中引用的第三方庫也不建議混淆

混淆舉例

以下是一些混淆的舉例和說明

//不混淆某個類
-keep public class name.huihui.example.Test { *; }
//不混淆某個類的子類
-keep public class * extends name.huihui.example.Test { *; }
//不混淆所有類名中包含了“model”的類及其成員
-keep public class **.*model*.** {*;}
//不混淆某個接口的實現
-keep class * implements name.huihui.example.TestInterface { *; }
//不混淆某個類的構造方法
-keepclassmembers class name.huihui.example.Test { 
    public <init>(); 
}
//不混淆某個類的特定的方法
-keepclassmembers class name.huihui.example.Test { 
    public void test(java.lang.String); 
}
//不混淆某個類的內部類
-keep class name.huihui.example.Test$* {*;}
//兩個常用的混淆命令,注意:
//一顆星表示只是保持該包下的類名,而子包下的類名還是會被混淆;
//兩顆星表示把本包和所含子包下的類名都保持;
-keep class com.suchengkeji.android.ui.*
-keep class com.suchengkeji.android.ui.**
//用以上方法保持類後,你會發現類名雖然未混淆,但裏面的具體方法和變量命名還是變了,
//如果既想保持類名,又想保持裏面的內容不被混淆,我們就需要以下方法了

//不混淆某個包所有的類
-keep class com.suchengkeji.android.bean.** { *; }

//不混淆某個具體類
-keep public class com.android.vending.licensing.ILicensingService

一個通用的混淆模板

這裏給大家提供一個通用的混淆模板

###########################################基本指令(基本不動)#######################################

#代碼混淆壓縮比,在0~7之間,默認爲5,一般不做修改
-optimizationpasses 5

#混合時不使用大小寫混合,混合後的類名爲小寫
-dontusemixedcaseclassnames

#指定不去忽略非公共庫的類
-dontskipnonpubliclibraryclasses

#指定不去忽略非公共庫的類的成員
-dontskipnonpubliclibraryclassmembers

#這句話能夠使我們的項目混淆後產生映射文件
#包含有類名->混淆後類名的映射關係
-verbose

#不做預校驗,preverify是proguard的四個步驟之一,Android不需要preverify,去掉這一步能夠加快混淆速度。
-dontpreverify

#保留Annotation不混淆
-keepattributes *Annotation*,InnerClasses

#保留泛型不混淆
-keepattributes Signature

#拋出異常時保留代碼行號
-keepattributes SourceFile,LineNumberTable

#指定混淆是採用的算法,後面的參數是一個過濾器 #這個過濾器是谷歌推薦的算法,一般不做更改
-optimizations !code/simplification/cast,!field/*,!class/merging/*


#########################################java部分###########################################

#native方法不被混淆
-keepclasseswithmembernames class * {
    native <methods>;
}

#枚舉enum類不被混淆
-keepclassmembers enum * {
    public static **[] values();
    public static ** valueOf(java.lang.String);
}

#Serializable類不被混淆
-keep public class * implements java.io.Serializable {*;}

#########################################android部分####################################

#保留support下的所有類及其內部類
-keep class android.support.** {*;}
#基類不被混淆
-keep class * extends android.app.Activity
-keep class * extends android.app.Application
-keep class * extends android.app.Service
-keep class * extends android.content.BroadcastReceiver
-keep class * extends android.content.ContentProvider
-keep class * extends android.app.backup.BackupAgentHelper
-keep class * extends android.preference.Preference

#自定義控件類不被混淆
-keepclasseswithmembers class * {
    public <init>(android.content.Context, android.util.AttributeSet);
}
-keepclasseswithmembers class * {
    public <init>(android.content.Context, android.util.AttributeSet, int);
}

#表示不混淆任何一個View中的setXxx()和getXxx()方法,
#因爲屬性動畫需要有相應的setter和getter的方法實現,混淆了就無法工作了。
-keep class * extends android.view.View{
    *** get*();
    void set*(***);
    <init>(...);
}

# 保留R下面的資源
#不混淆資源類下static的
-keepclassmembers class **.R$* {
    public static <fields>;
}

#Parcelable類不被混淆
-keep class * implements android.os.Parcelable {
  public static final android.os.Parcelable$Creator *;
}

#WebView相關不被混淆
-keepclassmembers class * extends android.webkit.WebView {*;}
-keepclassmembers class * extends android.webkit.WebViewClient {*;}
-keepclassmembers class * extends android.webkit.WebChromeClient {*;}
-keepclassmembers class * {
    @android.webkit.JavascriptInterface <methods>;
}

########################################本項目混淆規則##########################################

#非本項目不進行混淆
-keep class !com.ehai.store.** {*;}
-dontwarn **

#########################################其他#####################################################
#
##避免混淆Bugly
#-dontwarn com.tencent.bugly.**
#-keep public class com.tencent.bugly.**{*;}
#
##極光推送
#-dontoptimize
#-dontpreverify
#
#-dontwarn cn.jpush.**
#-keep class cn.jpush.** { *; }
#-keep class * extends cn.jpush.android.helpers.JPushMessageReceiver { *; }
#
#-dontwarn cn.jiguang.**
#-keep class cn.jiguang.** { *; }
#
##微衆銀行OCR SDK 混淆
##webank-cloud-normal-proguard-rules.pro的規則已經被webank-cloud-ocr-proguard-rules.pro “include”了,不需要再添加
#-include webank-cloud-ocr-proguard-rules.pro

混淆後的堆棧跟蹤

代碼經過 ProGuard 混淆處理後,想要讀取 StackTrace(堆棧追蹤)信息就會變得很困難。由於方法名稱和類的名稱都經過混淆處理,即使程序發生崩潰問題,也很難定位問題所在。幸運的是,ProGuard 爲我們提供了補救的措施,在着手進行之前,我們先來看一下 ProGuard 每次構建後生成了哪些內容。

混淆輸出結果

混淆構建完成之後,會在<module-name>/build/outputs/mapping/./ 目錄下生成以下文件:

  • dump.txt
    說明 APK 內所有類文件的內部結構。
  • mapping.txt
    提供混淆前後的內容對照表,內容主要包含類、方法和類的成員變量。
  • seeds.txt
    羅列出未進行混淆處理的類和成員。
  • usage.txt
    羅列出從 APK 中移除的代碼。

如何從堆棧中還原ProGuard混淆後的代碼

混淆後的代碼一旦發生崩潰,那麼所產生的日誌也是混淆的,調試起來很麻煩,所以此時有必要將其還原到原生態進行分析。

還原前

Caused by: java.lang.NullPointerException
at net.simplyadvanced.ltediscovery.be.u(Unknown Source)
at net.simplyadvanced.ltediscovery.at.v(Unknown Source)
at net.simplyadvanced.ltediscovery.at.d(Unknown Source)
at net.simplyadvanced.ltediscovery.av.onReceive(Unknown Source)

還原後

Caused by: java.lang.NullPointerException
at net.simplyadvanced.ltediscovery.UtilTelephony.boolean is800MhzNetwork()(Unknown Source)
at net.simplyadvanced.ltediscovery.ServiceDetectLte.void checkAndAlertUserIf800MhzConnected()(Unknown Source)
at net.simplyadvanced.ltediscovery.ServiceDetectLte.void startLocalBroadcastReceiver()(Unknown Source)
at net.simplyadvanced.ltediscovery.ServiceDetectLte$2.void onReceive(android.content.Context,android.content.Intent)(Unknown Source)

那麼如何還原呢?這裏提供兩種方式:GUI工具和命令行

GUI工具還原

  1. 打開/tools/proguard/bin/proguardgui.bat
  2. 選擇左邊欄的ReTrace選項
  3. 添加你的mapping文件和混淆過的堆棧信息
  4. 點擊ReTrace!

如圖:
ProGuard工具

命令行還原

  1. 需要你的ProGuard的mapping文件和你想要還原的堆棧信息(如stacktrace.txt)
  2. 最簡單的方法就是將這些文件拷貝到/tools/proguard/bin/目錄
  3. 運行以下命令
//Windows
retrace.bat -verbose mapping.txt stacktrace.txt > out.txt

//Mac\Linux
retrace.sh -verbose mapping.txt stacktrace.txt > out.txt

參考

  • https://tech.meituan.com/2018/04/27/mt-proguard.html
  • https://stuff.mit.edu/afs/sipb/project/android/sdk/android-sdk-linux/tools/proguard/docs/index.html#manual/introduction.html
  • https://developer.android.com/studio/build/shrink-code.html
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章