iOS開發-爲 iOS 編寫 Kotlin Parcelize 編譯器插件 序幕 iOS 中的打包 編寫編譯器插件 結論

這篇文章描述了我編寫 Kotlin 編譯器插件的經驗。我的主要目標是爲 iOS(Kotlin/Native)創建一個 Kotlin 編譯器插件,類似於 Android 的kotlin-parcelize。結果是新的kotlin-parcelize-darwin插件。

序幕

儘管本文的主要焦點是 iOS,但讓我們退後一步,重新審視一下Android 中Parcelablekotlin-parcelize編譯器插件和編譯器插件到底是什麼。

所述Parcelable接口允許我們連載實現類的包裹,因此它可以被表示爲一個字節數組。它還允許我們從 反序列化類,Parcel以便恢復所有數據。此功能廣泛用於保存和恢復屏幕狀態,例如當暫停的應用程序由於內存壓力而首先終止,然後重新激活時。

實現Parcelable接口很簡單。要實現的主要方法有兩種:writeToParcel(Parcel, …)— 將數據寫入ParcelcreateFromParcel(Parcel)— 從Parcel. 需要逐個字段寫入數據,然後按照相同的順序讀取。這可能很簡單,但同時編寫樣板代碼很無聊。它也容易出錯,因此理想情況下您應該爲Parcelable類編寫測試。

幸運的是,有一個 Kotlin 編譯器插件叫做kotlin-parcelize. 啓用此插件後,您所要做的就是使用註釋對Parcelable類進行@Parcelize註釋。該插件將自動生成實現。這將刪除所有相關的樣板代碼,並在編譯時確保實現是正確的。

iOS 中的打包

因爲當應用程序被終止然後恢復時,iOS 應用程序有類似的行爲,所以也有一些方法可以保留應用程序的狀態。其中一種方式是使用NSCoding協議,它與Android的Parcelable界面非常相似。類還必須實現兩種方法:encode(with: NSCoder)— 將對象編碼爲NSCoderinit?(coder: NSCoder)— 從NSCoder.

適用於 iOS 的 Kotlin Native

Kotlin 不僅限於 Android,它還可以用於爲 iOS編寫Kotlin Native框架,甚至是多平臺共享代碼。並且由於 iOS 應用程序在應用程序終止然後恢復時具有類似的行爲,因此會出現相同的問題。適用於 iOS 的 Kotlin Native 提供了與 Objective-C 的雙向互操作性,這意味着我們可以同時使用NSCodingNSCoder.

一個非常簡單的數據類可能如下所示:

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結果
  • 解碼過NSDataNSKeyedUnarchiver
  • 斷言解碼的對象等於原始對象。
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 模塊

  1. 我們需要的第一個模塊是kotlin-parcelize-darwin——它包含註冊編譯器插件的 Gradle 插件。它引用了兩個人工製品,一個用於 Kotlin/Native 編譯器插件,另一個用於所有其他目標的編譯器插件。
  2. kotlin-arcelize-darwin-compiler — 這是 Kotlin/Native 編譯器插件的模塊。
  3. kotlin-parcelize-darwin-compiler-j— 這是非本地編譯器插件的模塊。我們需要它,因爲它是強制性的,並且被 Gradle 插件引用。但實際上,它是空的,因爲我們不需要來自非本地變體的任何東西。
  4. k otlin-parcelize-darwin-runtime— 包含編譯器插件的運行時依賴項。比如Parcelable接口和@Parcelize註解都在這裏。
  5. 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 代碼生成有兩個主要階段。我們需要:

  1. 通過爲接口中缺少的fun coding(): NSCodingProtocol方法添加合成存根使代碼可編譯Parcelable
  2. 爲第一步中添加的存根生成實現。

生成存根

這部分由實現接口的ParcelizeResolveExtension完成SyntheticResolveExtension。很簡單,這個擴展實現了兩個方法:getSyntheticFunctionNamesgenerateSyntheticMethods。在編譯期間爲每個類調用這兩種方法。

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,指定其超類型(NSObjectNSCoding)以及@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-parcelizeAndroid 和kotlin-parcelize-darwiniOS。我們可以應用這兩個插件並@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標註在androidMainiosMain源集。要將它們放在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 應用程序。

文末推薦:iOS熱門文集

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章