写点什么

iOS 开发 - 为 iOS 编写 Kotlin Parcelize 编译器插件

用户头像
iOSer
关注
发布于: 14 小时前

这篇文章描述了我编写 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 = "name@domain.com"            )
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类时,它们看起来像这样:


@Parcelizedata 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. 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)
// ...}
复制代码


到目前为止,我们已经生成了以下内容:


@Parcelizedata 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)
// ...}
复制代码


现在生成的类如下所示:


@Parcelizedata 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)}
复制代码


生成的代码:


@Parcelizedata 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.Parcelableactual typealias Parcelize = com.arkivanov.parcelize.darwin.runtime.Parcelize
复制代码


androidMain源集中:


actual typealias Parcelable = android.os.Parcelableactual 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 应用程序。

如果你正在跳槽或者正准备跳槽不妨动动小手,添加一下咱们的交流群1012951431来获取一份详细的大厂面试资料为你的跳槽多添一份保障。

文末推荐:iOS 热门文集

用户头像

iOSer

关注

微信搜索添加微信 mayday1739 进微信群哦 2020.09.12 加入

更多大厂面试资料进企鹅群931542608

评论

发布
暂无评论
iOS开发-为 iOS 编写 Kotlin Parcelize 编译器插件