入口
為了決定哪些代碼要被保留哪些代碼要出丟棄和混淆,必須指定入口點。這些入口點通常是 main方法,activity,service等。
-
在壓縮階段,Proguard從這些入口點開始遞歸確定哪些類或類成員要被使用,其余的都會被丟棄。
-
在優化階段,ProGuard 會進一步優化代碼。在其他優化中,可以將不是入口點的類和方法設為 private,static 或 final ,刪除未使用的參數,並且可以內聯一些方法。
-
在混淆階段,ProGuard 會重新命名不屬於入口點的類和類成員。在整個過程中,保證入口點仍然可以通過其原始名稱訪問。
查看 Proguard 輸出結果
為了避免引入 bug 我們有必要對 結果進行檢查。
在Android中,開啟了混淆構建會在
-
dump.txt 描述APK文件中所有類的內部結構
-
mapping.txt 提供混淆前后類、方法、類成員等的對照表
-
seeds.txt 列出沒有被混淆的類和成員
-
usage.txt 列出被移除的代碼
我們可以根據 seeds.txt 文件檢查未被混淆的類和成員中是否已包含所有期望保留的,再根據 usage.txt 文件查看是否有被誤移除的代碼。
過濾器
ProGuard 為許多配置提供了不同方面的過濾選項:文件名稱,目錄,類別,軟件包,屬性,優化等。
過濾器是可以包含通配符的,以逗號分隔的,名稱列表。
只有與列表中的項目匹配的名稱才會通過過濾器。
每種配置的通配符可能有所不同,但以下通配符是通用的:
-
? 匹配名稱中的任何單個字符。
-
* 匹配不包含包分隔符或目錄分隔符的名稱的任何部分
-
** 匹配名稱的任何部分,可能包含任意數量的包分隔符或目錄分隔符。
此外,名稱前可以加上否定感嘆號 !
排除名稱與進一步嘗試匹配后續名稱。
因此,如果名稱與過濾器中的某個項目相匹配,則會立即接受或拒絕該項目,具體取決於項目是否具有否定符。
如果名稱與項目不匹配,則會針對下一個項目進行測試,依此類推。
它如果與任何項目不匹配,則根據最后一項是否具有否定符而被接受或拒絕。
如,"!foobar,*.bar" 匹配除了foobar之外的所有以bar結尾的名稱。
下面以過濾文件具體舉例。
文件過濾器
像通用過濾器一樣,文件過濾器是逗號分隔的文件名列表,可以包含通配符。只有具有匹配文件名的文件被讀取(在輸入的情況下),或者被寫入(在輸出的情況下)。支持以下通配符:
-
? 匹配文件名字中的任何單個字符
-
* 匹配不包含目錄分隔符的文件名的任何部分。
-
** 匹配文件名的任何部分,可以包含任意數目的目錄分隔符。
例如 "java/**.class ,javax/**.class" 可以匹配 java和javax目錄下所有的 class 文件。
此外,文件名前面可能帶有感嘆號'!'將文件名排除在與后續文件名匹配上。
例如 "!**.gif,images/**" 匹配images目錄下所有除了 gif 的文件
關於更詳細的用法 可以查看官方文檔 filtering
keep 選項
-keep [,modifier,...] class specification
指定類和類成員(字段,方法)作為入口點被保留。
例如,為了保留一個程序,你要指定Main方法和類。為了保留一個庫,你應該指定所有被公開訪問的元素。
- 保留 main 類和 main 方法
-keep public class com.example.MyMain {
public static void main(java.lang.String[]);
}
- 保留所有被公開訪問的元素
-keep public class * {
public protected *;
}
Note:如果你只保留了類,沒有保留類成員,那么你的類成員將不會被保留
例如 有一個實體類
public class Product implements Serializable {
public static final int A = 1;
public static final int B = 2;
private String name;
private String url;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
}
規則配置如下
# 保留 Product類
-keep class cn.sintoon.camera.Product
usage.txt文件中有以下內容 ,可以看到 類中的成員全部被移除了
cn.sintoon.camera.Product:
public static final int A
public static final int B
private java.lang.String name
private java.lang.String url
16:16:public java.lang.String getName()
20:21:public void setName(java.lang.String)
24:24:public java.lang.String getUrl()
28:29:public void setUrl(java.lang.String)
-keepclassmembers [,modifier,...] class specification
指定要保留的類成員,前提是它們的類也被保留了。
例如,你想保留實現了 Serializable 接口的類中的所有 serializable 方法和字段。
-keepclassmembers class * implements java.io.Serializable {
private static final java.io.ObjectStreamField[] serialPersistentFields;
private void writeObject(java.io.ObjectOutputStream);
private void readObject(java.io.ObjectInputStream);
java.lang.Object writeReplace();
java.lang.Object readResolve();
}
Note: 注意字段類型帶上包名; String 類型為 java.lang.String;另外,如果只保留了類成員沒有保留類跟沒有保留一樣
還是拿上面那個例子,改一下規則
-keepclassmembers class * implements java.io.Serializable{
private String name;
public String getName();
public static final int A;
}
再看 usage.txt 類都被移除了,保留字段沒毛線用。
cn.sintoon.camera.Product
-keepclasseswithmembers [,modifier,...] class specification
指定要保留的類和類成員,條件是所有指定的類成員都在。
例如,你要保留程序中所有的主程序,不用顯示的列出。
-keepclasseswithmembers public class * {
public static void main(java.lang.String[]);
}
還是用上面那個例子,保留住類和所有的類成員
-keepclasseswithmembers class cn.sintoon.camera.Product{
public static final int A;
public static final int B;
private java.lang.String name;
private java.lang.String url;
public java.lang.String getName();
public void setName(java.lang.String);
public java.lang.String getUrl();
public void setUrl(java.lang.String);
}
看 seeds.text 中就會出現這個類和類成員
cn.sintoon.camera.Product
cn.sintoon.camera.Product: int A
cn.sintoon.camera.Product: int B
cn.sintoon.camera.Product: java.lang.String name
cn.sintoon.camera.Product: java.lang.String url
cn.sintoon.camera.Product: java.lang.String getName()
cn.sintoon.camera.Product: void setName(java.lang.String)
cn.sintoon.camera.Product: java.lang.String getUrl()
cn.sintoon.camera.Product: void setUrl(java.lang.String)
Note:一定要注意指定的類成員必須存在,如果不存在的話,這個規則相當於沒有配,一點作用沒有
-keepnames class specification
-keep,allowshrinking class specification的簡寫
指定要保留名稱的類成員和類成員(如果它們在壓縮階段未被刪除)。
例如,你可能希望保留實現 Serializable 接口的類的所有類名,以便處理后的代碼與任何原始序列化的類保持兼容。
完全不用的類仍然可以刪除。只有在混淆時才適用。
-keepnames class * implements java.io.Serializable
Note: 前提是在壓縮階段沒有被刪除掉,這里相當於使用了修飾符 allowshrinking
-keepclassmembernames class specification
-keepclassmembers,allowshrinking class specification 的簡寫
指定要保留名稱的類成員(如果它們在壓縮階段未被刪除)。
例如,在處理由JDK 1.2或更早版本編譯的庫時,可能希望保留合成類$方法的名稱。
所以當處理使用處理過的庫的應用程序時,混淆器可以再次檢測到它(盡管ProGuard本身不需要這個)。
只有在混淆時才適用。
-keepclassmembernames class * {
java.lang.Class class$(java.lang.String);
java.lang.Class class$(java.lang.String, boolean);
}
Note: 前提是在壓縮階段沒有被刪除掉,這里相當於使用了修飾符 allowshrinking
-keepclasseswithmembernames class specification
-keepclasseswithmembers,allowshrinking class specification 的簡寫
指定要保留名稱的類和類成員,條件是所有指定的類成員都存在於收縮階段之后。
例如,可能希望保留所有本機方法名稱和類別的名稱,以便處理的代碼仍可以與本機庫代碼鏈接。完全沒有使用的本地方法仍然可以被刪除。
如果使用了一個類文件,但它的本地方法都不是,它的名字仍然會被混淆。只有在混淆時才適用。
-keepclasseswithmembernames,includedescriptorclasses class * {
native <methods>;
}
Note: 前提是在壓縮階段沒有被刪除掉,這里相當於使用了修飾符 allowshrinking
-printseeds [filename]
指定詳盡列出由各種-keep選項匹配的類和類成員。列表打印到標准輸出或給定文件。該列表可用於驗證是否真的找到了預期的類成員,尤其是在使用通配符的情況下。
例如,您可能想要列出您保存的所有應用程序或所有小程序。
參考上面說的 seeds.txt
-whyareyoukeeping class specification
指定打印詳細信息,說明為什么給定的類和類成員正在壓縮步驟中。
如果想知道為什么某些給定元素出現在輸出中,這會很有用。
一般來說,可能有很多不同的原因。
此選項為每個指定的類和類成員打印最短的方法鏈到指定的種子或入口點。
在當前的實施中,打印出的最短鏈有時可能包含循環扣除 - 這些並不反映實際收縮過程。
如果指定了 -verbose 選項,則跟蹤包括完整的字段和方法簽名。只適用於壓縮。
壓縮規則
-dontshrink
指定不被壓縮的類文件。
默認情況下壓縮是開啟的,除了用各種用 keep
選項直接或間接用到的類或類成員,其他的都會被移除。
壓縮步驟通常在優化之后,因為某些優化可能會打開已經刪除的類或類成員。
-printusage [filename]
指定列出移除的死代碼。該列表打印到標准輸出或給定文件。
參考上面說的 usage.txt
例如,您可以列出應用程序的未使用代碼。只適用於壓縮。
優化規則
-dontoptimize
指定不優化輸入類文件。默認情況下,優化已啟用;所有方法都在字節碼級別進行了優化
-optimizationpasses n
指定要執行的優化傳遞的數量。
默認情況下,執行一次傳遞。多次通行可能會導致進一步的改進。如果在優化后沒有找到改進,則優化結束。只適用於優化。
混淆規則
-dontobfuscate
指定不混淆輸入的類文件。
默認情況下,混淆是開啟的,類和類成員會被改成新的短隨機名稱,除了各種-keep選項列出的名稱外。
內部屬性對於調試很有用,例如源文件名,變量名和行號被刪除。
-printmapping [filename]
指定將舊名稱映射到已重命名的類和類成員的新名稱的映射。映射打印到標准輸出或給定文件。
例如,它是后續增量混淆所必需的,或者如果想再次理解混淆的堆棧跟蹤。只有在混淆時才適用。
參考 上面說的 mapping.txt。
-useuniqueclassmembernames
指定將相同的混淆名稱分配給具有相同名稱的類成員,並將不同混淆名稱分配給名稱不同的類成員(對於每個給定的類成員簽名)。
沒有這個選項,更多的類成員可以被映射到相同的短名稱,比如'a','b'等等。
這個選項因此稍微增加了結果代碼的大小,但是它確保了保存的混淆名稱映射總是可以在隨后的增量混淆步驟中受到尊重。
例如,考慮兩個不同的接口,它們包含具有相同名稱和簽名的方法。如果沒有此選項,這些方法可能會在第一個混淆步驟中獲取不同的混淆名稱。
如果添加了包含實現兩個接口的類的補丁程序,則ProGuard必須在增量混淆步驟中為這兩種方法強制執行相同的方法名稱。
原始模糊代碼已更改,以保持結果代碼的一致性。在最初的混淆步驟中使用此選項,這種重命名將永遠不是必需的。
該選項僅適用於混淆。
實際上,如果計划執行增量混淆,則可能希望完全避免壓縮和優化,因為這些步驟可能會刪除或修改部分代碼,這些代碼對於以后的添加至關重要。
-dontusemixedcaseclassnames
指定在混淆時不生成混合大小寫的類名。
默認情況下,混淆的類名可以包含大寫字符和小寫字符的混合。
創建的這個完全可接受和可用的jars 只有在不區分大小寫的文件系統(比如Windows)的平台上解壓縮jar時,解壓縮工具可能會讓類似命名的類文件相互覆蓋。
解壓縮后自毀的代碼!真正想在Windows上解壓他們的jar的開發人員可以使用這個選項來關閉這種行為。
混淆的jars會因此變得稍大。
只有在混淆時才適用。
-keeppackagenames [package_filter]
指定不混淆給定的軟件包名稱。
可選過濾器是包名稱的逗號分隔列表。包名可以包含?,*和**通配符,並且它們可以在!否定器。只有在混淆時才適用。
-flattenpackagehierarchy [package_name]
指定將所有重命名的軟件包重新打包,方法是將它們移動到單個給定的父軟件包中。如果沒有參數或空字符串(''),程序包將移動到根程序包中。
該選項是進一步混淆軟件包名稱的一個示例。它可以使處理后的代碼更小,更難理解。
只有在混淆時才適用。
-repackageclasses [package_name]
指定將所有重命名的類文件重新打包,方法是將它們移動到單個給定的包中。沒有參數或者使用空字符串(''),該軟件包將被完全刪除。
該選項將覆蓋 -flattenpackagehierarchy 選項。
這是進一步模糊軟件包名稱的另一個例子。
它可以使處理后的代碼更小,更難理解。
其不推薦使用的名稱是-defaultpackage。
只有在混淆時才適用。
警告:如果在別處移動它們,則在其包目錄中查找資源文件的類將不再正常工作。如有疑問,請不要使用此選項,以免觸及包裝。
-keepattributes [attribute_filter]
指定要保留的任何可選屬性。這些屬性可以用一個或多個-keepattributes指令來指定。
可選過濾器是Java虛擬機和ProGuard支持的屬性名稱的逗號分隔列表。
屬性名稱可以包含?,*和**通配符,並且可以在之前加上!否定器。
例如,在處理庫時,您至少應保留Exceptions,InnerClasses和Signature屬性。
您還應該保留SourceFile和LineNumberTable屬性以生成有用的混淆堆棧跟蹤。
最后,如果你的代碼依賴於它們,你可能需要保留注釋。
只有在混淆時才適用。
# 保留Annotation不混淆
-keepattributes *Annotation*,InnerClasses
# 避免混淆泛型
-keepattributes Signature
# 拋出異常時保留代碼行號
-keepattributes SourceFile,LineNumberTable
-keepparameternames
指定保留所保存方法的參數名稱和類型。
該選項實際上保留了調試屬性LocalVariableTable和LocalVariableTypeTable的修剪版本。
處理庫時它可能很有用。
一些IDE可以使用這些信息來幫助使用該庫的開發人員,
例如工具提示或自動完成。
只有在混淆時才適用。
-renamesourcefileattribute [string]
指定要放入類文件的SourceFile屬性(和SourceDir屬性)中的常量字符串。請注意,該屬性必須首先出現,所以它也必須使用-keepattributes指令明確保留。
例如,您可能希望讓處理過的庫和應用程序生成有用的混淆堆棧跟蹤。
只有在混淆時才適用
預校驗 規則
-dontpreverify
指定不預先驗證已處理的類文件。
默認情況下,如果類文件針對Java Micro Edition或Java 6或更高版本,則會對其進行預驗證。
對於Java Micro Edition,需要進行預驗證,因此如果指定此選項,則需要在處理的代碼上運行外部預驗證程序。
對於Java 6,預驗證是可選的,但從Java 7開始,它是必需的。
只有在最終對Android時,它才不是必需的,因此您可以將其關閉以縮短處理時間。
-android
指定已處理的類文件針對Android平台。然后ProGuard確保一些功能與Android兼容。
例如,如果您正在處理Android應用程序,則應該指定此選項。
一般規則
-verbose
指定在處理期間寫出更多信息。如果程序以異常終止,則此選項將打印出整個堆棧跟蹤,而不僅僅是異常消息。
-dontnote [class_filter]
指定不打印有關配置中可能的錯誤或遺漏的注釋,
例如類名中的拼寫錯誤或缺少可能有用的選項。
可選的過濾器是一個正則表達式;
ProGuard不打印有關匹配名稱的類的注釋。
-dontwarn [class_filter]
指定不警告有關未解決的引用和其他重要問題。
可選的過濾器是一個正則表達式; ProGuard不打印關於具有匹配名稱的類的警告。忽略警告可能是危險的。
例如,如果處理確實需要未解決的類或類成員,則處理后的代碼將無法正常工作。
只有在你知道自己在做什么的情況下才使用此選項!
-ignorewarnings
指定打印任何關於未解決的引用和其他重要問題的警告,但在任何情況下都繼續處理,忽略警告。
忽略警告可能是危險的。
例如,如果處理確實需要未解決的類或類成員,則處理后的代碼將無法正常工作。
只有在知道自己在做什么的情況下才使用此選項!
-printconfiguration [filename]
指定使用包含的文件和替換的變量寫出已解析的整個配置。結構打印到標准輸出或給定文件。
這對於調試配置或將XML配置轉換為更易讀的格式有時會很有用。
-dump [filename]
指定在任何處理后寫出類文件的內部結構。結構打印到標准輸出或給定文件。
例如,可能希望寫出給定jar文件的內容,而不進行處理。
參考上面說的 dump.txt。
-addconfigurationdebugging
指定用調試語句來處理已處理的代碼,這些語句顯示缺少ProGuard配置的建議。
如果處理后的代碼崩潰,那么在運行時獲得實用提示可能非常有用,因為它仍然缺少一些反射配置。
例如,代碼可能是使用GSON庫序列化類,可能需要一些配置。通常可以將控制台的建議復制/粘貼到配置文件中。
警告:不要在發行版本中使用此選項,因為它將混淆信息添加到已處理的代碼中。
keep 選項修飾符
includedescriptorclasses
指定-keep選項所保存的方法和字段的類型描述符中的任何類也應保存。
在保留方法名稱時,這通常很有用,以確保方法的參數類型不會重命名。他們的簽名保持完全不變,並與本地庫兼容。
includecode
指定保持-keep選項所保存的字段的方法的代碼屬性也應該保留,即可能未被優化或模糊處理。這對於已優化或混淆的類通常很有用,以確保在優化期間未修改其代碼。
allowshrinking
指定-keep選項中指定的入口點可能會壓縮,即使必須另外保留它們。
也就是說,可以在壓縮步驟中刪除入口點,但如果它們是必需的,則它們可能未被優化或混淆。
allowoptimization
指定-keep選項中指定的入口點可能會被優化,即使它們必須另外保存。
也就是說,入口點可能會在優化步驟中被更改,但它們可能不會被刪除或混淆。
此修飾符僅用於實現不尋常的要求。
allowobfuscation
指定在-keep選項中指定的入口點可能會被混淆,即使它們必須另外保存。
也就是說,入口點可能在混淆步驟中被重命名,但它們可能不會被刪除或優化。
此修飾符僅用於實現不尋常的要求。
keep 選項之間的關系
壓縮和混淆的各種-keep選項起初看起來有點混亂,但實際上它們背后有一個模式。
下表總結了它們之間的關系:
內容 | 被刪除或重命名 | 被重命名 |
---|---|---|
類和類成員 | -keep | -keepnames |
只有類成員 | -keepclassmembers | -keepclassmembernames |
類和類成員,引用成員存在 | -keepclasseswithmembers | -keepclasseswithmembernames |
如果指定了一個沒有類成員的類,ProGuard只保留該類及其無參數的構造函數作為入口點。它可能仍會刪除,優化或混淆其他班級成員。
如果指定了一個方法,則ProGuard僅將該方法作為入口點進行保存。其代碼可能仍會進行優化和調整。
類規范
類規范是類和類成員(字段和方法)的模板。它用於各種-keep選項和-assumenosideeffects選項中。相應的選項僅適用於與模板匹配的類和類成員。
模板的設計看起來非常類似於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關鍵字指的是任何接口或類。interface 關鍵字限制匹配接口類。 enum關鍵字限制匹配枚舉類。在 interface 或 enum 關鍵字前加上!將匹配限制為不是接口或枚舉的類。
每一個類名字都必須是完全限定名,例如 java.lang.String 內部類用美元符號“$”分隔,例如java.lang.Thread$State。類名可以被指定為包含以下通配符的正則表達式:
-
? 匹配類名稱中的任何單個字符,但不匹配包分隔符。例如 "com.example.Test?" 可以匹配 "com.example.Test1" 和 "com.example.Test2" 但不能匹配 "com.example.Test12"
-
* 匹配不包含包分隔符的類名的任何部分。例如 "com.example.*Test*" 能夠匹配 "com.example.MyTest" 和 "com.example.MyTestProduct" 但不能匹配 "com.example.mxc.MyTest" 或者 "com.example.*" 能夠匹配 "com.example" 但不能匹配 "com.example.mxc"
-
** 匹配類名稱的任何部分,可能包含任意數量的包分隔符。例如,"**.Testz" 匹配除根包以外的所有包中的所有Test類。或者,"com.example.**" 匹配 "com.example" 中的所有類及其子包。
-
<n> 在相同的選項中匹配第n個匹配的通配符。例如,"com.example.*Foo<1>" 匹配"com.example.BarFooBar"。
為了獲得更多的靈活性,類名實際上可以是逗號分隔的類名列表,可以加!。這個符號看起來不是很像java,所以應該適度使用。
為了方便和向后兼容,類名*指任何類,而不考慮它的包。
-
extends 和 **implements ** 通常用來限制使用通配符的類。目前他們是一樣的。他們的意思是 只有繼承或實現了給定類的類才有資格。給定的類本身不包含在這個集合中。如果需要,應該在單獨的選項中指定。
-
@ 可用於將類和類成員限制為使用指定的注釋類型進行注釋的類。annotationtype 就像類名一樣被指定。
-
除了方法參數列表不包含參數名稱外,字段和方法在Java中的定義非常類似(就像在javadoc和javap等其他工具中一樣)。這些規范還可以包含以下通配符通配符:
通配符 | 意義 |
---|---|
<init> | 匹配任何構造方法 |
<fields> | 匹配任何字段 |
<methods> | 匹配任何方法 |
* | 匹配任何方法和字段 |
請注意,上述通配符沒有返回類型。只有<init>通配符才有一個參數列表。
字段和方法也可以使用正則表達式來指定。名稱可以包含以下通配符:
通配符 | 意義 |
---|---|
? | 匹配方法名的任何單個字符 |
* | 匹配方法名的任何部分 |
<n> | 在相同的選項中匹配第n個匹配的通配符 |
類型可以包含以下通配符
通配符 | 意義 |
---|---|
% | 匹配任何原始類型(boolean,int 等,不包含 void) |
? | 匹配類名中的單個字符 |
* | 匹配類名中的任何部分但不包含包分隔符 |
** | 匹配類名中的任何部分但不包含包分隔符 |
*** | 匹配任何類型(原始類型或者非原始類型,數組或者非數組) |
--- | 匹配任何類型的任意數量的參數 |
<n> | 在相同的選項中匹配第n個匹配的通配符。 |
請注意,?,*和**通配符永遠不會匹配基本類型。
而且,只有***通配符才能匹配任何維度的數組類型。
例如,“** get *()”匹配“java.lang.Object getObject()”,但不匹配“float getFloat()”和“java.lang.Object [] getObjects()”。
-
也可以使用短類名(無包)或使用完整的類名來指定構造函數。和Java語言一樣,構造函數規范有一個參數列表,但沒有返回類型。
-
類訪問修飾符和類成員訪問修飾符通常用於限制通配類和類成員。它們指定必須為成員設置相應的訪問標志以匹配。前面加 "!" 決定相應的訪問標志應該被取消設置。
允許組合多個標志(例如,public static)。這意味着必須設置兩個訪問標志(例如 public static ),除非它們有沖突,在這種情況下,至少必須設置其中一個(例如至少public或 protected)。
ProGuard支持可能由編譯器設置的其他修飾符 synthetic,bridge和varargs。