一篇文章帶你領略Android混淆的魅力

在 Android 平常開發過程當中,混淆是咱們開發 App 的一項必不可少的技能。只要是咱們親身經歷過 App 打包上線的過程,或多或少都須要瞭解一些代碼混淆的基本操做。那麼,混淆究竟是什麼?它的好處有哪些?具體效果如何?別急,下面咱們來一一探索它的"獨特"魅力🐳。html

混淆簡介

代碼混淆Obfuscated code)是將程序中的代碼以某種規則轉換爲難以閱讀和理解的代碼的一種行爲。java

混淆的好處

混淆的好處就是它的目的:令 APK 難以被逆向工程,即很大程度上增長反編譯的成本。此外,Android 當中的"混淆"還可以在打包時移除無用資源,顯著減小 APK 體積。最後,還能以變通方式避免 Android 中常見的 64k 方法數引用的限制。android

咱們先來看一下混淆先後的 APK 結構對比。git

混淆前: github

混淆前

混淆後: windows

混淆後

從上面兩張圖能夠看出:通過混淆處理以後,咱們的 APK 中包名、類名、成員名等都被替換爲隨機、無心義的名稱,增長了代碼閱讀和理解的困難程度,提升了反編譯的成本。細心的小夥伴可能又會注意到:混淆先後 APK 的體積居然從 2.7M 減少到了 1.4M,體積縮減了近一倍!真的有這麼神奇嗎?哈哈,確實是這麼神奇,讓咱們慢慢來揭開它的神祕面紗吧😏。ruby

Android 當中的混淆

在 Android 中,咱們日常所說的"混淆"其實有兩層意思,一個是 Java 代碼的混淆,另一個是資源的壓縮。其實這二者之間並無什麼關聯,只不過習慣性地放在一塊兒來使用。那麼,說了這麼多,Android 平臺上到底該如何開啓混淆呢?bash

啓用混淆

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

以上就是開啓混淆的基本操做了,經過 minifyEnabled 設置爲 true 來開啓混淆。同時,能夠設置 shrinkResourcestrue 來開啓資源的壓縮。不難看出,咱們通常在打 release 包時才啓用混淆,由於混淆會增長額外的編譯時間,因此不建議在 debug 模式下啓用。此外,須要注意的是:只有在啓用混淆的前提下開啓資源壓縮纔會有效!以上代碼中的 proguard-android.txt 表示 Android 系統爲咱們提供的默認混淆規則文件,而 proguard-rules.pro 則是咱們想要自定義的混淆規則,至於如何自定義混淆規則咱們將在接下來會講到😄。markdown

代碼混淆

其實,Java 平臺爲咱們提供了 Proguard 混淆工具來幫助咱們快速地對代碼進行混淆。根據 Java 官方介紹,Proguard 對應的具體中文定義以下:app

  • 它是一個包含代碼文件壓縮優化混淆校驗等功能的工具
  • 它可以檢測並刪除無用的類、變量、方法和屬性
  • 它可以優化字節碼並刪除未使用的指令
  • 它可以將類、變量和方法的名字重命名爲無心義的名稱從而達到混淆效果
  • 最後,它還會校驗處理後的代碼,主要針對 Java 6 及以上版本和 Java ME

資源壓縮

Android 中,編譯器爲咱們提供了另一項強大的功能:資源的壓縮。資源壓縮可以幫助咱們移除項目及依賴倉庫中未使用到的資源,有效地下降了apk包的大小。因爲資源壓縮與代碼混淆是協同工做,因此,若是須要開啓資源的壓縮,切記要先開啓代碼混淆,不然會出現如下問題:

ERROR: Removing unused resources requires unused code shrinking to be turned on. See http://d.android.com/r/tools/shrink-resources.html for more information.
Affected Modules: app
複製代碼

自定義要保留的資源

當咱們開啓了資源壓縮以後,系統會默認替咱們移除全部未使用的資源,假如咱們須要保留某些特定的資源,能夠在咱們項目中建立一個被 <resources> 標記的 XML 文件(如 res/raw/keep.xml),並在 tools:keep 屬性中指定每一個要保留的資源,在 tools:discard 屬性中指定每一個要捨棄的資源。這兩個屬性都接受逗號分隔的資源名稱列表。一樣,咱們可使用字符 * 做爲通配符。如:

<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools" tools:keep="@layout/activity_video*,@layout/dialog_update_v2" tools:discard="@layout/unused_layout,@drawable/unused_selector" />
複製代碼

啓用嚴格檢查模式

正常狀況下,資源壓縮器可準確斷定系統是否使用了資源。不過,若是您的代碼(包含庫)調用 Resources.getIdentifier(),這就表示您的代碼將根據動態生成的字符串查詢資源名稱。這時,資源壓縮器會採起防護性行爲,將全部具備匹配名稱格式的資源標記爲可能已使用,沒法移除。例如,如下代碼會使全部帶 img_ 前綴的資源標記爲已使用:

String name = String.format("img_%1d", angle + 1);
res = getResources().getIdentifier(name, "drawable", getPackageName());
複製代碼

這時,我能夠開啓資源的嚴格審查模式,只會保留肯定已使用的資源:

<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools" tools:shrinkMode="strict" />
複製代碼

移除備用資源

Gradle 資源壓縮器只會移除未被應用引用的資源,這意味着它不會移除用於不一樣設備配置的備用資源。必要時,咱們可使用 Android Gradle 插件的 resConfigs 屬性來移除您的應用不須要的備用資源文件(常見的有用於國際化支持的 strings.xml,適配用的 layout.xml 等):

android {
    defaultConfig {
        ...
        //保留中文和英文國際化支持
        resConfigs "en", "zh"
    }
}
複製代碼

自定義混淆規則

品嚐完了以上"配菜",下面讓咱們來品味一下本文的"主菜":自定義混淆規則。首先,咱們來了解一下常見的混淆命令。

keep 命令

這裏說的 keep 命令指的是一系列以 -keep 開頭的命令,它主要用來保留 Java 中不須要進行混淆的元素。如下是常見的 -keep 命令:

  • -keep

    做用:保留指定的類和成員,防止被混淆處理。例如:

    # 保留包:com.moos.media.entity 下面的類以及類成員
    -keep public class com.moos.media.entity.**
    
    # 保留類:NumberProgressBar
    -keep public class com.moos.media.widget.NumberProgressBar {*;}
    複製代碼
  • -keepclassmembers

    做用:保留指定的類的成員(變量/方法),它們將不會被混淆。如:

    # 保留類的成員:MediaUtils類中的特定成員方法
    -keepclassmembers class com.moos.media.MediaUtils {
        public static *** getLocalVideos(android.content.Context);
        public static *** getLocalPictures(android.content.Context);
    }
    複製代碼
  • -keepclasseswithmembers

    做用:保留指定的類和其成員(變量/方法),前提是它們在壓縮階段沒有被刪除。與-keep 使用方式相似:

    # 保留類:BaseMediaEntity 的子類
    -keepclasseswithmembers public class * extends com.moos.media.entity.BaseMediaEntity{*;}
    
    # 保留類:OnProgressBarListener接口的實現類
    -keep public class * implements com.moos.media.widget.OnProgressBarListener {*;}
    複製代碼
  • @Keep

    除了以上方式,你也能夠選擇使用 @Keep 註解來保留指望代碼,防止它們被混淆處理。好比,咱們經過 @Keep 修飾一個類來保留它不被混淆:

    @Keep
    data class CloudMusicBean(var createDate: String,
                              var id: Long,
                              var name: String,
                              var url: String,
                              val imgUrl: String)
    複製代碼

    一樣地,咱們也可讓 @Keep 來修飾方法或者字段進而保留它們。

其餘命令

  1. dontwarn

    -dontwarn 命令通常在咱們引入新的 library 時會使用到,經常使用於處理 library 中沒法解決的警告。如:

    -keep class twitter4j.** { *; }
    
    -dontwarn twitter4j.**
    複製代碼
  2. 其餘的命令用法可參考 Android 系統提供的默認混淆規則:

    #混淆時不生成大小寫混合的類名
    -dontusemixedcaseclassnames
    
    #不跳過非公共的庫的類
    -dontskipnonpubliclibraryclasses
    
    #混淆過程當中記錄日誌
    -verbose
    
    #關閉預校驗
    -dontpreverify
    
    #關閉優化
    -dontoptimize
    
    #保留註解
    -keepattributes *Annotation*
    
    #保留全部擁有本地方法的類名及本地方法名
    -keepclasseswithmembernames class * {
        native <methods>;
    }
    
    #保留自定義View的get和set方法
    -keepclassmembers public class * extends android.view.View {
       void set*(***);
       *** get*();
    }
    
    #保留Activity中View及其子類入參的方法,如: onClick(android.view.View)
    -keepclassmembers class * extends android.app.Activity {
       public void *(android.view.View);
    }
    
    #保留枚舉
    -keepclassmembers enum * {
        **[] $VALUES;
        public *;
    }
    
    #保留序列化的類
    -keepclassmembers class * implements android.os.Parcelable {
      public static final android.os.Parcelable$Creator CREATOR;
    }
    
    #保留R文件的靜態成員
    -keepclassmembers class **.R$* {
        public static <fields>;
    }
    
    -dontwarn android.support.**
    
    -keep class android.support.annotation.Keep
    
    -keep @android.support.annotation.Keep class * {*;}
    
    -keepclasseswithmembers class * {
        @android.support.annotation.Keep <methods>;
    }
    
    -keepclasseswithmembers class * {
        @android.support.annotation.Keep <fields>;
    }
    
    -keepclasseswithmembers class * {
        @android.support.annotation.Keep <init>(...);
    }
    複製代碼

    更多混淆命令能夠參考文章:Proguard 最全混淆規則說明 ,這裏就不作詳細講解了。

混淆"黑名單"

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

枚舉

項目中不免可能會用到枚舉類型,然而它不能參與到混淆當中去。緣由是:枚舉類內部存在 values 方法,混淆後該方法會被從新命名,並拋出 NoSuchMethodException。慶幸的是,Android 系統默認的混淆規則中已經添加了對於枚舉類的處理,咱們無需再去作額外工做。想了解更多枚舉內部細節能夠去查看源碼,篇幅有限再也不細說。

被反射的元素

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

實體類

實體類即咱們常說的"數據類",固然常常伴隨着序列化反序列化操做。不少人也應該都想到了,混淆是將本來有特定含義的"元素"轉變爲無心義的名稱,因此,通過混淆的"洗禮"以後,序列化以後的 value 對應的 key 已然變爲沒有意義的字段,這確定是咱們不但願的。同時,反序列化的過程建立對象從根本上來講仍是藉助於反射,混淆以後 key 會被改變,因此也會違背咱們預期的效果。

四大組件

Android 中的四大組件一樣不該該被混淆。緣由在於:

  1. 四大組件使用前都須要在 AndroidManifest.xml 文件中進行註冊聲明,然而混淆處理以後,四大組件的類名就會被篡改,實際使用的類與 manifest 中註冊的類並不匹配,故而出錯。
  2. 其餘應用程序訪問組件時可能會用到類的包名加類名,若是通過混淆,可能會沒法找到對應組件或者產生異常。

JNI 調用的Java 方法

當 JNI 調用的 Java 方法被混淆後,方法名會變成無心義的名稱,這就與 C++ 中本來的 Java 方法名不匹配,於是會沒法找到所調用的方法。

其餘不該該被混淆的

  • 自定義控件不須要被混淆
  • JavaScript 調用 Java 的方法不該混淆
  • Java 的 native 方法不該該被混淆
  • 項目中引用的第三方庫也不建議混淆

混淆後的堆棧跟蹤

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

混淆輸出結果

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

  • dump.txt

    說明 APK 內全部類文件的內部結構。

  • mapping.txt

    提供混淆先後的內容對照表,內容主要包含類、方法和類的成員變量。

  • seeds.txt

    羅列出未進行混淆處理的類和成員。

  • usage.txt

    羅列出從 APK 中移除的代碼。

恢復堆棧跟蹤

瞭解完混淆構建完畢後輸出的內容以後,咱們如今就來看一下以前的問題:混淆處理後,StackTrace 定位困難。如何來恢復 StackTrace 的定位能力呢?系統爲咱們提供了 retrace 工具,結合上文提到的 mapping.txt 文件,就能夠將混淆後的崩潰堆棧追蹤信息還原成正常狀況下的 StackTrace 信息。主要有兩種方式來恢復 StackTrace,爲了方便理解,咱們如下面這段崩潰信息爲例,藉助兩種方式分別來還原:

java.lang.RuntimeException: Unable to start activity 
     Caused by: kotlin.KotlinNullPointerException
        at com.moos.media.ui.ImageSelectActivity.k(ImageSelectActivity.kt:71)
        at com.moos.media.ui.ImageSelectActivity.onCreate(ImageSelectActivity.kt:58)
        at android.app.Activity.performCreate(Activity.java:6237)
        at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1107)
複製代碼
  1. 經過 retrace 腳本工具

    首先咱們要進入到 Android SDK 路徑的 /tools/proguard/bin 目錄中,這裏以 Mac 系統爲例,能夠看到以下內容:

    retrace腳本目錄

    能夠看到如上三個文件,而 proguardgui.sh 纔是咱們須要的 retrace 腳本(Windows系統下爲 proguardgui.bat )。Windows 系統中只須要雙擊腳本 proguardgui.bat 便可運行,至於 Mac 系統,若是你沒有作任何配置,只須要將 proguardgui.sh 腳本拖動到 Mac 自帶的終端中,回車鍵便可運行。接着,咱們會看到以下界面:

    retrace腳本界面

    選擇 ReTrace 欄 ,並添加咱們項目中混淆生成的 mapping.txt 文件所在位置,而後將咱們的混淆後的崩潰信息複製到 Obfuscated stack trace 那一欄,點擊 ReTrace! 按鈕便可還原出咱們的崩潰日誌信息,結果如上圖所示,咱們以前的混淆日誌:at com.moos.media.ui.ImageSelectActivity.k(ImageSelectActivity.kt:71) 被還原成了 at com.moos.media.ui.ImageSelectActivity.initView(ImageSelectActivity.kt:71)ImageSelectActivity.k 是咱們混淆後的方法名,ImageSelectActivity.initView 則是最初未混淆前的方法名,藉助於 ReTrace 工具的幫助,咱們就能夠像之前同樣很快定位到崩潰代碼區域了。

  2. 經過 retrace 命令行

    咱們先要將崩潰信息複製到 txt 格式的文件(如:proguard_stacktrace.txt)中保存,而後執行如下命令便可(MAC系統):

    retrace.sh -verbose mapping.txt proguard_stacktrace.txt
    複製代碼

    若是你是 windows 系統,能夠執行如下命令:

    retrace.bat -verbose mapping.txt proguard_stacktrace.txt
    複製代碼

    最終還原的結果和以前效果同樣:

    retrace命令行還原stacktrace

    也許你經過以上兩種方式在對 stackTrace 進行恢復時,發現 Unknown Source 問題:

    unknown source問題

值得注意的是,記得在混淆規則中加上以下配置來提高咱們的 StackSource 查找效率:

# 保留源文件名和具體代碼行號
-keepattributes SourceFile,LineNumberTable
複製代碼

此外,咱們每次使用 ProGuard 建立發佈構建時都都會覆蓋以前版本的 mapping.txt 文件,所以咱們每次發佈新版本時都必須當心地保存一個副本。經過爲每一個發佈構建保留一個 mapping.txt 文件副本,咱們就能夠在用戶提交的已混淆的 StackTrace 來對舊版本應用的問題進行調試和修復。

漲姿式的操做

通過上文的介紹,咱們知道,APK 在通過代碼混淆處理後,包名、類名、成員名被轉化爲無心義、難以理解的名稱,增長反編譯的成本。Android ProGuard 爲咱們提供了默認的"混淆字典",即將元素名稱轉爲英文小寫字母的形式。那麼,咱們能夠定義本身的混淆字典嗎?賣個關子,咱們先來看一張效果圖:

自定義混淆字典

這個波操做是否是有點"出類拔萃"了?哈哈,就不賣關子了,其實很簡單,只要生成一套本身的 txt 格式的混淆字典,而後在混淆規則 Proguard-rules.pro 中應用一下便可:

混淆字典配置

本文中使用的混淆字典能夠在此處查看並下載:proguard_tradition.txt

固然,你們也能夠本身去定製化本身的"混淆字典",增長反編譯的難度。

一路走下來,咱們發現,從混淆技術的必要性和優勢來看,它仍是很值得咱們去深刻學習和研究的,本文帶你們領略的僅僅是"冰山一角"。因爲本人的技術水平有限,若你們發現有問題或者闡述不當之處,歡迎指出並修正。

相關參考

相關文章
相關標籤/搜索