這篇文章描述了我編寫 Kotlin 編譯器插件的經驗。我的主要目標是爲 iOS(Kotlin/Native)創建一個 Kotlin 編譯器插件,類似於 Android 的kotlin-parcelize。結果是新的kotlin-parcelize-darwin插件。
序幕
儘管本文的主要焦點是 iOS,但讓我們退後一步,重新審視一下Android 中Parcelable
的kotlin-parcelize
編譯器插件和編譯器插件到底是什麼。
所述Parcelable接口允許我們連載實現類的包裹,因此它可以被表示爲一個字節數組。它還允許我們從 反序列化類,Parcel
以便恢復所有數據。此功能廣泛用於保存和恢復屏幕狀態,例如當暫停的應用程序由於內存壓力而首先終止,然後重新激活時。
實現Parcelable
接口很簡單。要實現的主要方法有兩種:writeToParcel(Parcel, …)
— 將數據寫入Parcel
,createFromParcel(Parcel)
— 從Parcel
. 需要逐個字段寫入數據,然後按照相同的順序讀取。這可能很簡單,但同時編寫樣板代碼很無聊。它也容易出錯,因此理想情況下您應該爲Parcelable
類編寫測試。
幸運的是,有一個 Kotlin 編譯器插件叫做kotlin-parcelize
. 啓用此插件後,您所要做的就是使用註釋對Parcelable
類進行@Parcelize
註釋。該插件將自動生成實現。這將刪除所有相關的樣板代碼,並在編譯時確保實現是正確的。
iOS 中的打包
因爲當應用程序被終止然後恢復時,iOS 應用程序有類似的行爲,所以也有一些方法可以保留應用程序的狀態。其中一種方式是使用NSCoding協議,它與Android的Parcelable
界面非常相似。類還必須實現兩種方法:encode(with: NSCoder)
— 將對象編碼爲NSCoder,init?(coder: NSCoder)
— 從NSCoder
.
適用於 iOS 的 Kotlin Native
Kotlin 不僅限於 Android,它還可以用於爲 iOS編寫Kotlin Native框架,甚至是多平臺共享代碼。並且由於 iOS 應用程序在應用程序終止然後恢復時具有類似的行爲,因此會出現相同的問題。適用於 iOS 的 Kotlin Native 提供了與 Objective-C 的雙向互操作性,這意味着我們可以同時使用NSCoding
和NSCoder
.
一個非常簡單的數據類可能如下所示:
data class User(
val name: String,
val age: Int,
val email: String
)
現在讓我們嘗試添加NSCoding
協議實現:
data class User(
val name: String,
val age: Int,
val email: String
) : NSCodingProtocol {
override fun encodeWithCoder(coder: NSCoder) {
coder.encodeObject(name, forKey = "name")
coder.encodeInt32(age, forKey = "age")
coder.encodeObject(email, forKey = "email")
}
override fun initWithCoder(coder: NSCoder): User =
User(
name = coder.decodeObjectForKey(key = "name") as String,
age = coder.decodeInt32ForKey(key = "age"),
email = coder.decodeObjectForKey(key = "email") as String
)
}
看起來很簡單。現在,讓我們嘗試編譯:
e: ...: Kotlin 實現 Objective-C 協議必須有 Objective-C 超類(例如 NSObject)
好吧,讓我們的User
數據類擴展NSObject
類:
data class User(
val name: String,
val age: Int,
val email: String
) : NSObject(), NSCodingProtocol {
// Omitted code
}
但再一次,它不會編譯!
e: ...: 不能覆蓋 'toString',而是覆蓋 'description'
這很有趣。似乎編譯器試圖覆蓋並生成該toString
方法,但是對於擴展的類,NSObject
我們需要覆蓋該description
方法。另一件事是我們可能根本不想擴展NSObject
該類,因爲這可能會阻止我們擴展另一個 Kotlin 類。
適用於 iOS 的 Parcelable
我們需要另一個不強制主類擴展任何東西的解決方案。讓我們定義一個Parcelable
接口如下:
interface Parcelable {
fun coding(): NSCodingProtocol
}
這很簡單。我們的Parcelable
類將只有一個coding
返回 的實例的方法NSCodingProtocol
。其餘的將由協議的實現來處理。
現在讓我們改變我們的User
類,讓它實現Parcelable
接口:
data class User(
val name: String,
val age: Int,
val email: String
) : Parcelable {
override fun coding(): NSCodingProtocol = CodingImpl(this)
private class CodingImpl(
private val data: User
) : NSObject(), NSCodingProtocol {
override fun encodeWithCoder(coder: NSCoder) {
coder.encodeObject(data.name, forKey = "name")
coder.encodeInt32(data.age, forKey = "age")
coder.encodeObject(data.email, forKey = "email")
}
override fun initWithCoder(coder: NSCoder): NSCodingProtocol = TODO()
}
}
我們創建了嵌套CodingImpl
類,它將依次實現NSCoding
協議。該encodeWithCoder
是和以前一樣,但initWithCoder
就是有點棘手。它應該返回一個NSCoding
協議實例。但是,現在User
類現在不符合。
我們在這裏需要一個解決方法,一箇中間持有人類:
class DecodedValue(
val value: Any
) : NSObject(), NSCodingProtocol {
override fun encodeWithCoder(coder: NSCoder) {
// no-op
}
override fun initWithCoder(coder: NSCoder): NSCodingProtocol? = null
}
的DecodedValue
類符合NSCoding
協議並保持的值。所有方法都可以爲空,因爲此類不會被編碼或解碼。
現在我們可以在 User 的initWithCoder
方法中使用這個類:
data class User(
val name: String,
val age: Int,
val email: String
) : Parcelable {
override fun coding(): NSCodingProtocol = CodingImpl(this)
private class CodingImpl(
private val data: User
) : NSObject(), NSCodingProtocol {
override fun encodeWithCoder(coder: NSCoder) {
// Omitted code
}
override fun initWithCoder(coder: NSCoder): DecodedValue =
DecodedValue(
User(
name = coder.decodeObjectForKey(key = "name") as String,
age = coder.decodeInt32ForKey(key = "age"),
email = coder.decodeObjectForKey(key = "email") as String
)
)
}
}
測試
我們現在可以編寫一個測試來確定它確實有效。測試可能有以下步驟:
-
User
使用一些數據創建類的實例 - 編碼通過
NSKeyedArchiver
,接收NSData
結果 - 解碼過
NSData
孔NSKeyedUnarchiver
- 斷言解碼的對象等於原始對象。
class UserParcelableTest {
@Test
fun encodes_end_decodes() {
val original =
User(
name = "Some Name",
age = 30,
email = "[email protected]"
)
val data: NSData = NSKeyedArchiver.archivedDataWithRootObject(original.coding())
val decoded = (NSKeyedUnarchiver.unarchiveObjectWithData(data) as DecodedValue).value as User
assertEquals(original, decoded)
}
}
編寫編譯器插件
我們已經Parcelable
爲 iOS定義了接口並在User
類中進行了嘗試,我們還測試了代碼。現在我們可以自動化Parcelable
實現,這樣代碼就會自動生成,就像kotlin-parcelize
在 Android 中一樣。
我們不能使用Kotlin 符號處理(又名 KSP),因爲它不能改變現有的類,只能生成新的類。所以,唯一的解決方案是編寫一個 Kotlin 編譯器插件。編寫 Kotlin 編譯器插件並不像想象的那麼容易,主要是因爲還沒有文檔,API 不穩定等。如果您打算編寫 Kotlin 編譯器插件,建議使用以下資源:
該插件的工作方式與kotlin-parcelize
. 還有就是Parcelable
接口的類應該實現和@Parcelize
註釋,Parcelable
類應該被註解。該插件Parcelable
在編譯時生成實現。當你編寫Parcelable
類時,它們看起來像這樣:
@Parcelize
data class User(
val name: String,
val age: Int,
val email: String
) : Parcelable
插件名稱
該插件的名稱是kotlin-parcelize-darwin
. 它有“-darwin”後綴,因爲最終它應該適用於所有 Darwin (Apple) 目標,但目前,我們只對 iOS 感興趣。
Gradle 模塊
- 我們需要的第一個模塊是
kotlin-parcelize-darwin
——它包含註冊編譯器插件的 Gradle 插件。它引用了兩個人工製品,一個用於 Kotlin/Native 編譯器插件,另一個用於所有其他目標的編譯器插件。 -
kotlin-arcelize-darwin-compiler
— 這是 Kotlin/Native 編譯器插件的模塊。 -
kotlin-parcelize-darwin-compiler-j
— 這是非本地編譯器插件的模塊。我們需要它,因爲它是強制性的,並且被 Gradle 插件引用。但實際上,它是空的,因爲我們不需要來自非本地變體的任何東西。 - k
otlin-parcelize-darwin-runtime
— 包含編譯器插件的運行時依賴項。比如Parcelable
接口和@Parcelize
註解都在這裏。 -
tests
— 包含對編譯器插件的測試,它將插件模塊添加爲Included Builds。
插件的典型安裝如下。
在根build.gradle
文件中:
buildscript {
dependencies {
classpath "com.arkivanov.parcelize.darwin:kotlin-parcelize-darwin:<version>"
}
}
在項目的build.gradle
文件中:
apply plugin: "kotlin-multiplatform"
apply plugin: "kotlin-parcelize-darwin"
kotlin {
ios()
sourceSets {
iosMain {
dependencies {
implementation "com.arkivanov.parcelize.darwin:kotlin-parcelize-darwin-runtime:<version>"
}
}
}
}
實施
Parcelable 代碼生成有兩個主要階段。我們需要:
- 通過爲接口中缺少的
fun coding(): NSCodingProtocol
方法添加合成存根使代碼可編譯Parcelable
。 - 爲第一步中添加的存根生成實現。
生成存根
這部分由實現接口的ParcelizeResolveExtension完成SyntheticResolveExtension
。很簡單,這個擴展實現了兩個方法:getSyntheticFunctionNames
和generateSyntheticMethods
。在編譯期間爲每個類調用這兩種方法。
override fun getSyntheticFunctionNames(thisDescriptor: ClassDescriptor): List<Name> =
if (thisDescriptor.isValidForParcelize()) {
listOf(codingName)
} else {
emptyList()
}
override fun generateSyntheticMethods(
thisDescriptor: ClassDescriptor,
name: Name,
bindingContext: BindingContext,
fromSupertypes: List<SimpleFunctionDescriptor>,
result: MutableCollection<SimpleFunctionDescriptor>
) {
if (thisDescriptor.isValidForParcelize() && (name == codingName)) {
result += createCodingFunctionDescriptor(thisDescriptor)
}
}
private fun createCodingFunctionDescriptor(thisDescriptor: ClassDescriptor): SimpleFunctionDescriptorImpl {
// Omitted code
}
如您所見,首先我們需要檢查訪問的類是否適用於 Parcelize。有這個isValidForParcelize
功能:
fun ClassDescriptor.isValidForParcelize(): Boolean =
annotations.hasAnnotation(parcelizeName) && implementsInterface(parcelableName)
我們只處理具有@Parcelize
註釋並實現Parcelable
接口的類。
生成存根實現
您可以猜到這是編譯器插件中最困難的部分。這是由實現接口的ParcelizeGenerationExtension完成的IrGenerationExtension
。我們需要實現一個方法:
override fun generate(moduleFragment: IrModuleFragment, pluginContext: IrPluginContext) {
// Traverse all classes
}
我們需要遍歷所提供的每個類IrModuleFragment
。在這種特殊情況下,有ParcelizeClassLoweringPass擴展ClassLoweringPass
。
ParcelizeClassLoweringPass
只覆蓋一種方法:
override fun lower(irClass: IrClass) {
// Generate the code
}
類遍歷本身很容易:
override fun generate(moduleFragment: IrModuleFragment, pluginContext: IrPluginContext) {
ParcelizeClassLoweringPass(ContextImpl(pluginContext), logs)
.lower(moduleFragment)
}
代碼生成分多個步驟完成。我不會在這裏提供完整的實現細節,因爲有很多代碼。相反,我將提供一些高級別的調用。我還將展示如果手動編寫生成的代碼會是什麼樣子。我相信這對本文的目的會更有用。但如果您好奇,請在此處查看實現細節:ParcelizeClassLoweringPass。
首先,我們再次需要再次檢查該類是否適用於 Parcelize:
override fun lower(irClass: IrClass) {
if (!irClass.toIrBasedDescriptor().isValidForParcelize()) {
return
}
// ...
}
接下來,我們需要將CodingImpl
嵌套類添加到irClass
,指定其超類型(NSObject
和NSCoding
)以及@ExportObjCClass
註釋(以使該類在運行時查找時可見)。
override fun lower(irClass: IrClass) {
// Omitted code
val codingClass = irClass.addCodingClass()
// ...
}
如果你正在跳槽或者正準備跳槽不妨動動小手,添加一下咱們的交流羣1012951431來獲取一份詳細的大廠面試資料爲你的跳槽多添一份保障。
接下來,我們需要將主構造函數添加到CodingImpl
類中。構造函數應該只有一個參數:data: TheClass
,因此我們還應該生成data
字段、屬性和 getter。
override fun lower(irClass: IrClass) {
// Omitted code
val codingClassConstructor = codingClass.addSimpleDelegatingPrimaryConstructor()
val codingClassConstructorParameter =
codingClassConstructor.addValueParameter {
name = Name.identifier("data")
type = irClass.defaultType
}
val dataField = codingClass.addDataField(irClass, codingClassConstructorParameter)
val dataProperty = codingClass.addDataProperty(dataField)
val dataGetter = dataProperty.addDataGetter(irClass, codingClass, dataField)
// ...
}
到目前爲止,我們已經生成了以下內容:
@Parcelize
data class TheClass(/*...*/) : Parcelable {
override fun coding(): NSCodingProtocol {
// Stub
}
private class CodingImpl(
private val data: TheClass
) : NSObject(), NSCodingProtocol {
}
}
讓我們添加NSCoding
協議實現:
override fun lower(irClass: IrClass) {
// Omitted code
codingClass.addEncodeWithCoderFunction(irClass, dataGetter)
codingClass.addInitWithCoderFunction(irClass)
// ...
}
現在生成的類如下所示:
@Parcelize
data class TheClass(/*...*/) : Parcelable {
override fun coding(): NSCodingProtocol {
// Stub
}
private class CodingImpl(
private val data: TheClass
) : NSObject(), NSCodingProtocol {
override fun encodeWithCoder(coder: NSCoder) {
coder.encodeXxx(data.someValue, forKey = "someValue")
// ...
}
override fun initWithCoder(coder: NSCoder): NSCodingProtocol? =
DecodedValue(
TheClass(
someValue = coder.decodeXxx(key = "someValue"),
// ...
)
)
}
}
最後,我們需要做的就是coding()
通過簡單地實例化CodingImpl
類來生成方法的主體:
override fun lower(irClass: IrClass) {
// Omitted code
irClass.generateCodingBody(codingClass)
}
生成的代碼:
@Parcelize
data class TheClass(/*...*/) : Parcelable {
override fun coding(): NSCodingProtocol = CodingImpl(this)
private class CodingImpl(
private val data: TheClass
) : NSObject(), NSCodingProtocol {
// Omitted code
}
}
使用插件
當我們Parcelable
在 Kotlin 中編寫類時會使用該插件。一個典型的用例是保留屏幕狀態。這使得應用在被 iOS 殺死後恢復到原始狀態成爲可能。另一個用例是在 Kotlin 中管理導航時保留導航堆棧。
這是Parcelable
在 Kotlin中使用的一個非常通用的示例,它演示瞭如何保存和恢復數據:
class SomeLogic(savedState: SavedState?) {
var value: Int = savedState?.value ?: Random.nextInt()
fun saveState(): SavedState =
SavedState(value = value)
fun generate() {
value = Random.nextInt()
}
@Parcelize
class SavedState(
val value: Int
) : Parcelable
}
這是我們如何Parcelable
在 iOS 應用程序中編碼和解碼類的示例:
class AppDelegate: UIResponder, UIApplicationDelegate {
private var restoredSomeLogic: SomeLogic? = nil
lazy var someLogic: SomeLogic = { restoredSomeLogic ?? SomeLogic(savedState: nil) }()
func application(_ application: UIApplication, shouldSaveApplicationState coder: NSCoder) -> Bool {
CoderUtilsKt.encodeParcelable(coder, value: someLogic.saveState(), key: "some_state")
return true
}
func application(_ application: UIApplication, shouldRestoreApplicationState coder: NSCoder) -> Bool {
let state: Parcelable? = CoderUtilsKt.decodeParcelable(coder, key: "some_state")
restoredSomeLogic = SomeLogic(savedState: state as? SomeLogic.SavedState)
return true
}
}
在 Kotlin 多平臺中打包
現在我們有兩個插件:kotlin-parcelize
Android 和kotlin-parcelize-darwin
iOS。我們可以應用這兩個插件並@Parcelize
在公共代碼中使用!
我們共享模塊的build.gradle
文件將如下所示:
plugins {
id("kotlin-multiplatform")
id("com.android.library")
id("kotlin-parcelize")
id("kotlin-parcelize-darwin")
}
kotlin {
android()
ios {
binaries {
framework {
baseName = "SharedKotlinFramework"
export("com.arkivanov.parcelize.darwin:kotlin-parcelize-darwin-runtime:<version>")
}
}
}
sourceSets {
iosMain {
dependencies {
api "com.arkivanov.parcelize.darwin:kotlin-parcelize-darwin-runtime:<version>"
}
}
}
}
在這一點上,我們將有機會獲得這兩個Parcelable
接口,並@Parcelize
標註在androidMain
和iosMain
源集。要將它們放在commonMain
源集中,我們需要使用expect/actual
.
在commonMain
源集中:
expect interface Parcelable
@OptionalExpectation
@Target(AnnotationTarget.CLASS)
expect annotation class Parcelize()
在iosMain
源集中:
actual typealias Parcelable = com.arkivanov.parcelize.darwin.runtime.Parcelable
actual typealias Parcelize = com.arkivanov.parcelize.darwin.runtime.Parcelize
在androidMain
源集中:
actual typealias Parcelable = android.os.Parcelable
actual typealias Parcelize = kotlinx.parcelize.Parcelize
在所有其他源集中:
actual interface Parcelable
現在我們可以commonMain
以通常的方式在源集中使用它。爲Android編譯時,將由kotlin-parcelize
插件處理。在爲 iOS 編譯時,將由kotlin-parcelize-darwin
插件處理。對於所有其他目標,它不會執行任何操作,因爲Parcelable
接口爲空且未定義註釋。
結論
在本文中,我們探索了kotlin-parcelize-darwin
編譯器插件。我們探索了它的結構和它是如何工作的。我們還學習瞭如何在 Kotlin Native 中使用它,如何kotlin-parcelize
在 Kotlin Multiplatform 中與 Android 的插件配對,以及如何Parcelable
在 iOS 端使用類。
您將在 GitHub 存儲庫中找到源代碼。儘管尚未發佈,但您已經可以通過發佈到本地 Maven 存儲庫或使用Gradle Composite builds來試用它。
存儲庫中提供了一個非常基本的示例項目,其中包含共享模塊以及 Android 和 iOS 應用程序。