1. 程式人生 > >Android Proguard工具使用和配置詳解

Android Proguard工具使用和配置詳解

Android開發中的Proguard

Proguard是Android開發時經常會用到的一個工具,在Android SDK中已經集成了一個免費的Proguard版本,位於<sdk>/tools/proguard目錄中。

在Android專案中,通過修改module下面的build.gradle檔案來開啟使用Proguard選項,當開啟了此選項後,Android Studio在編譯該module時就會使用指定的配置來對編譯之後的Java位元組碼進行處理,得到一個優化後的jar包。

在最新的Android Studio 2.1.2版本建立的Android工程中,module中的build.gradle有如下一段配置。這裡的minifyEnabled即用來控制在編譯時是否需要啟用Proguard,將minifyEnabled修改為true,即表示啟用Proguard。proguardFiles配置的是Proguard對此module進行處理時使用的配置檔案,’proguard-android.txt’是Android SDK中自帶的一個基本Progurad配置檔案,它同樣位於<sdk>/tools/proguard目錄中,’proguard-rules.pro’則是當前module所在目錄中的一個配置檔案,預設是空白的,需要由開發者自行實現,當啟用了Proguard之後需要編輯這個檔案,向其中新增適合當前專案的Proguard配置。本文後面會對Proguard的配置進行詳細解析。

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

除了’proguard-android.txt’,Android SDK中還自帶了另外一個Pruguard配置檔案’proguard-android-optimize.txt’,它同樣位於<sdk>/tools/proguard目錄中,和’proguard-android.txt’ 的區別在於,’proguard-android-optimize.txt’中開啟了Proguard optimize的選項(optimize是Proguard的一項功能),而’proguard-android.txt’ 中沒有開啟optimize選項。如果需要開啟optimize,可以將這裡的’proguard-android.txt’修改為’proguard-android-optimize.txt’。當然,如果熟悉Proguard的配置,也可以直接編輯module中的’proguard-rules.pro’,向其中新增optimize的配置。

這裡有幾點需要注意的地方:

  1. 在較早的Android Studio 版本中,這裡的一些選項名字可能會有所不同,可能會看到runProguard這樣的配置。這是因為Android Plugin for Gradle 在0.14.0版本中修改了DSL中的幾個名字。雖然在實際專案中不會再使用這麼老的版本了,但是網路上很多文章仍然是在老版本基礎上寫的。參考: http://tools.android.com/tech-docs/new-build-system

  2. Proguard雖然被整合到了Android SDK中,但是Proguard並非是Google開發的,他最早是個人開發者Eric Lafortune在業餘時間做的一個開源專案,後來Eric Lafortune加入了GuardSquare(一家總部在比利時的公司)擔任CTO,Proguard也被GuardSquare當做公司的產品來宣傳,不過仍然是開源且免費的。 GuardSquare還有另外一款基於Proguard的產品Dexguard,這款產品是收費的,當然功能上也比Proguard要強大。從名字上就可以看出這款產品是專門針對Android APK優化的。

  3. 在Android Plugin for Gradle 2.0版本中集成了一個實驗性質的工具,可以用來對程式碼進行shrinker,也就是可以去掉程式碼中沒有用到的那些變數,方法和類。目前這個工具仍然是experimental,由於沒有混淆和優化的功能,和Proguard相比沒有任何優勢。也許Google會繼續開發,未來可能會像Android Studio取代Eclipse + ADT 一樣,取代掉Proguard,當然也可能會廢棄掉。
    要開啟這個工具,可以參考 http://tools.android.com/tech-docs/new-build-system/built-in-shrinker

Proguard功能介紹

Proguard經常被看做Android平臺的程式碼混淆工具,這種看法是比較片面的。Proguard專案誕生於2002年,而Android 1.0是2008年才釋出的,也就是說早在Android釋出之前Proguard就已經存在很久了。Proguard不僅適用於Android專案,也適合對其他使用Java開發專案的優化。此外,Proguard也不僅僅是一個程式碼混淆工具,程式碼混淆只是Proguard四項功能中的其中一項。它之所以被認為是Android平臺的程式碼混淆工具,是因為Google將其整合到了Android SDK和Android專案的編譯過程中,成為Android開發預設的程式碼優化工具,從而被廣大的Android開發者所熟悉。此外,開發者對程式碼混淆功能的需求比其他功能要迫切的多,Proguard程式碼混淆功能成為開發者必選的一項功能。

Proguard幫助文件中是這樣描述的

ProGuard is a Java class file shrinker, optimizer, obfuscator, and preverifier.

從這段話中可以看出,Proguard可以對Java class檔案執行shrink,optimize,obfuscate和preverify四項優化。這四項優化也代表了Proguard的四項功能。

shrink

shrink功能的作用是移除程式碼中沒有使用到的類,方法和成員變數,從而減少檔案大小。

這些沒有使用到的類,方法和成員變數的產生主要有兩種情況,一種是自身程式碼中由於功能,需求的變更,或者程式碼的重構,導致原先的一些程式碼不再被使用,這些程式碼有時會忘記刪除,有些則是故意保留下來,以備以後使用,這些被廢棄或故意存留的程式碼對程式的執行沒有任何用處。另一種是專案中經常會包含一些開原始碼或第三方SDK,而專案通常只會使用到開原始碼或第三方SDK中的部分功能,那些沒有用到的功能對應的程式碼同樣是可以去掉的。

所有這些沒有用處的類,方法和成員變數,對應用的執行沒有任何用處,但是它們都會被編譯到jar檔案中,不僅會增大jar檔案的大小,而且在Android平臺上可能還會導致方法數超過64k,引起一些不必要的問題。所以最好能夠將這些沒有用處的類,方法和成員變數都從jar檔案中刪除。但是,如果靠開發者手動刪除是很費事費力的。一方面,如果第三方SDK不是開源的,或者專案沒有將原始碼包含進來,是無法手動刪除的。另一方面,手動從專案中刪除程式碼是有副作用的,如果後面又需要用到這部分程式碼,又得費力加回來。因此,最好是可以有工具自動完成這項工作。

Proguard的shrink功能就是用來執行這項操作的,它會自動分析jar包中各個類,方法之類的呼叫和依賴關係,對那些沒有用到的類,方法和成員變數進行剔除。

optimize

optimize過程會在Java位元組碼層面上進行優化,剔除方法中一些冗餘的呼叫。幫助文件中列出了一些當前Proguard支援的優化。

對常量表達式進行計算
刪除不必要的欄位訪問和方法呼叫
刪除不必要的程式碼分支
刪除不必要的比較和instanceof測試
刪除未使用的程式碼塊
合併相同的程式碼塊
減少變數的分配
刪除只賦值但沒有使用的成員變數,以及未使用的方法引數
內聯常量欄位,方法引數和返回值
內聯很短的方法和只被呼叫一次的方法
簡化尾遞迴呼叫
合併類和介面
儘可能的將方法修飾為private,static和final
儘可能的將類修飾為static和final
移除只被一個類實現的介面
其他200多項細節優化,比如用 <<1 代替 *2 運算
刪除Log的相關程式碼(可選)

obfuscator

obfuscator也就是Proguard最常被提到的程式碼混淆功能。由於Java程式碼編譯之後的class檔案中仍然包含了除錯資訊,原始檔名,行號,類名,方法名,成員變數名,引數名,區域性變數名等資訊,通過反編譯可以很容易的將這些資訊還原出來。通過obfuscator,可以將jar包中不需要對外暴露的類名、方法名和變數名替換成一些簡短的,對人來說沒有意義的名字。

以下是一段程式碼混淆前和混淆後的對比。可以看到類名,成員變數名,方法名,引數名和區域性變數名都被替換成了一些簡單無意義的名字,從混淆後的程式碼中就很難理解原先程式碼的邏輯。然而這兩段程式碼對計算機來說是完全等價的。此外,由於混淆後jar包中原先很長的名字被替換成了簡短的名字,這使得jar包的體積更小了。這也是混淆帶來的另一個附加的好處。

public enum Edge {
    private float mCoordinate;

    public static float getWidth() {
        return Edge.RIGHT.getCoordinate() - Edge.LEFT.getCoordinate();
    }

    public static float getHeight() {
        return Edge.BOTTOM.getCoordinate() - Edge.TOP.getCoordinate();
    }

    private static float adjustLeft(float x, Rect imageRect, float imageSnapRadius, float aspectRatio) {

        float resultX = x;

        if (x - imageRect.left < imageSnapRadius)
            resultX = imageRect.left;
        else {
            float resultXHoriz = Float.POSITIVE_INFINITY;
            float resultXVert = Float.POSITIVE_INFINITY;

            if (x >= Edge.RIGHT.getCoordinate() - MIN_CROP_LENGTH_PX)
                resultXHoriz = Edge.RIGHT.getCoordinate() - MIN_CROP_LENGTH_PX;

            if (((Edge.RIGHT.getCoordinate() - x) / aspectRatio) <= MIN_CROP_LENGTH_PX)
                resultXVert = Edge.RIGHT.getCoordinate() - (MIN_CROP_LENGTH_PX * aspectRatio);

            resultX = Math.min(resultX, Math.min(resultXHoriz, resultXVert));
        }
        return resultX;
    }
}
public enum a {
  private float e;

  public static float a() {
    return c.c() - a.c();
  }

  public static float b() {
    return d.c() - b.c();
  }

  private static float a(float paramFloat1, Rect paramRect, float paramFloat2, float paramFloat3) {
    float f1 = paramFloat1;
    if (paramFloat1 - paramRect.left < paramFloat2)
      f1 = paramRect.left;
    else {
      float f2 = Float.POSITIVE_INFINITY;
      float f3 = Float.POSITIVE_INFINITY;
      if (paramFloat1 >= c.c() - 40.0F)
        f2 = c.c() - 40.0F;

      if ((c.c() - paramFloat1) / paramFloat3 <= 40.0F)
        f3 = c.c() - 40.0F * paramFloat3;

      f1 = Math.min(f1, Math.min(f2, f3));
    }
    return f1;
  }

preverifier

preverifier用來對Java class進行預驗證。預驗證主要是針對Java ME開發來說的,Android中沒有預驗證過程,所以不需要用到這項功能。

Proguard工作過程

總體工作過程

在Proguard幫助文件中給出了一個Proguard工作流程圖

這裡寫圖片描述

可以看到,Proguard會對輸入的jar檔案按照shrink - optimize - obfuscate - perverify的順序依次進行處理,最後得到輸出jar檔案。Proguard使用library jars來輔助對input jars類之間的依賴關係進行解析, library jars自身不會被處理,也不會被包含到output jars中。

Entry points(進入點)

設想我們手動對一個jar檔案進行shrink處理,為了決定這個jar檔案中哪些類和方法應該被保留,我們需要分析jar檔案中方法之間的呼叫過程,得到一張方法之間的依賴關係圖。然而這時仍然不知道哪些類和方法需要保留,不能因為一個類的某個方法被另外一個類的某個方法呼叫了,就認為這兩個類和這兩個方法就應該被保留,它們可能都沒有被其他程式碼所呼叫。仔細分析這個過程,由於jar檔案是一個孤立的個體,無論其內部有怎樣複雜的呼叫和依賴關係,如果我們不指定一個搜尋的起點的話,那麼整個jar包中所有的類和方法都是可以被shrink的。這個搜尋的起點就是jar檔案對外提供的一個入口。這個入口可能是Java中的main方法,可能是Android中的四大元件,可能是一個SDK對外提供的APIs…總之,要執行shrink之前必須先指定一個或若干個入口。

在Proguard中,將jar檔案的入口稱為Entry points。在Proguard的四項功能中,只有preverifier不需要用到Entry points,其他三項功能都必須在配置中指定Entry points後才可以執行。

Proguard在執行shrink時會將Entry points作為起點,遞迴的搜尋整個jar檔案中類和方法之間的呼叫關係圖,只有通過Entry points直接或間接呼叫的那些方法和類才會被保留,其他的類和方法都將被刪除。

在optimize過程中,沒有被指定為Entry points的類和方法可能會被修飾為private, static以及final,方法中沒有用到的引數可能會被移除,有些方法還可能會被內聯(整個方法被刪除,方法的程式碼直接拷貝到呼叫處,替代原先的方法呼叫)。這些優化的目的是為了提高執行的效率,並附帶的減少一些包體大小。但如果jar包一個類和方法需要被外部使用,則顯然不能執行這類優化,否則外部將不能通過原先約定的方式來使用這些類和方法。所以,Proguard對指定Entry points的類和方法不會執行這些優化。

在obfuscator過程中,如果一個類和方法沒有被指定為Entry points,則這個類和方法的名字將會被重新命名為無意義的名字。同樣,如果jar包一個類和方法需要被外部使用,則顯然不能執行這類修改,否則外部將不能通過原先約定的名字來使用這些類和方法。所以,Proguard對指定Entry points的類和方法不會執行混淆操作。

Entry points和反射機制

在jar檔案內部,可能會有一部分類和方法是通過Java反射方式來呼叫的,Proguard在分析jar包中類和方法之間的呼叫關係時,會考慮到反射方式的呼叫。如下反射方式呼叫的類和方法能夠被Proguard正確的分析,其中”SomeClass”,”someField”,”someMethod”指的是某個編譯時的字串常量,SomeClass是某個明確的型別。

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

例如,在某段程式碼中執行了Class.forName(“MyClass”),說明這裡需要引用MyClass類,因此MyClass類不能被shrink(假設這段程式碼所在的方法本身不會被shrink),同時在執行obfuscate時,如果MyClass的類名可以被混淆(沒有指定為Entry points),則在重新命名MyClass類名的同時會修改Class.forName()引數中的字串。

然而不幸的是,很多時候Proguard無法準確的判斷出所有通過反射方式來呼叫的類和方法,這主要是那些在執行時才能確定的名字的反射呼叫。例如,在某段程式碼中執行了Class.forName(getPackageName()+ “.R”),Proguard在分析該段程式碼時,並不知道這裡的getPackageName()+ “.R”指的是哪個類,所以也就無從知道是否要保持該類不被shrink和obfuscate。在這種情況下,雖然這個類並不會被jar包外部所使用,也需要將該類指定為Entry points。不然的話,如果這個類沒有被程式碼中其他地方使用,那麼它會在shrink時被刪除,即使沒有被刪除,它也會在obfuscate時被重新命名為其他的名字,這樣當代碼執行到Class.forName(getPackageName()+ “.R”)時還是無法找到這個類。

所以,需要將程式碼中那些通過反射方式呼叫,又不能自動推斷出呼叫關係的類和方法手動新增到Entry points的配置中。

Proguard使用

Proguard工具目錄結構

Proguard工具目錄結構如圖所示。

這裡寫圖片描述

  • lib目錄
    lib目錄中包含了Proguard工具對應的jar檔案,其中又包含三個檔案:proguard.jar,proguardgui.jar和retrace.jar。
    Proguard四項核心功能shrink,optimize,obfuscate和preverify的執行都是由proguard.jar來完成的,不過proguard.jar只能通過命令列方式來使用。
    proguardgui.jar是Proguard提供的一個圖形介面工具,通過proguardgui.jar可以方便的檢視和編輯Proguard配置,以及呼叫proguard.jar來執行一次優化過程。
    retrace.jar主要在debug時使用。混淆之後的jar檔案執行過程如果出現異常,生成的異常資訊將很難被解讀,方法呼叫的堆疊都是一些混淆之後的名字,通過retrace.jar可以將異常的堆疊資訊中的方法名還原成混淆前的名字,方便程式解決bug。

  • bin目錄
    bin目錄中包含了幾個bat和shell指令碼,通過這些指令碼可以直接執行proguard.jar,proguardgui.jar和retrace.jar。如果將bin目錄新增到環境變數中,就可以直接在命令列中執行proguard,proguardgui和retrace命令了,避免每次都要輸入java -jar + <proguard路徑>/lib/<jar檔名> + <引數>這一長串資訊來呼叫proguard 。不過proguard.bat,proguard.sh,retrace.bat和retrace.sh在執行時都需要在後面加上引數,所以它們還是隻能通過命令列的方式來執行。proguardgui.bat和proguardgui.sh則可以不用引數,可以直接雙擊開啟proguardgui.jar的圖形介面。

  • src目錄
    Proguard是開源的,src目錄中是Proguard原始碼,都是Java程式碼。

  • buildscripts目錄
    buildscripts目錄中是Proguard的編譯指令碼,提供了shell,makefile,Ant和Maven四種方式來編譯Proguard原始碼。

  • docs目錄
    docs目錄是Proguard提供的幫助文件,通過index.html來檢視。

  • examples目錄
    examples目錄提供了一些針對不同平臺的Proguard配置檔案示例,例如android.pro中包含了對Android平臺Proguard配置的說明。

proguard.jar的使用

使用proguard.jar有幾種方式。

  1. 通過命令列執行”java -jar + <proguard路徑>/lib/proguard.jar + <引數>”
  2. 通過命令列執行”<proguard路徑>/bin/proguard.bat(Windows) + <引數>” 或者 “<proguard路徑>/bin/proguard.sh+ <引數>”(Linux)。如果已經將”<proguard路徑>/bin”路徑新增到環境變數中,則可以直接執行proguard.bat(Windows) + <引數>” 或者 “proguard.sh+ <引數>”(Linux)

這裡的<引數>可以是Proguard支援的任意配置選項,Proguard的配置選項均以-開頭。不過由於Proguard一次執行過程中通常都需要很多配置選項,所以一般都會將所有需要的配置選項儲存為配置檔案的形式,以便重複使用和修改,proguard.jar也支援用配置檔案作為引數,不同的是配置檔案以@開頭。此外,proguard.jar還可以支援配置檔案和配置選項的混合形式。如下的寫法是正確的。

java -jar proguard.jar -injars myapp. jar -outjars myapp_out.jar -libraryjars 'D:\android-sdk\platforms\android-23\android.jar' // 只使用配置選項
java -jar proguard.jar @myconfig.pro                // 只使用配置檔案
java -jar proguard.jar @myconfig.pro -verbose       // 混合使用配置檔案和配置選項

proguardgui.jar的使用

使用proguardgui.jar有幾種方式。

  1. 通過命令列執行”java -jar + <proguard路徑>/lib/proguardgui.jar + <引數>”
  2. 通過命令列執行”<proguard路徑>/bin/proguardgui.bat(Windows) + <引數>” 或者 “<proguard路徑>/bin/proguardgui.sh+ <引數>”(Linux)。如果已經將”<proguard路徑>/bin”路徑新增到環境變數中,則可以直接執行proguardgui.bat(Windows) + <引數>” 或者 “proguardgui.sh+ <引數>”(Linux)

這裡的<引數>不是必須的,如果沒有引數,只只打開圖形介面。如果包含了引數,引數必須是Proguard配置檔案,proguardgui.jar不支援用配置選項作為引數。對作為引數的配置檔案,直接用檔名即可,不需要在檔名前加@等符號。如下的寫法是正確的。

java -jar proguardgui.jar                   // 不使用配置檔案
java -jar proguardgui.jar @myconfig.pro     // 使用配置檔案

retrace.jar的使用

使用retrace.jar有幾種方式。

  1. 通過命令列執行”java -jar + <proguard路徑>/lib/retrace.jar + <引數>”
  2. 通過命令列執行”<proguard路徑>/bin/retrace.bat(Windows) + <引數>” 或者 “<proguard路徑>/bin/retrace.sh+ <引數>”(Linux)。如果已經將”<proguard路徑>/bin”路徑新增到環境變數中,則可以直接執行retrace.bat(Windows) + <引數>” 或者 “retrace.sh+ <引數>”(Linux)

這裡的<引數>包含三個部分:retrace選項,mapping檔案和包含異常堆疊資訊的檔案,其中mapping檔案是必須的,其他兩個部分是可選的。mapping檔案是在執行proguard.jar時通過-printmapping選項生成的文字檔案,此檔案中包含了jar檔案混淆前的類,方法和成員變數名,以及它們混淆後對應的名字。包含異常堆疊資訊的檔案是Java中Exception物件的printStackTrace()方法打印出來的堆疊資訊儲存的文字檔案,如果沒有在引數中指定這個檔案,則會從標準輸入中讀取。retrace支援-verbose和-regex兩個配置選項。-verbose表示輸出結果中不僅包括堆疊資訊中的方法名,還包括返回值和引數。-regex用來指定一個正則表示式,指定的正則表示式會被用來匹配堆疊資訊中的類名,方法名等,一般來說不需要指定這個選項,除非堆疊資訊比較特殊,retrace預設的規則無法解析。如下的寫法是正確的。

java -jar retrace.jar mapping_file
java -jar retrace.jar mapping_file exception_statck_file.txt
java -jar retrace.jar -verbose mapping_file exception_statck_file.txt

Proguard配置選項

這裡的Proguard配置選項指的是proguard.jar所支援的配置選項。

配置檔案規則

由於大多數時候都是將Proguard配置選項寫到配置檔案中來使用,所以在介紹Proguard配置選項前,先介紹Proguard配置檔案的一些規則。

  1. 配置檔案中不能包含配置選項和註釋之外的其他字元。
  2. 配置檔案中可以添加註釋,註釋以#開頭,從#到該行末尾的文字都會看做註釋。不支援多行註釋。
  3. 在配置檔案中空格和換行有相同的效果,能用空格的地方都可以用換行,反之亦然。不過一般來說,空格用來分割配置選項和對應的值,換行用來分割不同的配置選項。此外,多餘的空格和換行會被忽略。
  4. 每條配置規則均以-開頭,-和後面的選項名之間不能有空格,例如’-verbose’寫成’- verbose’是不能被識別的。
  5. 有些配置規則中需要指定檔案或資料夾的路徑,如果路徑中包含空格,則需要將路徑用單引號或雙引號括起來。如果沒有空格,可以不需要引號。
  6. 配置規則中除-injars,-outjars之間有先後順序上的要求外,其他的不同配置選項在配置檔案中可以按照任意的順序來配置。

-injars,-outjars和-libraryjars

配置選項-injars表示輸入的檔案,也就是需要被Proguard處理的檔案。-outjars表示輸出檔案,也就是處理完畢後的檔案,-libraryjars表示-injars檔案執行時所依賴的jar檔案,例如Java執行時rt.java,Android執行時android.jar等。這幾個選項的配置規則如下。

  1. -injars,-outjars和-libraryjars不僅支援jar檔案,還支援 aar, war, ear, zip和apk格式的檔案,還可以指定一個包含這些型別檔案的資料夾。Proguard會讀取-injars中指定的檔案(為了簡化描述,這裡所說的檔案包含了資料夾的情況,對資料夾就是遍歷資料夾中所有支援的上述型別的檔案,下同)中的Java class檔案,對其進行處理,得到目標class檔案,最後將其打包到-outjars指定的檔案中。
  2. -injars檔案和-libraryjars檔案可以有多個,有幾個檔案,就需要寫幾條配置,不能在一條配置中寫多個檔案。以下是正確的寫法。

    -injars 'libs\in1.jar'
    -injars 'libs\in2.jar'
    -libraryjars 'android.jar'
    -libraryjars 'android-support-v4.jar'

    -injars檔案和-libraryjars檔案有多個時,配置的順序沒有影響。例如上面的寫法和下面這種寫法是等價的。

    -injars 'libs\in2.jar'
    -injars 'libs\in1.jar'
    -libraryjars 'android-support-v4.jar'
    -libraryjars 'android.jar'

    -injars檔案有多個時,要保證多個injars中不能包含相同名字的類,例如,上述in1.jar和in2.jar中都包含com.ccpat.test類,則無法處理。-libraryjars沒有這個限制。

  3. -injars,-outjars和-libraryjars都支援filter,filter是一個用來匹配類名的可以包含萬用字元的字串。對-injars和-libraryjars來說,filter用來匹配檔案中需要處理的class的範圍,不在範圍內的class不會被解析和處理,也不會被包含到-outjars檔案中,對-outjars來說,filter用來指定需要儲存到此檔案中的class的範圍。如果需要在-injars,-outjars和-libraryjars配置中增加filter,可以在檔名後加上(),然後在()中寫上符合filter語法的匹配字串。例如

    -injars 'libs\in1.jar(com/ccpat/**.class)'   # 只處理in1.jar中com.ccpat包名下的類
    -injars 'libs\in2.jar(!rx/**.class)'         # 不處理in2.jar中rx包名下的類
    -outjars libs\out1.jar(com/**.class)         # 將對in1.jar和in2.jar處理完成後的結果中com包名下的類儲存到out1.jar中
  4. -outjars也可以有多個,不過需要使用filter,沒有filter的-outjars後面不能直接再跟其他的-outjars。例如。

    -injars 'libs\in1.jar(com/ccpat/**.class)'
    -injars 'libs\in2.jar(!rx/**.class)'
    -outjars libs\out1.jar(com/**.class)           # 將處理完成後的結果中com包名下的類儲存到out1.jar中
    -outjars libs\out2.jar(org/**.class)           # 將處理完成後的結果中org包名下的類儲存到out2.jar中
    -outjars libs\out3.jar                         # 將處理完成後的結果中除com和org之外的包名下的類儲存到out3.jar中
    -outjars libs\out4.jar(de/**.class)            # 錯誤的用法,在上一條-outjars中沒有包含filter,所以不能在後面直接加-outjars配置。
                                                   # 因為這時已經沒有其他的類可以儲存到這個檔案中了。
  5. -injars和-outjars是有順序要求的,-injars配置應該在-outjars配置之前。對-libraryjars則沒有順序要求,它可以配置在-injars和-outjars之前,也可以配置在後面。甚至可以配置在兩條-injars或-outjars中間。以下是正確的寫法。

    -injars 'libs\in1.jar'
    -libraryjars 'android.jar'
    -injars 'libs\in2.jar'
    -outjars libs\out1.jar(com/**.class)
    -libraryjars 'android-support-v4.jar'
    -outjars libs\out2.jar

    以下是錯誤的寫法。

    -outjars libs\out1.jar
    -injars 'libs\in1.jar'
    -injars 'libs\in2.jar'
  6. -injars,-outjars和-libraryjars有多個時,不能出現相同的檔案,如下配置均是錯誤的。

    -injars libs\in.jar
    -injars libs\in.jar
    -outjars libs\out.jar
    -outjars libs\out.jar
    -libraryjars 'android.jar'
    -libraryjars 'android.jar'

    -injars,-outjars和-libraryjars之間配置的檔案也不能是同一個檔案,如下配置均是錯誤的。

    -injars libs\in.jar
    -outjars libs\in.jar
    -injars 'android-support-v4.jar'
    -libraryjars 'android-support-v4.jar'
    -outjars libs\out.jar
    -libraryjars libs\out.jar
  7. 可以同時配置多組-injars和-outjars,例如,如下配置會將in1.jar和in2.jar處理結果儲存到out1.jar中,in3.jar的處理結果儲存到out2.jar中。

    -injars libs\in1.jar
    -injars libs\in2.jar
    -outjars libs\out1.jar
    -injars libs\in3.jar
    -outjars libs\out2.jar

    當配置多組-injars時,-libraryjars檔案是可以被這多組-injars共用的,不存在配置多組-libraryjars檔案的概念。

    當配置多組-injars時,每一組的-injars和-outjars需要遵循規則5,也就是每一組的-injars都需要配置在-outjars前面。

    當配置多組-injars和-outjars,在不同的組中的jar檔案可以有相同的類,這點和同一組中是不一樣的,例如,上述in1.jar和in3.jar中都包含com.ccpat.test類,是可以正常處理的。

    當配置多組-injars和-outjars,即使在不同的組,同一個檔案也不能出現兩次。如下配置是錯誤的。

    -injars libs\in1.jar
    -injars libs\in2.jar
    -outjars libs\out1.jar
    -injars libs\in1.jar
    -outjars libs\out2.jar

    結合第6條規則,可以得出這樣一條規則:在一個配置檔案中-injars,-outjars和-libraryjars檔案都不能相同。

  8. 配置的-injars在被Proguard處理時可以當做-libraryjars來使用。例如有如下配置。

    -injars libs\in1.jar
    -injars libs\in2.jar
    -outjars libs\out1.jar

    假如in1.jar和in2.jar並非是完全獨立的,它們之間有相互依賴關係,in1.jar中的程式碼會呼叫in2.jar中的程式碼,in2.jar中的程式碼也會呼叫in1.jar中的程式碼,這種情況是可以被Proguard正確處理的,不需要再將in1.jar和in2.jar宣告為-libraryjars(事實上也無法將這兩個檔案宣告為-libraryjars)

    這點對有多組-injars和-outjars配置的情況也是適用的。例如有如下配置。

    -injars libs\in1.jar
    -outjars libs\out1.jar
    -injars libs\in2.jar
    -outjars libs\out2.jar

    假如這裡的in1.jar和in2.jar也同樣存在相互依賴關係,in1.jar中的程式碼會呼叫in2.jar中的程式碼,in2.jar中的程式碼也會呼叫in1.jar中的程式碼,這種情況也是可以被Proguard正確處理的。

類的模板class specification

由於在之後的一系列 -keep 宣告中需要使用到類的模板,這裡先講述下Proguard中類的模板的表示方法。

當需要在Proguard配置中表示一組類及其成員(包括成員變數和成員方法)的時候就需要用到類的模板,Proguard中類的模板看起來和Java中去掉一個類的方法實現部分後剩餘部分很像(類似於介面的定義),其中類名,成員名等可以用一些特定的萬用字元來表示,以匹配多個不同的類和成員。

它的完整語法如下(來源於官方文件)。”[]”中的內容表示可選的, “…”表示可以附加任意多個該欄位,”|”表示可以從一組選項中任意選擇一個。可以看到整個模板由一組可選或必選的欄位組合而成,其中只有interface|class|enum和classname 是必選的欄位,其他欄位都是可選的。

[@annotationtype] [[!]public|final|abstract|@ ...] [!]interface|class|enum classname [extends|implements [@annotationtype] classname]
[{
    [@annotationtype] [[!]public|private|protected|static|volatile|transient ...] <fields> | (fieldtype fieldname);
    [@annotationtype] [[!]public|private|protected|static|synchronized|native|abstract|strictfp ...] <methods> | <init>(argumenttype,...) | classname(argumenttype,...) |(returntype methodname(argumenttype,...));
    [@annotationtype] [[!]public|private|protected|static ... ] *;
    ...
}]

對這裡的各個欄位分別解析如下。

以下是模板中類名的表示

  1. @annotationtype
    這是一個可選的欄位,用來修飾類,表示該類附加的annotation。例如。

    @Deprecated public class *     // 可以匹配所有被@Deprecated修飾的類
    
  2. public|final|abstract|@
    這是一個可選的欄位,用來修飾類,public|final|abstract表示該類的修飾符,@表示需要annotation,可以任意組合使用,前面可以加上!表示不匹配該修飾符。需要注意的是這裡沒有private和protected修飾符。例如

    // 匹配所有是public,但不是final,不是abstract的類。
    public !final !abstract class *
    
    // 匹配所有是public,且有被註解修飾的類。
    public @class *
  3. interface|class|enum
    這是一個必選欄位,interface表示這是一個介面型別,class既可以匹配介面,也可以匹配類,enum表示這是一個列舉型別,前面可以加上!表示不匹配。此欄位只能從這三個中任意選取一個,不能組合使用。例如。

    // 匹配所有class和interface
    public class *
    
    // 匹配所有interface
    public interface*
    
    // 匹配不是interface的其他型別,也就是匹配classenum
    public !interface*
    
    // 匹配不是enum的其他型別,等同於public class *
    public !enum *
  4. classname
    classname是一個必選的欄位,表示類的名字,這裡必須使用完整的名字,也就是要帶上包名,例如要表示String類需要用java.lang.String,而不是直接用String,如果要表示內部類,可以用$來分割外部類和內部類的名字,例如 android.view.View$OnClickListener。
    classname中還可以包含如下的萬用字元。

    ? : 用來匹配除包分隔符.之外的其他任意單個字元
    * : 用來匹配任意多個字元(包括0個),匹配不能跨越包分隔符.。
    ** : 用來匹配任意多個字元(包括0個),和*不同的是,**匹配可以跨越包分隔符。

    *使用時有一個特例,單獨的*可以表示任意類,也就是可以跨包分隔符,和單獨的** 等價。
    以下是一些例子

    模板 可以匹配示例 不能匹配示例
    my.test? my.test1; my.testx my.test ; my.test12 ; my.test.1
    my.*test* my.test ; my.thistest ; my.testapp ; my.thistestapp my.sub.test ; my.test.app
    my.* my.abc ; my.abc123 my.sub.abc
    * abc ; myabc, my.abc
    **.test my.test ; your.sub.test my.testabc
    my.** my.app ; my.sub.app your.app
    ** my.app ; your.sub.test
  5. extends|implements
    這是一個可選的欄位,表示類的繼承或對介面實現的約束。例如

    // 匹配所有繼承自android.view.View,且是public的類。
    public class * extends android.view.View
    
    // 匹配所有實現了android.view.View$OnClickListener介面,且是public的類。
    public class * implements android.view.View$OnClickListener

    以下是模板中成員變數的表示

  6. @annotationtype
    用來修飾成員變數,表示該成員變數附加的annotation。例如。

    public class * {
        @Deprecated <fields>;       // 可以匹配一個類中所有被@Deprecated修飾的成員變數
    }
    
  7. public|private|protected|static|final|volatile|transient
    用來修飾成員變數,它們都是成員變數的修飾符,前面可以加上!表示不匹配該修飾符。例如,

    class * {
        public int mCount;       // 匹配一個類中被public 修飾的int型別名字為mCount的變數
    }
    
  8. <fields>
    <fields>表示任意型別任意名字的所有成員變數。例如

    class * {
        public static <fields>;       // 匹配一個類中被public static修飾的所有成員變數
    }
    
  9. fieldtype
    fieldtype表示成員變數的資料型別,它需要和fieldname組合一起使用。fieldtype中可以包含如下的萬用字元。

    % : 用來匹配任意Java基本資料型別(包括byte,short,int,long,float,double,char和boolean,不包括void)
    ? : 用來匹配成員變數型別中除包分隔符.之外的任意單個字元
    * : 用來匹配成員變數型別中除包分隔符.之外的任意多個字元(包括0個)
    ** : 用來匹配成員變數型別中任意多個字元(包括0個),包括包分隔符.
    ***: 用來匹配任意型別,包括基本資料型別和陣列

    需要注意的是,?,*和**不會匹配Java的基本資料型別,如果要匹配基本資料型別,需要使用%或***。
    以下是一些示例

    模板 可以匹配示例 不能匹配示例
    % byte,short,int,long,float,double,char,boolean java.lang.String,void
    my.test? my.test1; my.testx my.test ; my.test12 ; my.test.1
    my.in? my.ins, my.ing my.int
    my.*test* my.test ; my.thistest ; my.testapp ; my.thistestapp my.sub.test ; my.test.app
    my.* my.abc ; my.abc123 my.sub.abc
    * abc ; myabc my.abc, int, boolean
    **.test my.test ; your.sub.test -
    my.** my.app ; my.sub.app -
    ** my.app ; your.sub.test int, boolean, my.app []
    *** my.app ; your.sub.test, int, boolean, java.lang.String[] -
  10. fieldname
    fieldname表示成員變數的名字。fieldname中可以包含如下的萬用字元。

    ? : 用來匹配成員變數名中任意單個字元
    * : 用來匹配成員變數名中任意多個字元(包括0個)

    以下是模板中成員方法的表示

  11. @annotationtype
    用來修飾成員方法,表示該成員方法附加的annotation。例如。

    public class * {
        @Deprecated <methods>;       // 匹配一個類中所有被@Deprecated修飾的成員方法
    }
    
  12. public|private|protected|static|final|synchronized|native|abstract|strictfp
    用來修飾成員方法,它們都是成員方法的修飾符,前面可以加上!表示不匹配該修飾符。部分修飾符可以組合使用。例如,

    class * {
        public static final int get*();     // 匹配一個類中被public static final修飾的,返回值為int型別,名字前面為get,且沒有引數的成員方法
    }
    
  13. <methods>
    <methods>表示返回值為任意型別,任意名字,任意引數的所有成員方法。例如

    class * {
        public static <methods>;       // 匹配一個類中被public static修飾的所有成員方法
    }
    
  14. <init>
    <init>(argumenttype,…)表示構造方法。和<methods>不同的是,這裡必須包含引數描述,也就是和argumenttype一起使用,例如

    class * {
        public <init>();       // 匹配一個類中public且沒有引數的構造方法
    }
    class * {
        public <init>(int, java.lang.string);      // 匹配一個類中public且包含兩個引數,第一個為int,第二個為String的構造方法
    }
  15. classname
    classname同樣表示構造方法,這裡的classname必須和第4項的classname完全相同。和<init>是等價的。例如

    public class abc {
        public abc (int);       // 匹配abc類中引數為int的構造方法
    }
  16. returntype
    returntype表示成員方法返回值的型別。它需要和methodname組合一起使用。returntype中萬用字元的用法和fieldtype完全相同。

  17. methodname
    methodname表示方法的名字。它需要和argumenttype一起使用。methodname中萬用字元的用法和fieldname完全相同。

  18. argumenttype
    argumenttype表示成員方法的引數列表,多個引數之間用逗號分割。argumenttype中萬用字元的用法和fieldtype基本相同,fieldtype可以使用的萬用字元在argumenttype中都可以使用,且有相同的含義,不同的是argumenttype中有一個…的萬用字元,它的含義是表示任意多個任意型別的引數。例如

    public class abc {
        public <init>(*);     // 匹配abc類中有一個引數,引數型別為除基本資料型別之外,且沒有子包的任意名字的構造方法
    }
    public class abc {
        public <init>(**);    // 匹配abc類中有一個引數,引數型別為除基本資料型別之外任意型別的構造方法
    }
    public class abc {
        public <init>(***);   // 匹配abc類中有一個引數,引數型別為任意型別的構造方法
    }
    public class abc {
        public <init>(...);   // 匹配abc類任意的構造方法
    }
  19. *
    單獨的*表示任意的成員變數和成員方法。例如

    public class abc {
        public *;     // 匹配abc類任意的public成員變數和成員方法
    }

注意:如果一個模板中沒有包含某個欄位,並不表示它只能匹配那些不包含該修飾字段的類和成員,而是表示不關心該欄位,該欄位可以不存在,也可以是任意的。例如,模板public class ; 中沒有包含@annotationtype欄位,它表示不關心類是否使用了註解,因此它既可以匹配那些沒有使用註解的類,也可以匹配使用了任意註解的類;再比如模板class ;中沒有包含public|final|abstract欄位,這個模板可以匹配使用了任意註解,任意訪問修飾符的類,當然也包括那些沒有註解,沒有訪問修飾符的類。

keep修飾符(modifier)

指定為keep的類和類的成員預設既不會被shrink,也不會被混淆,也不會被優化。可以指定修飾符modifier來改變這一預設的過程。keep修飾符有四個,分別描述如下。

  • allowshrinking
    加上此修飾符後,可以讓 -keep選項指定的類和類的成員可以被 shrink。也就是說,此entry points只對混淆和優化階段有用(不會被混淆和優化),在shrink階段不被考慮(可以被shrink)。注意,由於shrink是最先執行的,如果 -keep選項指定的類和類的成員被shrink了,那麼顯然它也無法再被混淆和優化了。

  • allowoptimization
    加上此修飾符後,可以讓 -keep選項指定的類和類的成員可以被 優化。也就是說,此entry points只對shrink和混淆階段有用(不會被shrink和混淆),在優化階段不被考慮(可以被優化)。

  • allowobfuscation
    加上此修飾符後,可以讓 -keep選項指定的類和類的成員可以被 混淆。也就是說,此entry points只對shrink和優化階段有用(不會被shrink和優化),在混淆階段不被考慮(可以被混淆)。

  • includedescriptorclasses
    加上此修飾符後,可以讓 -keep選項指定的類的成員變數的型別,成員方法的引數型別,成員方法的返回值型別都加入到entry points中。此選項主要用來保護native方法。

keep選項

在Proguard中通過一系列的keep選項用來指定Entry points。

  • -keep [,modifier,…] class_specification
    指定需要保留的類和類的成員(包括成員變數和成員方法),modifier和class_specification的含義見上文。
  • -keepclassmembers [,modifier,…] class_specification
    指定需要保留的類的成員(包括成員變數和成員方法),modifier和class_specification的含義見上文。類本身是可以被shrink,混淆和優化處理的。
  • -keepclasseswithmembers [,modifier,…] class_specification
    指定需要保留的類和類的成員(包括成員變數和成員方法),modifier和class_specification的含義見上文。和-keep不同的是,需要指定的類的成員都存在才會被保留。
  • -keepnames class_specification
    指定需要不被混淆和優化的類和類的成員(包括成員變數和成員方法),指定的類和類的成員可以被shrink。等同於-keep,allowshrinking class_specification。
    -keepclassmembernames class_specification
    指定需要不被混淆和優化的類的成員(包括成員變數和成員方法),指定的類的成員可以被shrink。等同於-keepclassmembers,allowshrinking class_specification
    -keepclasseswithmembernames class_specification
    指定需要不被混淆和優化的類的成員(包括成員變數和成員方法),需要指定的類的成員都存在才會被處理。指定的類和類的成員可以被shrink。等同於-keepclasseswithmembers,allowshrinking class_specification
    -printseeds [filename]
    輸出jar檔案中和各類-keep選項匹配的類和成員,如果不指定filename,則輸出到標準輸出,指定filename後則輸出到指定的檔案。

—————未完待續—————-