Bootstrap

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

这篇文章描述了我编写 Kotlin 编译器插件的经验。我的主要目标是为 iOS(Kotlin/Native)创建一个 Kotlin 编译器插件,类似于 Android 的。结果是新的插件。

序幕

尽管本文的主要焦点是 iOS,但让我们退后一步,重新审视一下Android 中的编译器插件和编译器插件到底是什么。

所述接口允许我们连载实现类的,因此它可以被表示为一个字节数组。它还允许我们从 反序列化类,以便恢复所有数据。此功能广泛用于,例如当暂停的应用程序由于内存压力而首先终止,然后重新激活时。

实现接口很简单。要实现的主要方法有两种:— 将数据写入,— 从. 需要逐个字段写入数据,然后按照相同的顺序读取。这可能很简单,但同时编写样板代码很无聊。它也容易出错,因此理想情况下您应该为类编写测试。

幸运的是,有一个 Kotlin 编译器插件叫做. 启用此插件后,您所要做的就是使用注释对类进行注释。该插件将自动生成实现。这将删除所有相关的样板代码,并在编译时确保实现是正确的。

iOS 中的打包

因为当应用程序被终止然后恢复时,iOS 应用程序有类似的行为,所以也有一些方法可以保留应用程序的状态。其中一种方式是使用协议,它与Android的界面非常相似。类还必须实现两种方法:— 将对象编码为,— 从.

适用于 iOS 的 Kotlin Native

Kotlin 不仅限于 Android,它还可以用于为 iOS编写框架,甚至是共享代码。并且由于 iOS 应用程序在应用程序终止然后恢复时具有类似的行为,因此会出现相同的问题。适用于 iOS 的 Kotlin Native 提供了与 Objective-C 的双向互操作性,这意味着我们可以同时使用和.

一个非常简单的数据类可能如下所示:

data class User(
    val name: String,
    val age: Int,
    val email: String
)

现在让我们尝试添加协议实现:

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)好吧,让我们的数据类扩展类:

data class User(
    val name: String,
    val age: Int,
    val email: String
) : NSObject(), NSCodingProtocol {
    // Omitted code
}

但再一次,它不会编译!

e: ...: 不能覆盖 'toString',而是覆盖 'description'

这很有趣。似乎编译器试图覆盖并生成该方法,但是对于扩展的类,我们需要覆盖该方法。另一件事是我们可能根本不想扩展该类,因为这可能会阻止我们扩展另一个 Kotlin 类。

适用于 iOS 的 Parcelable

我们需要另一个不强制主类扩展任何东西的解决方案。让我们定义一个接口如下:

interface Parcelable {
    fun coding(): NSCodingProtocol
}

这很简单。我们的类将只有一个返回 的实例的方法。其余的将由协议的实现来处理。

现在让我们改变我们的类,让它实现接口:


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()
    }
}

我们创建了嵌套类,它将依次实现协议。该是和以前一样,但就是有点棘手。它应该返回一个协议实例。但是,现在类现在不符合。

我们在这里需要一个解决方法,一个中间持有人类:

class DecodedValue(
    val value: Any
) : NSObject(), NSCodingProtocol {
    override fun encodeWithCoder(coder: NSCoder) {
        // no-op
    }

    override fun initWithCoder(coder: NSCoder): NSCodingProtocol? = null
}

的类符合协议并保持的值。所有方法都可以为空,因为此类不会被编码或解码。

现在我们可以在 User 的方法中使用这个类:

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
                )
            )
    }
}

测试

我们现在可以编写一个测试来确定它确实有效。测试可能有以下步骤:

  • 使用一些数据创建类的实例

  • 编码通过,接收结果

  • 解码过孔

  • 断言解码的对象等于原始对象。

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)
    }
}

编写编译器插件

我们已经为 iOS定义了接口并在类中进行了尝试,我们还测试了代码。现在我们可以自动化实现,这样代码就会自动生成,就像在 Android 中一样。

我们不能使用(又名 KSP),因为它不能改变现有的类,只能生成新的类。所以,唯一的解决方案是编写一个 Kotlin 编译器插件。编写 Kotlin 编译器插件并不像想象的那么容易,主要是因为还没有文档,API 不稳定等。如果您打算编写 Kotlin 编译器插件,建议使用以下资源:

  • — 

  • — 文章

该插件的工作方式与. 还有就是接口的类应该实现和注释,类应该被注解。该插件在编译时生成实现。当你编写类时,它们看起来像这样:

@Parcelize
data class User(
    val name: String,
    val age: Int,
    val email: String
) : Parcelable

插件名称

该插件的名称是. 它有“-darwin”后缀,因为最终它应该适用于所有 Darwin (Apple) 目标,但目前,我们只对 iOS 感兴趣。

Gradle 模块

插件的典型安装如下。

在根文件中:

buildscript {
    dependencies {
        classpath "com.arkivanov.parcelize.darwin:kotlin-parcelize-darwin:"
    }
}

在项目的文件中:

apply plugin: "kotlin-multiplatform"
apply plugin: "kotlin-parcelize-darwin"

kotlin {
    ios()

    sourceSets {
        iosMain {
            dependencies {
                implementation "com.arkivanov.parcelize.darwin:kotlin-parcelize-darwin-runtime:"
            }
        }
    }
}

实施

Parcelable 代码生成有两个主要阶段。我们需要:

生成存根

这部分由实现接口的完成。很简单,这个扩展实现了两个方法:和。在编译期间为每个类调用这两种方法。

override fun getSyntheticFunctionNames(thisDescriptor: ClassDescriptor): List =
    if (thisDescriptor.isValidForParcelize()) {
        listOf(codingName)
    } else {
        emptyList()
    }

override fun generateSyntheticMethods(
    thisDescriptor: ClassDescriptor,
    name: Name,
    bindingContext: BindingContext,
    fromSupertypes: List,
    result: MutableCollection
) {
    if (thisDescriptor.isValidForParcelize() && (name == codingName)) {
        result += createCodingFunctionDescriptor(thisDescriptor)
    }
}

private fun createCodingFunctionDescriptor(thisDescriptor: ClassDescriptor): SimpleFunctionDescriptorImpl {
    // Omitted code
}

如您所见,首先我们需要检查访问的类是否适用于 Parcelize。有这个功能:

fun ClassDescriptor.isValidForParcelize(): Boolean =
    annotations.hasAnnotation(parcelizeName) && implementsInterface(parcelableName)

我们只处理具有注释并实现接口的类。

生成存根实现

您可以猜到这是编译器插件中最困难的部分。这是由实现接口的完成的。我们需要实现一个方法:


override fun generate(moduleFragment: IrModuleFragment, pluginContext: IrPluginContext) {
    // Traverse all classes
}

我们需要遍历所提供的每个类。在这种特殊情况下,有扩展。

 只覆盖一种方法:

override fun lower(irClass: IrClass) {
    // Generate the code
}

类遍历本身很容易:

override fun generate(moduleFragment: IrModuleFragment, pluginContext: IrPluginContext) {
    ParcelizeClassLoweringPass(ContextImpl(pluginContext), logs)
        .lower(moduleFragment)
}

代码生成分多个步骤完成。我不会在这里提供完整的实现细节,因为有很多代码。相反,我将提供一些高级别的调用。我还将展示如果手动编写生成的代码会是什么样子。我相信这对本文的目的会更有用。但如果您好奇,请在此处查看实现细节:

首先,我们再次需要再次检查该类是否适用于 Parcelize:

override fun lower(irClass: IrClass) {
    if (!irClass.toIrBasedDescriptor().isValidForParcelize()) {
        return
    }

    // ...
}

接下来,我们需要将嵌套类添加到,指定其超类型(和)以及注释(以使该类在运行时查找时可见)。

override fun lower(irClass: IrClass) {
    // Omitted code 

    val codingClass = irClass.addCodingClass()

    // ...
}

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

接下来,我们需要将主构造函数添加到类中。构造函数应该只有一个参数:,因此我们还应该生成字段、属性和 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 {
    }
}

让我们添加协议实现:

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"),
                    // ...
                )
            )
    }
}

最后,我们需要做的就是通过简单地实例化类来生成方法的主体:

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
    }
}

使用插件

当我们在 Kotlin 中编写类时会使用该插件。一个典型的用例是保留屏幕状态。这使得应用在被 iOS 杀死后恢复到原始状态成为可能。另一个用例是在 Kotlin 中管理导航时保留导航堆栈。

这是在 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
}

这是我们如何在 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 多平台中打包

现在我们有两个插件:Android 和iOS。我们可以应用这两个插件并在公共代码中使用!

我们共享模块的文件将如下所示:

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:")
            }
        }
    }

    sourceSets {
        iosMain {
            dependencies {
                api "com.arkivanov.parcelize.darwin:kotlin-parcelize-darwin-runtime:"
            }
        }
    }
}

在这一点上,我们将有机会获得这两个接口,并标注在和源集。要将它们放在源集中,我们需要使用.

在源集中:

expect interface Parcelable

@OptionalExpectation
@Target(AnnotationTarget.CLASS)
expect annotation class Parcelize()

在源集中:

actual typealias Parcelable = com.arkivanov.parcelize.darwin.runtime.Parcelable
actual typealias Parcelize = com.arkivanov.parcelize.darwin.runtime.Parcelize

在源集中:

actual typealias Parcelable = android.os.Parcelable
actual typealias Parcelize = kotlinx.parcelize.Parcelize

在所有其他源集中:

actual interface Parcelable

现在我们可以以通常的方式在源集中使用它。为Android编译时,将由插件处理。在为 iOS 编译时,将由插件处理。对于所有其他目标,它不会执行任何操作,因为接口为空且未定义注释。

结论

在本文中,我们探索了编译器插件。我们探索了它的结构和它是如何工作的。我们还学习了如何在 Kotlin Native 中使用它,如何在 Kotlin Multiplatform 中与 Android 的插件配对,以及如何在 iOS 端使用类。

您将在 GitHub 存储库中找到源代码。尽管尚未发布,但您已经可以通过发布到本地 Maven 存储库或使用来试用它。

存储库中提供了一个非常基本的,其中包含共享模块以及 Android 和 iOS 应用程序。

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

文末推荐:iOS热门文集

  • ① 

  • ② 

  • ③ 

  • ④