这篇文章描述了我编写 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
)
)
}
}
复制代码
测试
我们现在可以编写一个测试来确定它确实有效。测试可能有以下步骤:
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
类时,它们看起来像这样:
@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 应用程序。
如果你正在跳槽或者正准备跳槽不妨动动小手,添加一下咱们的交流群1012951431来获取一份详细的大厂面试资料为你的跳槽多添一份保障。
文末推荐:iOS 热门文集
评论