Java 混淆那些事(一):重新認識 ProGuard

QuincySx發表於2019-03-24

本文已授權微信公眾號「玉剛說」獨家釋出。

大家好,你現在看到的是「Java 混淆那些事」系列文章的第一篇,通過這個系列我想帶大家重新認識一下 ProGuard 到底能幹什麼?最終領悟怎麼才能寫好混淆規則。所以說這個系列文章的重點將會放到書寫 keep 規則上面。我會最大程度用大白話寫明白。

首先我們瞭解一下 ProGuard 到底是什麼能幹什麼?

ProGuard 是可以對 Java 類檔案進行壓縮、優化、混淆和預驗證的工具。

簡單解釋一下 ProGuard 的功能

  • 壓縮 (Shrinker):刪除無效的類、欄位、方法等。
  • 優化 (Optimizer):優化位元組碼,合併方法,刪除無用欄位等。
  • 混淆 (Obfuscator):將類名、屬性名、方法名以及欄位名混淆為難以讀懂的字母,比如a, b, c等。
  • 預校驗 (Preverifier):對 class 檔案進行預檢驗,確保虛擬機器載入的 class 檔案是安全並且可以執行的。

我們再來看下一個問題 ProGuard 是以什麼樣的流程進行工作的。

Java 混淆那些事(一):重新認識 ProGuard

  1. 壓縮階段 ProGuard 會從「程式碼入口點」開始遞迴查詢,把用到的類或變數等留下來,沒用到的全都刪掉。

  2. 優化階段 ProGuard 會優化經過壓縮階段留下來的類,比如將外部沒有呼叫的非程式碼入口點的方法或類改為私有的,又或者把一部分方法改為 final 的,相應的欄位改為 static、final,或者把幾個方法合併成一個,刪除沒有用到的引數等等的優化操作。

  3. 混淆階段 將程式碼入口點呼叫到的類和方法(非程式碼入口點方法),給他改個名字,比如簡短的或者複雜的,這個過程中重新命名的字典可以自定義。改完名字後還能保證程式的正常執行邏輯。

  4. 預校驗階段 在編譯版本為 Java ME 或 1.6 以及更高版本時是預設開啟的。但編譯成 Android版本時,預校驗是不必須的。

那麼程式碼入口點到底是什麼呢?

好的現在就忘記以上這些廢話,我們重點來看第一個知識點「程式碼入口點」。我們剛才應該也看到了 ProGuard 壓縮階段是從程式碼入口點開始遞迴查詢用到的程式碼的。

舉個例子:比如你寫了一個很方便的下載類,假設需要使用的就這一個方法 new DowonloadClien("url").start() 那麼這個方法就應該指定為程式碼入口點。

ProGuard 怎麼知道哪裡是程式碼入口點的呢? 沒錯這個程式碼入口點如果我們不告訴 ProGuard,他是不會知道的。那麼怎麼告訴他呢?我們通過 keep 規則就可以告訴 ProGuard 了,具體用法我們以後文章中具體說,這裡就瞭解一下。

舉個例子

下面我們寫一個通俗的小例子,配合程式碼理解一下。看看壓縮、優化、混淆這些功能。

//測試程式碼,如下程式碼純屬為了測試,除此之外沒有任何合理性。
src
 -> model
    -> ModelA.java
        int testA = 2;
        
        public void modelA(int age) {
            int a = 1 + age;
            int b = testA + age;
            System.out.println("print " + b);
        }
        
        public void modelB(String name) {
            System.out.println("print " + name);
        }

    -> ModelB.java
        public void modelA(String name) {
            System.out.println("print " + name);
        }

        public void modelB(String name) {
            System.out.println("print " + name);
        }

 -> utils
    -> UtilsA.java
        private static final String UtilA = "utila";

        public static void printA() {
            System.out.println("print " + UtilA);
        }

        public static void printB() {
            System.out.println("print B");
        }

    -> UtilsB.java
        public static void printA(){
            System.out.println("print A");
        }

        public static void printB(){
            System.out.println("print B");
        }

 Main.java
        public static Main sMain = null;

        public static void main(String[] args) {
            sMain = new Main();
            sMain.run();
        }

        private void run() {
            ModelA modelA = new ModelA();
            modelA.modelA(5);
            UtilsA.printA();
        }

//我們先不新增任何混淆引數,混淆之後的結果

src
 -> a
    -> a.java
        private int a = 2;
        public final void a(int i) {
            System.out.println("print " + (this.a + 5));
        }

 -> defpackage
    -> Main.java
        private static Main a = null;

        public static void main(String[] strArr) {
            a = new Main();
            new a().a(5);
            System.out.println("print utila");
        }
複製程式碼

對比一下混淆前和混淆後的 Jar 包內容

看到幾個很顯然的效果

  1. 沒有被程式碼入口點呼叫到的類、方法都刪除了。
  2. 定義的多個變數也都合併到一起了,甚至完全消失不見了。
  3. 很多方法也進行了合併。
  4. 除了程式碼入口點之外,留下來方法名和變數名全都改變了。
  5. 優化了程式碼,可以看到上面 public static Main sMain = null; 混淆完自動給改成了 private。
  6. 他還會自動把一部分方法優化為 final 的。

為什麼 Main 這個類以及 main 方法沒有被混淆呢?

在 ProGuard 預設生成的配置檔案下有個條匹配規則

-keepclasseswithmembers public class * {
    public static void main(java.lang.String[]);
}
複製程式碼

解釋一下:匹配每個類裡面的 main 方法為程式碼入口點,如果沒有任何一個類有 main 方法。那麼我們的上面的例子就是空的檔案了,因為在壓縮階段就已經把所有程式碼全都刪了。

main 方法是 Java 應用程式的入口方法,程式執行執行的第一個方法。

小結

經過這個小例子,除了預校驗之外,其他特性我們都已經明顯的看到了。概念也大概的懂了。恭喜你打怪升級成功,快去看看下一篇吧。

相關文章