ProGuard 混淆解析

最近被keep keepnames keepclassmembers等幾個混淆規(guī)則搞得暈頭轉(zhuǎn)向,看起來雖然簡單,但用起來卻經(jīng)常跟自己預(yù)想的一樣。所以決定放棄看他人總結(jié)的博客,直接看ProGuard官方文檔,目前為止,總算是有一定了解了。

1 ProGuard簡介

通常我們都認為ProGuard是一個代碼混淆工具,實際上其作用還不至于此,而是包括了四部分內(nèi)容:

  • shrink(刪減):刪減無用代碼,包括無用的類、無用的變量、方法等
  • optimize(優(yōu)化):優(yōu)化方法字節(jié)碼
  • obfuscate(混淆):混淆現(xiàn)有代碼
  • preverify(預(yù)校驗):給類添加預(yù)校驗信息,這是J2ME和Java 6及以上要求的

Proguard的整個工作過程如下圖所示:

這里寫圖片描述

了解了Proguard的四大步驟,我們才能更好地理解Proguard混淆規(guī)則。

1.1 Entry Points

entry points就是程序入口點。ProGuard以entry points作為代碼掃描入口,遍歷所有代碼,并最終決定哪些代碼需要被丟棄或混淆。比較典型的 entry points包括:

  • main方法
  • applets
  • midlets
  • activities

在ProGuard的不同階段中,entry points起到不同的作用:

  • shrinking: 在shrinking階段以entry points作為起點遞歸遍歷所有代碼,不可達的代碼會被丟棄
  • optimization:在optimization階段會對代碼進行進一步優(yōu)化:
    • 非入口代碼可能會被改為private、static、final
    • 未被使用參數(shù)會被刪除
    • 部分方法可能會被優(yōu)化為內(nèi)聯(lián)方法
  • obfuscation:obsfucation階段會將非入口代碼進行混淆。被標識為入口的代碼則會免于被混淆

2 Keep 選項

keep選項是為了在代碼混淆的過程中保留部分類及其字段不被混淆以滿足程序運行需求。keep選項一共有如下6種規(guī)則:

  • keep
  • keepnames
  • keepclassmember
  • keepclassmembernames
  • keepclasseswithmembers
  • keepclasseswithmembernames

2.1 keep

keep規(guī)則用于標識程序入口,被keep規(guī)則修飾的類及其成員會被指定為程序入口,從而免于被混淆。

2.2 keepnames

keepnames修飾的類及其成員不會被混淆,但前提是對應(yīng)的成員在shrinking類沒有被刪減掉。比如保留所有實現(xiàn)Serializable接口的類名:

-keepnames class * implements java.io.Serializable 

2.3 keepclassmembers

keepclassmembers僅保留指定的類成員不被混淆,但類名會被混淆。接著上面的例子,如果我們不僅向保留所有實現(xiàn)Seriablizable接口的類名,同時還要保留其所有的接口方法:

-keepnames class * implements java.io.Serializable 
-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(); 
} 

2.4 keepclassmembernames

keepclassmembernames保留指定類成員不被混淆,前提是相關(guān)的類成員沒有在shrinking階段被刪減。

2.5 keepclasseswithmembers

keepclasseswithmembers會保留類和類成員不被混淆,前提是對應(yīng)的類包含所有指定的類成員。keepclasseswithmembers適用于指定一批擁有功能類成員的方法,而不用一一列舉。比如保留所有又main方法的類:

-keepclasseswithmembers public class * { 
    public static void main(java.lang.String[]); 
} 

2.6 keepclasswithmembernames

keepclasseswithmembernames保留類和類成員不被混淆,前提是對應(yīng)的類包含所有指定的類成員,同時對應(yīng)的類成員在shrinking階段沒有被刪減。比如保留所有native方法:

-keepclasseswithmembernames class * { 
    native <methods>; 
} 

2.7 關(guān)系梳理

看完上述幾個規(guī)則一定有點暈,沒有關(guān)系,記住下面這個表就是:

Keep From being removed or renamed From being renamed
Classes and class members -keep -keepnames
Class members only -keepclassmembers -keepclassmembernames
Classes and class members, if class members present -keepclasseswithmembers keepclasseswithmembernames

每一條keep規(guī)則都應(yīng)該跟一個類說明(specification of classes and class members)。如下就是一個類說明的例子:

class * { 
    native <methods>; 
} 

類說明的規(guī)則將在下一節(jié)詳細介紹。

如果你不清楚到底該用哪個keep規(guī)則,建議直接使用keep,被keep標明的類及其類成員不會被刪減或重命名。需要注意的是,如果僅僅指明要keep的類,而不指明其類成員:

keep class yourpackage.demo

那ProGuard僅會保留其類和無參數(shù)構(gòu)造方法不被刪減或重命名。

3 類說明(Class Specification)

類說明(class specification)是一個用于描述要keep的類及其成員的描述模板,其完整的格式如下所示:

[@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 ... ] *;
    ...
}]

看起來好像很復(fù)雜,這是因為其功能強大,提供的選項很多,實際我們在實際使用過程中都是使用的簡化模式。

不過這里我們還是來看看完整的格式,類說明模板有很多符號,理解這些符號的作用很有必要:

  • []表示可選項
  • 表示非
  • | 表示或,如public|private表示要修飾的對象是publicprivate

理解了基本的符號含義,再來看這個模板就簡單些了,整個表達式分為兩部分:

  • 類描述
  • 類成員描述

3.1 類描述

類描述用于限定類本身,其對應(yīng)的是上面完整表達式的:

[@annotationtype] [[!]public|final|abstract|@ ...] [!]interface|class|enum classname
    [extends|implements [@annotationtype] classname]

部分。

  • [@annotationtype]用于描述類注解(可選)
  • [[!]public|final|abstract|@ ...] 用于描述類的訪問權(quán)限
  • [!]interface|class|enum用于描述要類的類型:
    • class:可以表示任何類或接口
    • interface:僅表示接口
    • enum:枚舉
  • classname 類名
  • [extends|implements [@annotationtype] classname]用于描述繼承、實現(xiàn)關(guān)系,通常用于描述一組類,如上文中提到過的所有實現(xiàn)Serializable接口的類:`class * implements java.io.Serializable

這個類描述中所涉及到的兩個classname都支持通配符,用以指定一組類,其通配符使用說明如下:

通配符 描述
匹配所有的單個字符。比如mypackage.test?可以指代mypackage.test1mypackage.test2,但不能指代mypackage.test12
* 匹配任意長度的類名,但不包括分隔符.。比如mypackage.*Test*可以描述mypackage.Testmypackage.YourTestApplication。但無法描述mypackage.mysubpackage.MyTest。通常的用法是,用mypackage.*來描述mypacakge包下的所有類,但不包括其子包中的類
** 匹配任意長度的類名,包括分隔符.mypackage.**用于描述mypackage包下的所有類,也包括其子包中的類。

3.2 類成員描述

類成員描述對應(yīng)于上文中的:

[{
    [@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 ... ] *;
    ...
}]

部分。

首先,整個類成員描述部分都是可選的,這部分不寫也是可以的,比如keep class mypackage.MyTest,這種情況下keep或保留類名和類的無參數(shù)構(gòu)造方法不被移除或混淆。

類成員描述的形式大致有三種,也就是類成員描述模板中用;分割開來的三個表達式,接下來分別講下。

3.2.1 類成員變量描述
[@annotationtype] [[!]public|private|protected|static|volatile|transient ...] <fields> |
                                                                      (fieldtype fieldname);

用于描述類的成員變量,[@annotationtype][[!]public|private|protected|static|volatile|transient ...]用于限定變量的注解類型和訪問權(quán)限,均為可選。而變量名的描述也有兩種方式:

  • <fields>:指代所有的變量
  • (fieldtype fieldname):指定具體的某個變量。注意,fieldtype和fieldname必須成對出現(xiàn)
3.2.2 類成員方法描述
    [@annotationtype] [[!]public|private|protected|static|synchronized|native|abstract|strictfp ...] <methods> |
                                                                                           <init>(argumenttype,...) |
                                                                                           classname(argumenttype,...) |
                                                                                           (returntype methodname(argumenttype,...));

前半部分和類成員變量的描述一致,[@annotationtype][[!]public|private|protected|static|...]用于限定方法的注解類型和訪問權(quán)限。而方法名的描述有四種方式:

  • <methods>:指代所有方法
  • <init>(argumenttype,...):指代構(gòu)造方法。<init>指代所有的構(gòu)造方法,(argumenttype,...)描述方法的參數(shù)列表
  • classname(argumenttype,...):另一種指代構(gòu)造方法的方式,因為只有構(gòu)造方法才沒有返回類型
  • (returntype methodname(argumenttype,...)):指代特定成員方法
3.2.3 類成員描述通配符

類成員的描述也支持通配符:

通配符 描述
% 描述任何的原型類型,如:boolean、int、但不包括void
描述任意的單個字符
* 匹配任意長度的類名,但不包括分隔符.
** 匹配任意長度的類名,包括分隔符.
*** 匹配所有的數(shù)據(jù)類型,(原型類型或非原型類型,數(shù)組或非數(shù)組)
... 匹配任意長度的任意類型參數(shù)列表
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容