Android | 代碼混淆到底做了什麼?

前言

代碼混淆對於每個入門的 Android 工程師來說都不會太陌生,因爲在編譯正式版本時,這是一個必不可少的過程。而且使用代碼混淆也相當簡單,簡單到只需要配置一句minifyEnabled true。但是你是否理解混淆的原理,如果問你代碼混淆到底做了什麼,你會怎麼說?

 

 

1、混淆編譯器

如果以混淆編譯器來劃分的話,Android 代碼混淆可以分爲以下兩個時期:

  • ProGuard:一個通用的 Java 字節碼優化工具,由比利時團隊 GuardSquare 開發
  • R8:ProGuard 的繼承者,專爲 Android 設計,在編譯性能上更優秀

下圖梳理了它們隨着 Android Gradle Plugin 版本迭代相應做出的變更:

 

Android Gradle Plugin 版本迭代

其中,混淆編譯器的變更:

  • 遠古: ProGuard
  • 3.2.0:ProGuard(默認),R8(引入)
  • 3.4.0:R8(默認)

其中:DEX編譯器的變更:

  • 遠古: DX
  • 3.0.0:DX(默認),D8(引入)
  • 3.1.0:D8(默認)

如果需要修正 Android Gradle Plugin 的默認行爲,可以在gradle.properties中添加配置:

  • 啓用與禁用 R8
    # 顯式啓用 R8
    android.enableR8 = true
    
    # 1. 只對 Android Library module 停用 R8 編譯器
    android.enableR8.libraries = false
    # 2. 對所有 module 停用 R8 編譯器
    android.enableR8 = false
    
  • 啓用與禁用 D8
    # 顯式啓用 D8
    android.enableD8 = true
    
    # 顯式禁用 D8
    android.enableD8 = false
    

另外,如果在應用模塊的 build.gradle 文件中設置useProguard = false,也會使用 R8 編譯器代替 ProGuard。


2、四大功能

ProGuard 與它的繼承者 R8 都提供了壓縮(shrinker)、優化(optimizer)、混淆(obfuscator)、預校驗(preverifier)四大功能:

  • 壓縮(也稱爲搖樹優化,tree shaking):從應用及依賴項中移除未使用的類、方法和字段,有助於規避64方法數的瓶頸
  • 優化:通過代碼分析移除更多未使用的代碼,甚至重寫代碼
  • 混淆:使用無意義的簡短名稱重命名類、方法和字段,增加逆向難度
  • 預校驗:對於面向 Java 6 或者 Java 7 JVM 的 class 文件,編譯時可以把預校驗信息添加到類文件中(StackMap 和 StackMapTable屬性),從而加快類加載效率。預校驗對於 Java 7 JVM 來說是必須的,但是對於 Android 平臺無效

使用 ProGuard 時,部分編譯流程如下圖所示:

  • ProGuard 對 .class 文件執行代碼壓縮、優化與混淆
  • D8 編譯器執行脫糖,並將 .class 文件轉換爲 .dex文件

 

使用ProGuard的部分編譯流程

使用 R8 時,部分編譯流程如下圖所示:

  • R8 將脫糖(Desugar)、壓縮、優化、混淆和 dex(D8 編譯器)整合到一個步驟
  • R8 對 .class 文件執行代碼壓縮、優化與混淆
  • D8 編譯器執行脫糖,並將 .class 文件轉換爲 .dex文件

使用R8的編譯流程

對比以下 ProGuard 與 R8 :

  • 共同點:

1、開源
2、R8 支持所有現有 ProGuard 規則文件
3、都提供了四大功能:壓縮優化混淆預校驗

  • 不同點:

1、ProGuard 可用於 Java 項目,而 R8 專爲 Android 項目設計
2、R8 將脫糖(Desugar)、壓縮、優化、混淆和 dex(D8 編譯器)整合到一個步驟中,顯着提高了編譯性能

關於 D8 編譯器

將 .class 字節碼轉化爲 .dex 字節碼的過程被稱爲 DEX 編譯,最初是由DX 編譯器完成。與 DX 編譯器相比,新的 D8 編譯器的編譯速度更快,輸出的 .dex 文件更小,卻能保持相同乃至更出色的應用運行時性能


3、 使用示例

無論使用R8還是ProGuard,默認不會啓用壓縮、優化和混淆功能。這個設計主要是出於兩方面考慮:一方面是因爲這些編譯時任務會增加編譯時間,另一方面是因爲如果沒有充分定義混淆保留規則,還可能會引入運行時錯誤。因此,最好只在應用的測試版本和發佈版本中啓用這些編譯時任務,參考使用示例:

// build.gradle
...
android {

  buildTypes {

    // 測試版本
    preview {
      // 啓用代碼壓縮、優化和混淆(由R8或者ProGuard執行)
      minifyEnabled true
      // 啓用資源壓縮(由Android Gradle plugin執行)
      shrinkResources true
      // 指定混淆保留規則文件
      proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
    } 

    // 發佈版本
    release {
      minifyEnabled true
      shrinkResources true
      proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
    }

    // 開發版本
    debug{
      minifyEnabled false
    }
  }
...
}
  • minifyEnabled:(默認情況下)啓用代碼壓縮、優化、混淆與預校驗
  • shrinkResources:啓用資源壓縮
  • proguardFilesproguardFile:指定 ProGuard 規則文件,前者可以指定多個參數。下面兩段配置的作用是一樣的。
    // 方式一:
    proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
    // 方式二:
    proguardFile getDefaultProguardFile('proguard-android-optimize.txt')
    proguardFile 'proguard-rules.pro'
    

前面提到了:無論使用R8還是ProGuard,壓縮、優化和混淆功能都是默認關閉的。通過以下配置可以靈活控制:

  • 整體關閉
minifyEnabled false
// 這行就沒有效果了
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
  • 關閉壓縮
-dontshrink
  • 關閉優化(R8 無效)
-dontoptimize

注意:R8 不能關閉優化,也不允許修改優化的行爲,事實上,R8 會忽略修改默認優化行爲的規則。例如設置 -optimizations 和 -optimizationpasses後會得到編譯時警告:

AGPBI: {"kind":"warning","text":"Ignoring option: -optimizations","sources":[{"file":"省略..."}],"tool":"D8"}
AGPBI: {"kind":"warning","text":"Ignoring option: -optimizationpasses","sources":"省略..."}],"tool":"D8"}
  • 關閉混淆(建議在開發版本關閉混淆)
-dontobfuscate
  • 關閉預校驗(對 Android 平臺無效,建議關閉)
-dontpreverify

4、ProGuard 規則文件

R8 延續了 ProGuard 使用規則文件修改默認行爲的做法。在很多時候,規則文件也被稱爲混淆保留規則文件,這是因爲該文件內定義的絕大多數規則都是和代碼混淆相關的。事實上,文件內還可以定義代碼壓縮、優化和預校驗規則,因此稱爲 ProGuard 規則文件比較嚴謹。

在上一節裏,我們提到了使用proguardFilesproguardFile指定 ProGuard 規則文件。對於任何一個項目,它的 ProGuard 規則文件有以下三種來源:

  • 1、Android Gradle 插件

在編譯時,Android Gradle 插件會生成 proguard-android-optimize.txt、 proguard-android.txt,位置在<module-dir>/build/intermediates/proguard-files/。這兩個文件中除了註釋之外,唯一的區別是前者啓用瞭如下代碼壓縮,而後者關閉了代碼壓縮,如下所示:

# proguard-android-optimize.txt
-optimizations !code/simplification/arithmetic,!code/simplification/cast,!field/*,!class/merging/*
-optimizationpasses 5
-allowaccessmodification
相同部分省略...
# proguard-android.txt
-dontoptimize
相同部分省略...

其中相同的那部分混淆規則中,下面這一部分是比較特殊的:

-keep class android.support.annotation.Keep
-keep class androidx.annotation.Keep
// 保留@Keep註解的類,保留...TODO
-keep @android.support.annotation.Keep class * {*;}
-keep @androidx.annotation.Keep class * {*;}
// 保留@Keep修飾的方法
-keepclasseswithmembers class * {
    @android.support.annotation.Keep <methods>;
}
-keepclasseswithmembers class * {
    @androidx.annotation.Keep <methods>;
}
// 保留@Keep修飾的字段
-keepclasseswithmembers class * {
    @android.support.annotation.Keep <fields>;
}
-keepclasseswithmembers class * {
    @androidx.annotation.Keep <fields>;
}
// 保留@Keep修飾的構造方法
-keepclasseswithmembers class * {
    @android.support.annotation.Keep <init>(...);
}
-keepclasseswithmembers class * {
    @androidx.annotation.Keep <init>(...);
}

它指定了與@Keep註解相關的所有保留規則,這裏就解釋了爲什麼使用@Keep修飾的成員不會被混淆了吧?

  • 2、Android Asset Package Tool 2 (AAPT2)

在編譯時,AAPT2 會根據對 Manifest 中的類、佈局及其他應用資源的引用來生成aapt_rules.txt,位置在<module-dir>/build/intermediates/proguard-rules/debug/aapt_rules.txt
例如,AAPT2 會爲 Manifest 中註冊的每個組件添加保留規則:

Referenced at [項目路徑]/app/build/intermediates/merged_manifests/release/AndroidManifest.xml:19
-keep class com.have.a.good.time.MainActivity { <init>(); }
省略...

在這裏,AAPT2 生成了MainActivity的保留規則,同時它還指出了引用出處:AndroidManifest.xml:19。這是因爲 啓動 Activity 的過程中,需要使用反射的方式實例化具體的每一個 Activity ,有興趣可以看下 ActivityThread#performLaunchActivity() -> Instrumentation#newActivity()

  • 3、Module

創建新 Module 時,IDE 會在該模塊的根目錄中創建一個 proguard-rules.pro 文件。當然,除了這個自動生成的文件,還可以按需創建額外的規則文件。例如,下面的配置對 release 添加了額外的規則文件:

...
android {
  ...
  buildTypes {
    release {
      minifyEnabled true
      proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
    }
  }
  productFlavors {
    dev{
      ...
    }
    release{
      proguardFile 'release-rules.pro'
    }
  }
}
...

小結一下:

規則文件來源 描述
Android Gradle 插件 在編譯時,由 Android Gradle 插件生成
AAPT2 在編譯時,AAPT2 根據對應用清單中的類、佈局及其他應用資源的引用來生成保留規則
Module 創建新 Module 時,由 IDE 創建,或者另外按需創建

如果將 minifyEnabled 屬性設爲 trueProGuard 或 R8 會將來自上面列出的所有可用來源的規則組合在一起。爲了看到完整的規則文件,可以在proguard-rules.pro 中添加以下配置,輸出編譯項目時應用的所有規則的完整報告:

-printconfiguration build/intermediates/proguard-files/full-config.txt

5、組件化混淆

在組件化的項目中,需要注意應用 Module 和 Library Module 的行爲差異和組件化的資源匯合規則,總結爲以下幾個重點:

  • 編譯時會依次對各層 Library Module進行編譯,最底層的 Base Module 會最先被編譯爲 aar 文件,然後上一層編譯時會將依賴 Module 輸出的 aar 文件/ jar 文件解壓到模塊的 build 中相應的文件夾中
  • App Module 這一層彙總了全部的 aar 文件後,才真正開始編譯操作
  • 後編譯的 Module 會覆蓋之前編譯的 Module 中的同名資源

 

組件化資源彙總

 

Lib Module 彙總到 App Module

使用較高版本的 Android Gradle Plugin,不會將彙總的資源放置在 exploded-aar文件夾。即便如此,Lib Module 的資源彙總到 App Module 的規則是一樣的。


我們通過一個簡單示例測試不同配置下的混淆結果:

  配置一 配置二 配置三 配置四
App Module 開啓混淆 X X
Base Module 開啓混淆 X X

 

示例程序:App Module 依賴了 Base Module

將構建的 apk 包拖到 Android Studio 面板上即可分析 Base 類混淆結果,例如配置一的結果:

 

使用配置一時,Base 類沒有被混淆

全部測試結果如下:

  配置一 配置二 配置三 配置四
App Module 開啓混淆 X X
Base Module 開啓混淆 X X
(結果)Base 類是否被混淆 X X

可以看到,混淆開啓由 App Module 決定, 與Lib Module 無關


現在我們分別在 Lib Module 和 App Module 的 proguard-rules.pro中添加 Base 類的混淆保留規則,並在 build.gradle中添加配置文件,測試 Base 類是否能保留:

-keep class com.rui.base.Base

測試結果如下:

配置位置 Lib Module App Module
(結果)Base 類是否保留 X

可以看到:(默認情況)混淆規則以 App Module 中的混淆規則文件爲準


這裏就引入兩種主流的組件化混淆方案:

  • 在 App Module 中設置混淆規則

這種方案將混淆規則都放置到 App Module 的proguard-rules.pro中,最簡單也最直觀,缺點是移除 Lib Module 時,需要從 App Module 中移除相應的混淆規則。儘管多餘的混淆規則並不會造成編譯錯誤或者運行錯誤,但還是會影響編譯效率。

很多的第三方 SDK,就是採用了這種組件化混淆方案。在 App Module 中添加依賴的同時,也需要在proguard-rules.pro中添加專屬的混淆規則,這樣才能保證release版本正常運行。

  • 在 App Module 中設置公共混淆規則,在 Lib Module 中設置專屬混淆規則

這種方案將專屬的混淆規則設置到 Lib Module 的proguard-rules.pro,但是根據前面的測試,在 Lib Module 中設置的混淆規則是不生效的。爲了讓規則生效,還需要在 Lib Module 的build.gradle中添加以下配置:

...
android{
  defaultConfig{
    consumerProguardFiles 'consumer-rules.pro'
  }
}

其中consumer-rules.pro文件:

-keep class com.rui.base.Base

測試結果表明,Base 類已經被保留了。這種使用consumerProguardFiles的方式有以下幾個特點:

  • consumerProguardFiles只對 Lib Module 生效,對 App Module 無效
  • consumerProguardFiles會將混淆規則輸出爲proguard.txt文件,並打包進 aar 文件
  • App Module 會使用 aar 文件中的proguard.txt彙總爲最終的混淆規則,這一點可以通過前面提到的-printconfiguration證明

最後

如果你看到了這裏,覺得文章寫得不錯就給個讚唄!歡迎大家評論討論!如果你覺得那裏值得改進的,請給我留言。一定會認真查詢,修正不足,定期免費分享技術乾貨。感興趣的小夥伴可以點一下關注哦。謝謝!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章