Jetpack Room 庫在 SQLite 上提供了一個抽象層,能夠在沒有任何樣板代碼的情況下,提供編譯時驗證 SQL 查詢的能力。它通過處理代碼注解和生成 Java 源代碼的方式,實現上述行為。
Room
https://developer.android.google.cn/training/data-storage/room
注解處理器非常強大,但它們會增加構建時間。這對于用 Java 寫的代碼來說通常是可以接受的,但對于 Kotlin 而言,編譯時間消耗會非常明顯,這是因為 Kotlin 沒有一個內置的注解處理管道。相反,它通過 Kotlin 代碼生成了存根 Java 代碼來支持注解處理器,然后將其輸送到 Java 編譯器中進行處理。
由于并不是所有 Kotlin 源代碼中的內容都能用 Java 表示,因此有些信息會在這種轉換中丟失。同樣,Kotlin 是一種多平臺語言,但 KAPT 只在面向 Java 字節碼的情況下生效。
認識 Kotlin 符號處理
Kotlin 符號處理
https://github.com/google/ksp
隨著注解處理器在 Android 上的廣泛使用,KAPT 成為了編譯時的性能瓶頸。為了解決這個問題,Google Kotlin 編譯器團隊開始研究一個替代方案,來為 Kotlin 提供一流的注解處理支持。當這個項目誕生之初,我們非常激動,因為它將幫助 Room 更好地支持 Kotlin。從 Room 2.4 開始,它對 KSP 有了實驗性的支持,我們發現編譯速度提高了 2 倍,特別是在全量編譯的情況下。
本文內容重點不在注解的處理、Room 或者 KSP。而在于重點介紹我們在為 Room 添加 KSP 支持時所面臨的挑戰和所做的權衡。為了理解本文您并不需要了解 Room 或者 KSP,但必須熟悉注解處理。
注意: 我們在 KSP 發布穩定版之前就開始使用它了。因此,尚不確定之前做的一些決策是否適用于現在。
本篇文章旨在讓注解處理器的作者們在為項目添加 KSP 支持前,充分了解需要注意的問題。
Room 工作原理簡介
Room 的注解處理分為兩個步驟。有一些 “Processor” 類,它們遍歷用戶的代碼,驗證并提取必要的信息到 “值對象” 中。這些值對象被送到 “Writer” 類中,這些類將它們轉換為代碼。和其他諸多的注解處理器一樣,Room 非常依賴 Auto-Common 與 javax.lang.model 包 (Java 注解處理 API 包) 中頻繁引用的類。
Auto-Commonhttps://github.com/google/auto/tree/master/common
為了支持 KSP,我們有三種選擇:
復制 JavaAP 和 KSP 的每個 “Processor” 類,它們會有相同的值對象作為輸出,我們可以將其輸入到 Writer 中;
在 KSP/Java AP 之上創建一個抽象層,以便處理器擁有一個基于該抽象層的實現;
用 KSP 代替 JavaAP,并要求開發者也使用 KSP 來處理 Java 代碼。
選項 C 實際上是不可行的,因為它會對 Java 用戶造成嚴重的干擾。隨著 Room 使用數量的增加,這種破壞性的改變是不可能的。在 “A” 和 “B” 兩者之間,我們決定選擇 “B”,因為處理器具有相當數量的業務邏輯,將其分解并非易事。
認識 X-Processing
X-Processing
https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:room/room-compiler-processing/
在 JavaAP 和 KSP 上創建一個通用的抽象并非易事。Kotlin 和 Java 可以互操作,但模式卻不相同,例如,Kotlin 中特殊類的類型如 Kotlin 的值類或者 Java 中的靜態方法。此外,Java 類中有字段和方法,而 Kotlin 中有屬性和函數。
我們決定實現 “Room 需要什么”,而不是嘗試去追求完美的抽象。從字面意思來看,在 Room 中找到導入了 javax.lang.model 的每一個文件,并將其移動到 X-Processing 的抽象中。這樣一來,TypeElement 變成了 XTypeElement,ExecutableElemen 變成了 XExecutableElemen 等等。
遺憾的是,javax.lang.model API 在 Room 中的應用非常廣泛。一次性創建所有這些 X 類,會給審閱者帶來非常嚴重的心理負擔。因此,我們需要找到一種方法來迭代這一實現。
另一方面,我們需要證明這是可行的。所以我們首先對其做了原型設計,一旦驗證這是一個合理的選擇,我們就用他們自己的測試逐一重新實現了所有 X 類。
原型
https://android-review.googlesource.com/c/platform/frameworks/support/+/1362062
逐一重新實現了所有 X 類
https://android-review.googlesource.com/c/platform/frameworks/support/+/1362102
關于我說的實現 “Room 需要什么”,有一個很好的例子,我們可以在關于類的字段更改中看到。當 Room 處理一個類的字段時,它總是對其所有的字段感興趣,包括父類中的字段。所以我們在創建相應的 X-Processing API 時,只添加了獲取所有字段的能力。
interface XTypeElement { fun getAllFieldsIncludingPrivateSupers(): List《XVariableElement》}
更改https://android-review.googlesource.com/c/platform/frameworks/support/+/1362165/6/room/compiler-xprocessing/src/main/java/androidx/room/processing/javac/JavacTypeElement.kt
如果我們正在設計一個通用庫,這樣可能永遠不會通過 API 審查。但因為我們的目標只是 Room,并且它已經有一個與 TypeElement 具有相同功能的輔助方法,所以復制它可以減少項目的風險。
一旦我們有了基本的 X-Processing API 和它們的測試方法,下一步就是讓 Room 來調用這個抽象。這也是 “實現 Room 所需要的東西” 獲得良好回報的地方。Room 在 javax.lang.model API 上已經擁有了用于基本功能的擴展函數/屬性 (例如獲取 TypeElement 的方法)。我們首先更新了這些擴展,使其看起來與 X-Processing API 類似,然后在 1 CL 中將 Room 遷移到 X-Processing。
1 CLhttps://android-review.googlesource.com/c/platform/frameworks/support/+/1361181/21/room/compiler/src/main/kotlin/androidx/room/preconditions/Checks.kt
改進 API 可用性
保留類似 JavaAP 的 API 并不意味著我們不能改進任何東西。在將 Room 遷移到 X-Processing 之后,我們又實現了一系列的 API 改進。
例如,Room 多次調用 MoreElement/MoreTypes,以便在不同的 javax.lang.model 類型 (例如 MoreElements.asType) 之間進行轉換。相關調用通常如下所示:
val element: Element 。..if (MoreElements.isType(element)) { val typeElement:TypeElement = MoreElements.asType(element)}
MoreElements.asType
https://github.com/google/auto/blob/master/common/src/main/java/com/google/auto/common/MoreElements.java#L131
我們把所有的調用放到了 Kotlin contracts 中,這樣一來就可以寫成:
val element: XElement 。..if (element.isTypeElement()) { // 編譯器識別到元素是一個 XTypeElement}
Kotlin contracts
https://kotlinlang.org/docs/whatsnew13.html#contracts
另一個很好的例子是在一個 TypeElement 中找尋方法。通常在 JavaAP 中,您需要調用 ElementFilter 類來獲取 TypeElement 中的方法。與此相反,我們直接將其設為 XTypeElement 中的一個屬性。
// 前val methods = ElementFilter.methodsIn(typeElement.enclosedElements)// 后val methods = typeElement.declaredMethods
ElementFilter
https://docs.oracle.com/javase/7/docs/api/javax/lang/model/util/ElementFilter.html
最后一個例子,這也可能是我最喜歡的例子之一,就是可分配性。在 JavaAP 中,如果您要檢查給定的 TypeMirror 是否可以由另一個 TypeMirror 賦值,則需要調用 Types.isAssignable。
val type1: TypeMirror 。..val type2: TypeMirror 。..if (typeUtils.isAssignable(type1, type2)) { 。..}
Types.isAssignable
https://docs.oracle.com/javase/8/docs/api/javax/lang/model/util/Types.html#isAssignable-javax.lang.model.type.TypeMirror-javax.lang.model.type.TypeMirror-
這段代碼真的很難讀懂,因為您甚至無法猜到它是否驗證了類型 1 可以由類型 2 指定,亦或是完全相反的結果。我們已經有一個擴展函數如下:
fun TypeMirror.isAssignableFrom( types: Types, otherType: TypeMirror): Boolean
在 X-Processing 中,我們能夠將其轉換為 XType 上的常規函數,如下方所示:
interface XType { fun isAssignableFrom(other: XType): Boolean}
為 X-Processing 實現 KSP 后端
這些 X-Processing 接口每個都有自己的測試套件。我們編寫它們并非是用來測試 AutoCommon 或者 JavaAP 的,相反,編寫它們是為了在有了它們的 KSP 實現時,我們就可以運行測試用例來驗證它是否符合 Room 的預期。
AutoCommon
https://github.com/google/auto/tree/master/common
由于最初的 X-Processing API 是按照 avax.lang.model 建模,它們并非每次都適用于 KSP,所以我們也改進了這些 API,以便在需要時為 Kotlin 提供更好的支持。
這樣產生了一個新問題。現有的 Room 代碼庫是為了處理 Java 源代碼而寫的。當應用是由 Kotlin 編寫時,Room 只能識別該 Kotlin 在 Java 存根中的樣子。我們決定在 X-Processing 的 KSP 實現中保持類似行為。
例如,Kotlin 中的 suspend 函數在編譯時生成如下簽名:
// kotlinsuspend fun foo(bar:Bar):Baz// javaObject foo(bar:Bar, Continuation《? extends Baz》)
為保持相同的行為,KSP 中的 XMethodElement 實現為 suspend 方法合成了一個新參數,以及新的返回類型。(KspMethodElement.kt)
KspMethodElement.kt
https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspMethodElement.kt;l=108?q=KspSuspendMethodElement&ss=androidx
注意: 這樣做效果很好,因為 Room 生成的是 Java 代碼,即使在 KSP 中也是如此。當我們添加對 Kotlin 代碼生成的支持時,可能會引起一些變化。
另一個例子與屬性有關。Kotlin 屬性也可能具有基于其簽名的合成 getter/setter (訪問器)。由于 Room 期望找到這些訪問器作為方法 (參見: KspTypeElement.kt),因此 XTypeElement 實現了這些合成方法。
KspTypeElement.kt
https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeElement.kt;l=144
注意: 我們已有計劃更改 XTypeElement API 以提供屬性而非字段,因為這才是 Room 真正想要獲取的內容。正如您現在猜到的那樣,我們決定 “暫時” 不這樣做來減少 Room 的修改。希望有一天我們能夠做到這一點,當我們這樣做時,XTypeElement 的 JavaAP 實現將會把方法和字段作為屬性捆綁在一起。
在為 X-Processing 添加 KSP 實現時,最后一個有趣的問題是 API 耦合。這些處理器的 API 經常相互訪問,因此如果不實現 XField / XMethod,就不能在 KSP 中實現 XTypeElement,而 XField / XMethod 本身又引用了 XType 等等。在添加這些 KSP 實現的同時,我們為它們的實現部分寫了單獨的測試用例。當 KSP 的實現變得更加完整時,我們逐漸通過 KSP 后端啟動全部的 X-Processing 測試。
需要注意的是,在此階段我們只在 X-Processing 項目中運行測試,所以即使我們知道測試的內容沒問題,我們也無法保證所有的 Room 測試都能通過 (也稱之為單元測試 vs 集成測試)。我們需要通過一種方法來使用 KSP 后端運行所有的 Room 測試,“X-Processing-Testing” 就應運而生。
認識 X-Processing-Testing
X-Processing-Testing
https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:room/room-compiler-processing-testing/
注解處理器的編寫包含 20% 的處理器代碼和 80% 的測試代碼。您需要考慮到各種可能的開發者錯誤,并確保如實報告錯誤消息。為了編寫這些測試,Room 已經提供一個輔助方法如下:
fun runTest( vararg javaFileObjects: JavaFileObject, process: (TestInvocation) -》 Unit): CompilationResult
runTest 在底層使用了 Google Compile Testing 庫,并允許我們簡單地對處理器進行單元測試。它合成了一個 Java 注解處理器并在其中調用了處理器提供的 process 方法。
val entitySource : JavaFileObject //示例 @Entity 注釋類val result = runTest(entitySource) { invocation -》 val element = invocation.processingEnv.findElement(“Subject”) val entityValueObject = EntityProcessor(。..).process(element) // 斷言 entityValueObject}// 斷言結果是否有誤,警告等
Google Compile Testing
https://github.com/google/compile-testing
糟糕的是,Google Compile Testing 僅支持 Java 源代碼。為了測試 Kotlin 我們需要另一個庫,幸運的是有 Kotlin Compile Testing,它允許我們編寫針對 Kotlin 的測試,而且我們為該庫貢獻了對 KSP 支持。
Kotlin Compile Testing
https://github.com/tschuchortdev/kotlin-compile-testing
注意: 我們后來用內部實現替換了 Kotlin Compile Testing,以簡化 AndroidX Repo 中的 Kotlin/KSP 更新。我們還添加了更好的斷言 API,這需要我們對 KCT 執行 API 不兼容的修改操作。
內部實現
https://android-review.googlesource.com/c/platform/frameworks/support/+/1779266
作為能讓 KSP 運行所有測試的最后一步,我們創建了以下測試 API:
fun runProcessorTest( sources: List《Source》, handler: (XTestInvocation) -》 Unit): Unit
這個和原始版本之間的主要區別在于,它同時通過 KSP 和 JavaAP (或 KAPT,取決于來源) 運行測試。因為它多次運行測試且 KSP 和 JavaAP 兩者的判斷結果不同,因此無法返回單個結果。
因此,我們想到了一個辦法:
fun XTestInvocation.assertCompilationResult( assertion: (XCompilationResultSubject) -》 Unit}
每次編譯后,它都會調用結果斷言 (如果沒有失敗提示,則檢查編譯是否成功)。我們把每個 Room 測試重構為如下所示:
val entitySource : Source //示例 @Entity 注釋類runProcessorTest(listOf(entitySource)) { invocation -》 // 該代碼塊運行兩次,一次使用 JavaAP/KAPT,一次使用 KSP val element = invocation.processingEnv.findElement(“Subject”) val entityValueObject = EntityProcessor(。..).process(element) // 斷言 entityValueObject invocation.assertCompilationResult { // 結果被斷言為是否有 error,warning 等 hasWarningContaining(“。..”) }}
接下來的事情就很簡單了。將每個 Room 的編譯測試遷移到新的 API,一旦發現新的 KSP / X-Processing 錯誤,就會上報,然后實施臨時解決方案;這一動作反復進行。由于 KSP 正在大力開發中,我們確實遇到了很多 bug。每一次我們都會上報 bug,從 Room 源鏈接到它,然后繼續前進 (或者進行修復)。每當 KSP 發布之后,我們都會搜索代碼庫來找到已修復的問題,刪除臨時解決方案并啟動測試。
一旦編譯測試覆蓋情況較好,我們在下一步就會使用 KSP 運行 Room 的集成測試。這些是實際的 Android 測試應用,也會在運行時測試其行為。幸運的是,Android 支持 Gradle 變體,因此使用 KSP 和 KAPT 來運行我們 Kotlin 集成測試便相當容易。
集成測試
https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:room/integration-tests/
Kotlin 集成測試
https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:room/integration-tests/kotlintestapp/build.gradle
下一步
將 KSP 支持添加到 Room 只是第一步。現在,我們需要更新 Room 來使用它。例如,Room 中的所有類型檢查都忽略了 nullability,因為 javax.lang.model 的 TypeMirror 并不理解 nullability。因此,當調用您的 Kotlin 代碼時,Room 有時會在運行時觸發 NullPointerException。有了 KSP,這些檢查現在可在 Room 中創建新的 KSP bug (例如 b/193437407)。我們已經添加了一些臨時解決方案,但理想情況下,我們仍希望改進 Room 以正確處理這些情況。
b/193437407
https://issuetracker.google.com/issues/193437407
改進
https://android-review.googlesource.com/c/platform/frameworks/support/+/1844471
同樣,即使我們支持 KSP,Room 仍然只生成 Java 代碼。這種限制使我們無法添加對某些 Kotlin 特性的支持,比如 Value Classes。希望在將來,我們還能對生成 Kotlin 代碼提供一些支持,以便在 Room 中為 Kotlin 提供一流的支持。接下來,也許更多 :)。
Value Classes
https://kotlinlang.org/docs/inline-classes.html
我能在我的項目上使用 X-Processing 嗎?
答案是還不能;至少與您使用任何其他 Jetpack 庫的方式不同。如前文所述,我們只實現了 Room 需要的部分。編寫一個真正的 Jetpack 庫有很大的投入,比如文檔、API 穩定性、Codelabs 等,我們無法承擔這些工作。話雖如此,Dagger 和 Airbnb (Paris、DeeplinkDispatch) 都開始用 X-Processing 來支持 KSP (并貢獻了他們需要的東西)。也許有一天我們會把它從 Room 中分解出來。從技術層面上講,您仍然可以像使用 Google Maven 庫一樣使用它,但是沒有 API 保證可以這樣做,因此您絕對應該使用 shade 技術。
Paris
https://github.com/airbnb/paris
DeeplinkDispatch
https://github.com/airbnb/DeepLinkDispatch
Google Maven 庫
https://maven.google.com/web/index.html#androidx.room
shade
https://github.com/johnrengelman/shadow
總結
我們為 Room 添加了 KSP 支持,這并非易事但絕對值得。如果您在維護注解處理器,請添加對 KSP 的支持,以提供更好的 Kotlin 開發者體驗。
特別感謝 Zac Sweers 和 Eli Hart 審校這篇文章的早期版本,他們同時也是優秀的 KSP 貢獻者。
Zac Sweers
https://medium.com/@ZacSweers
Eli Hart
https://medium.com/@konakid
更多資源
關于 Room 對于 KSP 支持的 Issue Tracker
https://issuetracker.google.com/issues/160322705
X-Processing 源碼
https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:room/room-compiler-processing/
X-Processing-Testing 源碼
https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:room/room-compiler-processing-testing/
KSP 源碼
https://github.com/google/ksp
責任編輯:haq
-
處理器
+關注
關注
68文章
19404瀏覽量
230965 -
源代碼
+關注
關注
96文章
2946瀏覽量
66894
原文標題:Room & Kotlin 符號的處理
文章出處:【微信號:Google_Developers,微信公眾號:谷歌開發者】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論