深入學習ProGuard之:ProGuard簡介與android的應用

LemonYang發表於2016-12-17

什麼是ProGuard

ProGuard的官網中,關於ProGuard的描述是這樣的:

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

ProGuard是一個開源的Java類檔案(只能處理Java程式碼,但是對應資原始檔等是不能起作用的)的壓縮器、優化器、混淆器和預校驗器。其處理的過程主要分為以下幾個步驟:

深入學習ProGuard之:ProGuard簡介與android的應用

ProGuard可以幹什麼

  • shrinker(壓縮):移除無效的類、屬性、方法等
  • optimizer(優化):優化位元組碼,並刪除未使用的結構、方法介面等。(從java原始檔來說,我們實現一個介面的時候是必須要實現介面的全部方法,但是從位元組碼檔案來說,是可以將空的實現移除,從而達到優化的目的的)
  • obfuscator(混淆):將類名、屬性名、方法名混淆為難以讀懂的字母,比如a,b,c等(當然,我們可以通過相關的屬性設定指定混淆使用的字典,增加閱讀性的難度)
  • preverifier(預校驗):對經過之前的步驟處理之後的class檔案進行預檢驗,確保虛擬機器載入的class檔案是可以執行的。

ProGuard不可以幹什麼

其實從ProGuard的描述我們就可以知道,ProGuard是java的位元組碼檔案(也即.class檔案)進行處理的,因此對於我們android的一些其他檔案是無能為力的,比如android中的資原始檔、XML檔案等等,是不能被優化和混淆處理的,如果我們希望能夠完成這些東西的工作,可以參考:

入口點(Entry points)

關於入口點這個概念可能提及得不多,但是其實非常容易理解。因為ProGuard會對我們的java檔案進行混淆、優化等處理,但是我們需要確保一些標誌性的類、方法等內容不會被ProGuard進行處理,比如說我們的main方法、開放給別人呼叫的activity等。這些我們定義好的不應該被ProGuard進行混淆、優化處理的內容就是入口點。(因此我們在ProGuard配置檔案通過keep配置的其實就是各種入口點)

  • 在壓縮階段:ProGuard會從入口點開始,遞迴地搜尋類以及屬性、方法等的使用和引用鏈關係。對於那些沒有被使用到的類以及類成員都會被移除,從而達到壓縮的目的。(這感覺跟java虛擬機器查詢物件的引用鏈有點相像)
  • 在優化階段:非入口點的類、方法都會被設定為private、static或final,不使用的引數會被移除,此外,有些方法會被標記為內聯的.關於ProGuard可以實現的更多的優化工作可以參考官方文件的FAQ部分
  • 在混淆階段:非入口點的類、屬性等就會被重新命名,達到混淆的目的
  • 預校驗階段是唯一一個與入口點概念無關的步驟

android配置ProGuard

android的構建系統已經整合了ProGuard,因此我們並不需要手動的去執行ProGuard。只需要在我們專案的build.gradle檔案中進行以下的配置就可以了:

android {
    buildTypes {
        release {
            minifyEnabled true
            proguardFile getDefaultProguardFile('proguard-android.txt')
        }
    }

    productFlavors {
        flavor1 {
        }
        flavor2 {
            // 我們可以針對不同的buildTypes、productFlavors指定的不同的配置檔案
            proguardFiles 'some-other-rules.txt'
            // 使用proguardFiles我們可以同時指定多個ProGuard配置檔案
            proguardFiles getDefaultProguardFile('proguard-android.txt'),'proguard-rules.pro'
        }
    }
}複製程式碼

ProGuard萬用字元使用

在掌握ProGuard的具體配置之前,我們先看一下ProGuard當中會用到的各種萬用字元,這對於我們使用ProGuard的配置項自己定義入口點的時候會有極大的幫助

通用萬用字元

萬用字元 意義
? 匹配名稱當中的任意一個字元
* 匹配名稱中的任意部分,但是不包括目錄的分隔符、包分隔符
** 匹配名稱中的任意部分,可以包含任意數量的目錄分隔符、包分隔符

比如說,"java/.class,javax/.class" 可以匹配在java、javax目錄下面的所有檔案

值得注意的是,我們可以在我們的匹配字元前面使用“!”表示不包含符合條件的內容,比如說:"!.gif,images/",可以匹配在images目錄下除了gif檔案以外的所有檔案。

類描述萬用字元

萬用字元 意義
? 匹配單個字元
* 匹配類名中的任何部分,但不包含包分隔符
** 匹配類名中的任何部分,並且可以包含包分隔符
% 匹配java中的基本資料型別(int, boolean, long, float,double等)
... 匹配任意引數列表
* 匹配所有型別,包括初始型別和非初始型別,陣列和非陣列
< init > 匹配任何構造器
< ifield> 匹配任何欄位名
< imethod> 匹配任何方法
$ 指內部類

需要注意的是:?, , * 不能夠匹配任何java中的基本型別和陣列(在匹配型別的時候)

類描述模版

一個完整的類描述可以使用下面的模版:

[@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 ... ] *;
    ...
}]複製程式碼

關於上面的模版的意義總結如下:

  • [] 表示可選可不選
  • ... 表示可以有更多的配置項
  • | 表示多選一
  • ()表示一個整體,不能分割
  • class關鍵字可以匹配class類或interface類,但是interface關鍵字只能匹配interface類,enum關鍵字只能匹配enum類。在interface或enum關鍵字前加一個!,可以表示非這種型別的類;
  • classname 必須寫全名,比如java.lang.String。內部類用 \$ 間隔。例java.lang.Thread$State。
  • extend與implements 關鍵字是用來限制類的範圍的。他們目前是等價的,用來匹配某些類的子類。需要注意的是,這個指定的類並不包括在匹配結果中,如果想要該類也被匹配到,就需要額外宣告一項配置。
  • @ 符號匹配那些註解標誌的類或類成員,它的萬用字元形式與classname的形式一樣。
  • 建構函式可以使用簡單類名或全類名來指定,就像java中的建構函式一樣有引數列表但是沒有返回型別
  • 成員變數和成員方法的匹配形式與java非常像,只是方法的引數不帶引數名。
  • 類或者類成員的修飾符也是匹配類的限制條件。通過修飾符限制,可以縮小匹配的範圍。修飾符組合也是可以的,就像java中的public static一樣,但是不能衝突, 比如public private

ProGuard 配置檔案

由於android的gradle構建本身支援了ProGuard的配置,因此一些諸如輸入、輸出的設定,這裡不再提及。如果你希望瞭解這部分的內容,可以檢視ProGuard的幫助文件,上面關於配置使用有非常詳細的講述,這裡只是針對我們在android專案中編輯ProGuard配置檔案,使得專案能夠使用ProGuard完成混淆、壓縮、優化等功能。

很多人聽說ProGuard的時候,可能都覺得它只是用來在程式碼混淆的,實際上,從文章開頭的圖片我們就可以知道,ProGuard的處理其實一共有壓縮、優化、混淆和預校驗四個步驟,而當中的每個步驟,ProGuard都提供了配置項,讓我們能夠自主的決定每個步驟的行為。所以下面主要從這結構步驟的配置分別描述。

keep配置

  • -keep [,modifier,...] class_specification

指定類和類的成員變數是入口節點,保護它們不被移除混淆。例如:

# 這裡我們需要keep住應用的入口點
-keep public class mypackage.MyMain { 
      public static void main(java.lang.String[]); 
}
# 這裡使用了萬用字元,表示要keep住所有的public元素
-keep public class * { 
      public protected *; 
}複製程式碼
  • -keepclassmembers [,modifier,...] class_specification

保護的指定的成員變數不被移除、優化、混淆.例如

# 這裡keep住所有實現了Serializable介面的類的名字不被混淆
-keepnames class * implements java.io.Serializable
# 這裡keep住所有序列化類的成員
-keepclassmembers class * implements java.io.Serializable { 
    static final long serialVersionUID; 
    private static final java.io.ObjectStreamField[] serialPersistentFields; 
    !static !transient <fields>; 
    private void writeObject(java.io.ObjectOutputStream); 
    private void readObject(java.io.ObjectInputStream); 
    java.lang.Object writeReplace(); 
    java.lang.Object readResolve(); 
}複製程式碼
  • -keepclasseswithmembers [,modifier,...] class_specification

擁有指定成員的類將被保護,根據類成員確定一些將要被保護的類.例如

# 這裡keep住了所有帶有main方法的類
-keepclasseswithmembers public class * { 
    public static void main(java.lang.String[]); 
}複製程式碼
  • -keepnames class_specification

指定一些類名受到保護,前提是他們在壓縮這一階段沒有被去掉。也就是說沒有被入口節點直接或間接引用的類還是會被刪除。僅在混淆階段有效。例子如上面出現的例子所示。

  • -keepclassmembernames class_specification

保護指定的類成員名稱,前提是這些成員在壓縮階段沒有被刪除。

  • -keepclasseswithmembernames class_specification

擁有指定成員的類名稱將被保護,根據類成員確定一些將要被保護的類名稱,前提是這些類在壓縮階段沒有被去掉,僅在混淆階段有效。

  • -printseeds [filename]

指定通過-keep配置匹配的類或者類成員的詳細列表。列表可以列印到標準輸出流或者檔案裡面。這個列表可以看到我們想要保護的類或者成員有沒有被真正的保護到,尤其是那些使用萬用字元匹配的類。

在上面的配置項中,我們會發現keep和keepnames是成對出現的。如果我們使用的是keep,那麼符合匹配條件的元素不會被移除和混淆,但是如果只是使用了keepnames的話,則不能確保相應的元素不會在壓縮階段被移除(對於那些與入口點之間沒有直接或者間接聯絡的元素在壓縮階段會被移除)。但是如果符合條件的元素在壓縮階段存活了下來,那麼則能保證在混淆階段不會被混淆。

程式碼壓縮配置

  • -dontshrink

設定不壓縮輸入檔案。
預設情況下,除了-keep相關配置指定的類,所有其它沒有被引用到的類都會被刪除。每次optimizate操作之後,也會執行一次壓縮操作,因為每次optimizate操作可能刪除一部分不再需要的類.

  • -printusage [filename]

設定列印出那些被刪除的元素。
這個列表可能列印到標準輸出流或者一個檔案中(如果我們指定了filename的值)。

  • -whyareyoukeeping class_specification

設定列印出為什麼一個類或類的成員變數被保護。

優化配置

  • -dontoptimize

設定不優化輸入檔案。
預設情況下,優化選項是開啟的,並且所有的優化都是在位元組碼層進行的

  • -optimizations optimization_filter

更加細粒度地宣告優化開啟或者關閉。
關於optimization_filter的具體設定,可以參考官方文件,這個設定難度比較大。

  • -optimizationpasses n

設定執行優化的次數,預設情況下,只執行一次優化。執行多次優化可以提高優化的效果,但是,如果執行過一次優化之後沒有效果,就會停止優化,剩下的設定次數不再執行

  • -assumenosideeffects class_specification

設定“被匹配的方法被刪除也沒有影響”,在優化階段,如果確定這些方法的返回值沒有使用,那麼就會刪除這些方法的呼叫。proguard會自動的分析你的程式碼,但不會分析處理類庫中的程式碼。例如,可以指定System.currentTimeMillis(),這樣在optimize階段就會刪除所有的它的呼叫。還可以用它來刪除列印Log的呼叫。

-assumenosideeffects class android.util.Log { 
    public static boolean isLoggable(java.lang.String, int); 
    public static int v(...); 
    public static int i(...); 
    public static int w(...); 
    public static int d(...); 
    public static int e(...); 
}複製程式碼
  • -allowaccessmodification

設定是否允許改變作用域的

  • -mergeinterfacesaggressively

指定一些介面可能被合併,即使一些子類沒有同時實現兩個介面的方法。這種情況在java原始碼中是不允許存在的,但是在java位元組碼中是允許存在的。它的作用是通過合併介面減少類的數量,從而達到減少輸出檔案體積的效果。

上面介紹了關於優化階段proguard的一些配置屬性,關於proguard可以處理的優化工作其實有很多項,在上面的描述中我們有提供了官方的文件描述連結,感興趣的可以去看一下,這裡大概羅列一下:

  • 直接計算出靜態常量表示式的值
  • 移除沒有用到的屬性和方法呼叫
  • 移除不會被執行的分支
  • 移除不必要的比較和測試的例項
  • 移除不必要的程式碼塊
  • 合併相同的程式碼塊
  • 減少變數的分配
  • 移除只有寫屬性(不能被讀取到)的屬性值和沒有用的方法引數
  • 內聯靜態常量值、方法引數和返回值
  • 內聯那些短而且只被呼叫一次的方法

還有很多這裡不再羅列,文件說明地址在這裡:What kind of optimizations does ProGuard support?

混淆配置

  • -dontobfuscate

設定不混淆。
預設情況下,混淆是開啟的。除了keep配置中宣告的類,其它的類或者類的成員混淆後會改成簡短隨機的名字。

  • -printmapping [filename]

設定輸出新舊元素名的對照表的檔案。對映表會被輸出到標準輸出流或者是一個指定的檔案。

  • -applymapping filename

指定重用一個已經寫好了的map檔案作為新舊元素名的對映。元素名已經存在在mapping檔案中的元素,按照對映表重新命名;沒有存在到mapping檔案的元素,重新賦一個新的名字。

  • -obfuscationdictionary filename

指定一個字典檔案用來生成混淆後的名字。預設情況下,混淆後的名字一般為a,b,c這種。通過使用-obfuscationdictionary配置的字典檔案,可以使用一些非英文字元做為類名。

  • -keeppackagenames [package_filter]

設定不混淆指定的包名。
後面配置的過濾器是逗號隔開的一組包名。包名可以包含?,,*萬用字元,並且可以在前面加!否定符。

  • -keepattributes [attribute_filter]

設定受保護的屬性,可以有一個或者多個-keepattributes配置項,每個配置項後面跟隨的是Java虛擬機器和proguard支援的attribute,兩個屬性之間用逗號分隔。
屬性名中可以包含,*,?等萬用字元。也可以加!做前導符,將某個屬性排除在外。當混淆一個類庫的時候,至少要保持InnerClasses, Exceptions,Signature屬性。為了跟蹤異常資訊,需要保留SourceFile, LineNumberTable兩個屬性。如果程式碼中有用到註解,需要把Annotion的屬性保留下來。
可以設定的屬性名稱可以參考官方文件Attributes

  • -keepparameternames

指定被保護的方法的引數型別和引數名不被混淆

在上面的配置項中,我忽略掉了一些不常用的配置項,如果你希望有更深入的理解,歡迎檢視官方文件 Obfuscation options

預校驗配置

  • -dontpreverify

設定不預校驗即將執行的類。

通用配置

  • -verbose

設定在proguard處理過程中輸出更多資訊

  • -dump [filename]

設定輸出整個處理之後的jar檔案的類結構,可以輸出到標準輸出流或者一個檔案

關於proguard的常用配置大概如上所示,其中我忽略了一些不常用的屬性,建議大家遇到問題的時候可以自己到官方文件進行檢視,上面會有非常詳細的說明以及示例。

在上面的配置中,有好幾個地方是我們可以自己定義將某些處理資訊輸出到文字檔案上面的,這裡簡單總結一下:

  • -dump [filename] :我們可以在設定的文字檔案中找到apk檔案中所有類檔案間的內部結構
  • -printmapping [filename] :列出了原始的類,方法,和欄位名與混淆後程式碼之間的對映
  • -printseeds [filename] : 列出了未被混淆的類和成員
  • -printusage [filename]:列出了從apk中刪除的程式碼

參考文件

faq
ProGuard manual
shrink Your Code and Resources
Android Proguard

關於ProGuard的入門和配置使用簡單介紹到這裡,只要掌握了萬用字元的使用再結合官方文件中的例子,相信大家都能夠掌握ProGuard的使用。在後面可能有空的話,自己還會分析一下ProGuard在gradle構建系統中的執行過程,與android構建會結合起來。

如果你喜歡這篇文章的話,歡迎點贊,也歡迎大家關注我,最後,感謝你寶貴的時間閱讀這篇文章,謝謝。

相關文章