# 让 Kotlin Inline 可以构造 NewInstance


> 之后文章会全部转到飞书文档发布，此网页可能存在更新不及时的问题，飞书文档首页 [zsqw123 Homepage](https://eqyrx3fg3l.feishu.cn/docx/TkWidN8RtoLK4ix1NRRcWpdmnQf)

# 让 Kotlin Inline 可以构造 NewInstance

### 原理概述

Kotlin 的 Inline 是一个很重要的能力，它能够极大的简化我们的开发，我们能够使用 Inline 做很多事情，同时 [reified](https://kotlinlang.org/docs/inline-functions.html#reified-type-parameters) 也是一个重要的能力，它能够让类型“实”化，提供更加安全的 `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](https://youtrack.jetbrains.com/issue/KT-6728/Suggestion-Allow-constraint-for-reified-type-parameter-to-have-zero-arg-constructor)

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

```kotlin
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](https://eqyrx3fg3l.feishu.cn/docx/Tbg6dsnLPoimtGxNENxcV3rXnAe)，但我觉得这太粗粒度了，我没有看爽，我希望能更进一步了解编译器内部实现，于是我先是做了 [Kotlin cacheable - 缓存一切函数](https://eqyrx3fg3l.feishu.cn/docx/BhKodOaLhom97txemxdcx9vanLH) 了解了 IR 修改的整体流程，这次我决定想深入探究 Kotlin inline 的真实逻辑。

事实上 Intrinsic 在 Kotlin 中非常常见，Kotlin 的很多操作都是通过 Intrinsic 进行实现的，比如语法层面的 `==`、`||`、`&&`、String 的 `plus`、`..` 和 `..<` 等，以及函数层面的 `::class.java`、`.javaClass`、`Array(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 文档](https://eqyrx3fg3l.feishu.cn/docx/Tbg6dsnLPoimtGxNENxcV3rXnAe)来对整体编译流程有所了解。下文我就讲讲其中最简单的一个细节，就是 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！如图：

![image.png](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 的反射操作，性能更好：

```kotlin
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


