做者 / Morten Krogh-Jespeersen, Mads Agerhtml
R8 是 Android 默認的程序縮減器,它能夠經過移除未使用的代碼和優化其他代碼的方式下降 Android 應用大小,R8 同時也支持縮減 Android 庫大小。除了生成更小的庫文件,庫壓縮操做還能夠隱藏開發庫裏的新特性,等到這些特性相對穩定或者能夠面向公衆的時候再對外開放。java
Kotlin 對於編寫 Android 應用和開發庫來講是很是棒的開發語言。不過,使用 Kotlin 反射來縮減 Kotlin 開發庫或者應用就沒那麼簡單了。Kotlin 使用 Java 類文件中的元數據 來識別 Kotlin 語言中的結構。若是程序縮減器沒有維護和更新 Kotlin 的元數據,相應的開發庫或者應用就沒法正常工做。android
R8 如今支持維持和重寫 Kotlin 的元數據,從而全面支持使用 Kotlin 反射來壓縮 Kotlin 開發庫和應用。該特性適用於 Android Gradle 插件版本 4.1.0-beta03。歡迎你們踊躍嘗試,並在 Issue Tracker 頁面 向咱們反饋總體使用感覺和遇到的問題。git
本文接下來的內容爲你們介紹了 Kotlin 元數據的相關信息以及 R8 中對於重寫 Kotlin 元數據的支持。github
Kotlin 元數據 是存儲在 Java 類文件的註解中的一些額外信息,它由 Kotlin JVM 編譯器生成。元數據肯定了類文件中的類和方法是由哪些 Kotlin 代碼構成的。好比,Kotlin 元數據能夠告訴 Kotlin 編譯器類文件中的一個方法其實是 Kotlin 擴展函數。api
咱們來看一個簡單的例子,如下庫代碼定義了一個假想的用於指令構建的基類,用於構建編譯器指令。數據結構
package com.example.mylibrary /** CommandBuilderBase 包含 D8 和 R8 中通用的選項 */ abstract class CommandBuilderBase { internal var minApi: Int = 0 internal var inputs: MutableList<String> = mutableListOf() abstract fun getCommandName(): String abstract fun getExtraArgs(): String fun build(): String { val inputArgs = inputs.joinToString(separator = " ") return "${getCommandName()} --min-api=$minApi $inputArgs ${getExtraArgs()}" } } fun <T : CommandBuilderBase> T.setMinApi(api: Int): T { minApi = api return this } fun <T : CommandBuilderBase> T.addInput(input: String): T { inputs.add(input) return this }
而後,咱們能夠定義一個假想 D8CommandBuilder
的具體實現,它繼承自 CommandBuilderBase
,用於構建簡化的 D8 指令。jvm
package com.example.mylibrary /** D8CommandBuilder to build a D8 command. */ class D8CommandBuilder: CommandBuilderBase() { internal var intermediateOutput: Boolean = false override fun getCommandName() = "d8" override fun getExtraArgs() = "--intermediate=$intermediateOutput" } fun D8CommandBuilder.setIntermediateOutput(intermediate: Boolean) : D8CommandBuilder { intermediateOutput = intermediate return this }
上面的示例使用的擴展函數來保證當您在 D8CommandBuilder
上調用 setMinApi
方法的時候,所返回的對象類型是 D8CommandBuilder
而不是 CommandBuilderBase
。在咱們的示例中,這些擴展函數屬於頂層的函數,而且僅存在於 CommandBuilderKt
類文件中。接下來咱們來看一下經過精簡後的 javap
命令所輸出的內容。ide
$ javap com/example/mylibrary/CommandBuilderKt.class Compiled from "CommandBuilder.kt" public final class CommandBuilderKt { public static final <T extends CommandBuilderBase> T addInput(T, String); public static final <T extends CommandBuilderBase> T setMinApi(T, int); ... }
從 javap
的輸出內容裏能夠看到擴展函數被編譯爲靜態方法,該靜態方法的第一個參數是擴展接收器。不過這些信息還不足以告訴 Kotlin 編譯器這些方法須要做爲擴展函數在 Kotlin 代碼中調用。因此,Kotlin 編譯器還在類文件中增長了 kotlin.Metadata 註解。註解中的元數據裏包含本類中針對 Kotlin 特有的信息。若是咱們使用 verbose 選項就能夠在 javap 的輸出中看到這些註解。函數
$ javap -v com/example/mylibrary/CommandBuilderKt.class ... RuntimeVisibleAnnotations: 0: kotlin/Metadata( mv=[...], bv=[...], k=..., xi=..., d1=["^@.\n^B^H^B\n^B^X^B\n^@\n^B^P^N\n^B...^D"], d2=["setMinApi", ...])
元數據註解的 d1 字段包含了大部分實際的內容,它們以 protocol buffer 消息的形式存在。元數據內容的具體意義並不重要。重要的是 Kotlin 編譯器會讀取其中的內容,而且經過這些內容肯定了這些方法是擴展函數,以下 Kotlinp
dump 輸出內容所示。
$ kotlinp com/example/mylibrary/CommandBuilderKt.class package { // signature: addInput(CommandBuilderBase,String)CommandBuilderBase public final fun <T : CommandBuilderBase> T.addInput(input: kotlin/String): T // signature: setMinApi(CommandBuilderBase,I)CommandBuilderBase public final fun <T : CommandBuilderBase> T.setMinApi(api: kotlin/Int): T ... }
該元數據代表這些函數將在 Kotlin 用戶代碼中做爲 Kotlin 擴展函數使用:
D8CommandBuilder().setMinApi(12).setIntermediate(true).build()
正如前文所提到的,爲了可以在庫中使用 Kotlin API,Kotlin 的元數據很是重要,然而,元數據存在於註解中,而且會以 protocol buffer 消息的形式存在,而 R8 是沒法識別這些的。所以,R8 會從下面兩個選項中擇其一:
可是這兩個選項都不可取。
若是去除元數據,Kotlin 編譯器就再也沒法正確識別擴展函數。好比在咱們的例子中,當編譯相似 D8CommandBuilder().setMinApi(12)
這樣的代碼時,編譯器就會報錯,提示不存在該方法。這徹底說得通,由於沒有了元數據,Kotlin 編譯器惟一能看到的就是一個包含兩個參數的 Java 靜態方法。
保留原始的元數據也一樣會出問題。首先 Kotlin 元數據中所保留的類是父類的類型。因此,假設在縮減開發庫大小的時候,咱們僅但願 D8CommandBuilder
類可以保留它的名稱。這時候也就意味着 CommandBuilderBase
會被重命名,通常會被命名爲 a。若是咱們保留原始的 Kotlin 元數據,Kotlin 編譯器會在元數據中尋找 D8CommandBuilder
的超類。若是使用原始元數據,其中所記錄的超類是 CommandBuilderBase
而不是 a
。此時編譯就會報錯,而且提示 CommandBuilderBase
類型不存在。
爲了解決上述問題,擴展後的 R8 增長了維護和重寫 Kotlin 元數據的功能。它內嵌了 JetBrains 在 R8 中開發的 Kotlin 元數據開發庫。元數據開發庫能夠在原始輸入中讀取 Kotlin 元數據。元數據信息被存儲在 R8 的內部數據結構中。當 R8 完成對開發庫或者應用的優化和縮小工做後,它會爲全部聲明被保留的 Kotlin 類合成新的正確元數據。
來一塊兒看一下咱們的示例有哪些變化。咱們將示例代碼添加到一個 Android Studio 庫工程中。在 gradle.build 文件中,經過將 minifyEnbled
置 true 來啓用包大小縮減功能,咱們更新縮減器配置,使其包含以下內容:
#保留 D8CommandBuilder 和它的所有方法 -keep class com.example.mylibrary.D8CommandBuilder { <methods>; } #保留擴展函數 -keep class com.example.mylibrary.CommandBuilderKt { <methods>; } #保留 kotlin.Metadata 註解從而在保留項目上維持元數據 -keepattributes RuntimeVisibleAnnotations -keep class kotlin.Metadata { *; }
上述內容告訴 R8 保留 D8CommandBuilder
以及 CommandBuilderKt
中的所有擴展函數。它還告訴 R8 保留註解,尤爲是 kotlin.Metadata
註解。這些規則僅僅適用於那些被顯式聲明保留的類。所以,只有 D8CommandBuilder
和 CommandBuilderKt
的元數據會被保留。可是 CommandBuilderBase
中的元數據不會被保留。咱們這麼處理能夠減小應用和開發庫中沒必要要的元數據。
如今,啓用縮減後所生成的庫,裏面的 CommandBuilderBase
被重命名爲 a
。此外,所保留的類的 Kotlin 元數據也被重寫,這樣全部對於 CommandBuilderBase
的引用都被替換爲對 a
的引用。這樣開發庫就能夠正常使用了。
最後再說明一下,在 CommandBuilderBase
中不保留 Kotlin 元數據意味着 Kotlin 編譯器會將生成的類做爲 Java 類進行對待。這會致使庫中 Kotlin 類的 Java 實現細節產生奇怪的結果。要避免這樣的問題,就須要保留類。若是保留了類,元數據就會被保留。咱們能夠在保留規則中使用 allowobfuscation
修飾符來容許 R8 重命名類,生成 Kotlin 元數據,這樣 Kotlin 編譯器和 Android Studio 都會將該類視爲 Kotlin 類。
-keep,allowobfuscation class com.example.mylibrary.CommandBuilderBase
到這裏,咱們介紹了庫縮減和 Kotlin 元數據對於 Kotlin 開發庫的做用。經過 kotlin-reflect 庫使用 Kotlin 反射的應用一樣須要 Kotlin 元數據。應用和開發庫所面臨的問題是同樣的。若是 Kotlin 元數據被刪除或者沒有被正確更新,kotlin-reflect 庫就沒法將代碼做爲 Kotlin 代碼進行處理。
舉個簡單的例子,好比咱們但願在運行時查找而且調用某個類中的一個擴展函數。咱們但願啓用方法重命名,由於咱們並不關心函數名,只要能在運行時找到它而且調用便可。
class ReflectOnMe() { fun String.extension(): String { return capitalize() } } fun reflect(receiver: ReflectOnMe): String { return ReflectOnMe::class .declaredMemberExtensionFunctions .first() .call(receiver, "reflection") as String }
在代碼中,咱們添加了一個調用: reflect(ReflectOnMe())
。它會找到定義在 ReflectOnMe
中的擴展函數,而且使用傳入的 ReflectOnMe
實例做爲接收器,"reflection"
做爲擴展接收器來調用它。
如今 R8 能夠在全部保留類中正確重寫 Kotlin 元數據,咱們能夠經過使用下面的縮減器配置啓用重寫。
#保留反射的類和它的方法 -keep,allowobfuscation class ReflectOnMe { <methods>; } #保留 kotlin.Metadata 註解從而在保留項目上維持元數據 -keepattributes RuntimeVisibleAnnotations -keep class kotlin.Metadata { *; }
這樣的配置使得縮減器在重命名 ReflectOnMe 和擴展函數的同時,仍然維持而且重寫 Kotlin 元數據。
歡迎嘗試 R8 對於 Kotlin 庫項目中 Kotlin 元數據重寫的特性,以及在 Kotlin 項目中使用 Kotlin 反射。該特性能夠在 Android Gradle Plugin 4.1.0-beta03 及之後的版本中使用。若是在使用過程當中遇到任何問題,請在咱們的 Issue Tracker 頁面中提交問題。