目录

让 Kotlin Inline 可以构造 NewInstance

之后文章会全部转到飞书文档发布,此网页可能存在更新不及时的问题,飞书文档首页 zsqw123 Homepage

让 Kotlin Inline 可以构造 NewInstance

原理概述

Kotlin 的 Inline 是一个很重要的能力,它能够极大的简化我们的开发,我们能够使用 Inline 做很多事情,同时 reified 也是一个重要的能力,它能够让类型“实”化,提供更加安全的 as, ::class 等在常规的 Java 泛型中不安全的能力,在使用 reified T 之后:

  • as 的类型转换会变得安全,在非 Inline 的情况下,as T 会被事实上翻译为 as Any(或 T 的上界),意味着转换大概率会是成功的。
  • ::class、typeOf 等能力也能拿到对应类型,便于进行一些运行时的反射或元编程操作,而在没加 reified 时,由于泛型擦除,则会导致真实类型信息丢失。

但 reified 缺失了一个重要的能力:构造一个新对象。

而在这种能力的缺失下,通常来说我们会使用 ::class.java 拿到对应 Java Class 之后反射 newInstance 来创建对象,我们都知道反射是低效的,在我参与研发的一些超过 10 亿级用户量级的 App 中,性能对我们来说非常重要,而在大量页面频繁的 ViewModel 创建的背后,通常情况下都会使用 Activity/Fragment 自带的 ViewModelFactory 创建,即 NewInstanceFactory,通过反射创建 ViewModel。尽管 Dagger/Hilt 能一部分缓解这个问题,但仍不是我认为的最优解。

事实上在实践中会有很多人选择一些手动的做法,即手动传入 Factory 构建对象,但这实在不优雅,我认为既然是 Inline,那我一定能在编译期 Inline 时拿到对应的类型,创建对应的对象并完成 inline。

其实我在做完之后浏览 Youtrack 也发现有一些提到了这样的想法,但官方并没有去做,如:KT-6728 Suggestion-Allow-constraint-for-reified-type-parameter-to-have-zero-arg-constructor

于是我自己设计了这样一个 API:

1
2
3
inline fun <reified T> newInstance(): T {
    throw UnsupportedOperationException("This function is implemented as an intrinsic")
}

我打算在编译期间,通过 KCP 实现我自己的 Intrinsic,修改 inline 行为,拿到 T 的真实实现,查找其构造函数是否匹配,最终构建实例。现在直接想试试这个的同学可以直接移步文末仓库地址。这里我不讲太多我是如何用 KCP 实现的,我只说一些技术思考和思路。

在之前我对 Compiler 有过一次非常彻底的分析,我仔细分析了其执行流程:Kotlin Compilation Process Overview,但我觉得这太粗粒度了,我没有看爽,我希望能更进一步了解编译器内部实现,于是我先是做了 Kotlin cacheable - 缓存一切函数 了解了 IR 修改的整体流程,这次我决定想深入探究 Kotlin inline 的真实逻辑。

事实上 Intrinsic 在 Kotlin 中非常常见,Kotlin 的很多操作都是通过 Intrinsic 进行实现的,比如语法层面的 ==||&&、String 的 plus....< 等,以及函数层面的 ::class.java.javaClassArray(size) 等。

相信会有很多人在看到 Array 等类内部都是空实现的时候一定是一脸懵逼,我曾经也对这个问题有所好奇,但之前也一直没能找到我的答案,但现在我知道他们的实现其实都是 Intrinsic,只不过官方允许这种行为发生在 std lib 中罢了:

https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/3bc48ca2f3324a828b1e34243991c24c~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=1144&h=1022&s=104515&e=png&b=292d35

当然也不是所有标准库中的 Intrinsic 都是这么写的,一些新的 api 也使用了这种直接抛出 Exception 的方式来编写。如果我想要实现我自己的 Intrinsic,那对我们常规的 lib 开发者来说,使用 typeOf 的这种设计方案应当是最优解。

https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f7e6dd6a2df74fc9aa905a79e6501cf7~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=1412&h=469&s=66557&e=png&b=292d35

相关的代码实现大家可以直接去翻看 Compiler 中的 JvmIrCodegenFactory 的代码,同时在翻看之前也建议先去查看我的 Overview 文档来对整体编译流程有所了解。下文我就讲讲其中最简单的一个细节,就是 Kotlin 是如何实现 reified 的,同时也是插件如何规范的实现自己所需要的 reified 行为的一个参考。

在处理 Reified 时,编译器会将 FunctionCallSymbol 传递全部的 Intrinsic Processor,由 Intrinsic Processor 来决定是否要对其进行处理,需要注意的是此时我们还拿不到具体的类型信息,所谓的处理只是对你要处理的一些 Instruction node 进行标记。

如果处理,那么 Intrinsic Processor 需要在 InsnList 内添加如下的标记内容:

  • iconst(6) // 标记你要进行的 reified operationType,目前官方限定了 type,前几个都是内部用途,是有选 6 的时候才能够允许插件自定义自己的行为,事实上 6 是复用了 typeOf 的 operationType
  • aconst(T) // 你要对哪个 reified 的 typeParameter 进行处理
  • invokestatic Intrinsics.reifiedOperationMarker // 调用函数进行标记,如果此函数没有在内联函数中调用,那么会在运行期抛出异常
  • aconst(null) // 插入一些信息,一般来说为了栈平衡,推荐插入 null,但其实这里插入啥并不是很重要,但我有时会用这个东西藏一些怪东西来实现 Intrinsic Processor 之间的通信 :)
  • aconst(string) // 可以用来插入一些插件信息,如 plugin id 来在 Intrinsic Processor 时判断这个 Intrinsic 是不是应该我们处理
  • invokestatic magic // 这一步是个 magic call,标志着这段代码的结束,应当在 Intrinsic Processor 处理完之后删除

标记之后,Kotlin 编译器便会开始真正的内联操作,内联之后便能够拿到每个泛型参数的真实 IrType,并传递给每个 Intrinsic Processor 进行处理,编译器会将上文所说的 aconst(null) 节点传给你,你可以在这里展开你的操作,比如我们上文提到的 NewInstance!如图:

https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f032c2e11f744bf899a83402c63aa031~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=1658&h=782&s=72153&e=png&b=fffdfd

  • 对于前面的三个 Instruction,我们不需要修改,它也能帮我们防止被错误使用在不正常场景
  • 对于 aconst(null),我们将其修改为我们自己的指令来创建对象
  • 对于后面两个标记位,在处理完成之后我们应当将其删除。

这就是大致了原理啦,此外我还实现了有参数的构造函数,并能够在 compile 阶段 inline 时,对类型进行检查,但更多的详细实现可以去看的文末的仓库~

更多实际应用

就如我们在前文所提到的 ViewModel 的例子,在使用 newInstance 框架之后其 ktx api 设计能够变得更加简洁优雅,也不再需要 NewInstanceFactory 的反射操作,性能更好:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
inline fun <reified T : ViewModel> viewModel(): ReadOnlyProperty<Activity, T> {
    val factory = { newInstance<T>() }
    return ReadOnlyProperty { _, _ -> factory() }
}

inline fun <reified T : Fragment> fragment(): ReadOnlyProperty<Activity, T> {
    val factory = { newInstance<T>() }
    return ReadOnlyProperty { _, _ -> factory() }
}

class MyActivity : Activity() {
    val fooViewModel: FooViewModel by viewModel()
    val fooFragment: FooFragment by fragment()
}

不过只是简单的 proto type 伪代码,事实上的 ViewModel 还需要传入更多信息,如 lifecycle、ViewModelStore 等,但这一方式能让 by 代理的能力得到进一步的增强,性能也变得更加优异。

仓库及其他链接

  • 飞书文档原文:https://eqyrx3fg3l.feishu.cn/docx/CwMSdochfoOkvTximpgcLoZ3nBb
  • Github Repo kotlin-newInstance:https://github.com/zsqw123/kotlin-newInstance
  • Gradle plugin portal:https://plugins.gradle.org/plugin/host.bytedance.kotlin-newInstance
  • 个人主页:https://eqyrx3fg3l.feishu.cn/docx/TkWidN8RtoLK4ix1NRRcWpdmnQf