入口
為了決定哪些代碼要被保留哪些代碼要出丟棄和混淆,必須指定入口點,這些入口點通常是 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,
參考資料
-
https://www.guardsquare.com/en/proguard/manual/introduction
-
https://www.diycode.cc/topics/380
轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/1219.html
標籤:Android