# Kotlin cacheable - 缓存一切函数


# Kotlin cacheable - 缓存一切函数

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

## 背景

在 Kotlin 中，Lazy 是我们经常用到的操作，当我们需要用到时才创建对象，只需要一个 lazy 即可搞定，如下：

```kotlin
val myObj by lazy { createObj() }
```

但稍微了解一些 Lazy 背后原理的同学都知道，Lazy 是不完美的，下面列举一下它带来的影响：

1.  每个类会在 init 的时候创建所有的 Lazy 对象，性能其实不够极致
2.  Lambda 会被翻译成匿名内部类，对包大小也不友好
3.  Lazy 是无输入的，创建完成后，内部的对象永远不再会更改。

    > 开发过 Intellij 相关内容（Kotlin 编译器 / IDEA 插件）的同学应该知道，Intellij 提供了 Compute 的函数来实现追踪修改的 cache 功能

  


## Cacheable 框架

### 实现 Lazy

基于 Lazy 的这些问题，我能想到的最简单办法就是给每个 Lazy 对象创建一个 backend field 持有其数据，重写其 Kotlin getter 来实现缓存，伪代码如下：

```kotlin
// 转换前
@get:Cacheable
val myObj get() = createObj()

// 转换后
var backend_myObj = null
@get:Cacheable
val myObj get() = backend_myObj ?: createObj().also { backend_myObj = it }
```

我们可以看到，转换后的代码添加了一个幕后属性，并且在幕后属性为空的时候，执行创建操作并赋值幕后属性。当然在实际使用中，我会读取 Cacheable 注解中传递的参数来选择是否生成线程安全的 synchronized 初始化体。我选择了修改 Kotlin IR 来实现这个功能，只需要实现一个 KCP 即可，具体可以看仓库中代码。

### 追踪变动

我们也希望在函数参数变化的时候，动态决定要不要计算新的 value，下面是一个例子，在每次函数参数与上次不同时（通过 equals 方法），会重新计算函数返回值：

```kotlin
var a = 0

@Cacheable(cacheMode = CacheMode.TRACK_ARGS)
fun bar(param0: String, param1: Int): Int = ++a

// test function to call cacheable
// only changed when argument changed (through `equals`)
fun test() {
    bar("a", 0) // 1
    bar("a", 0) // 1
    bar("b", 1) // 2
    bar("b", 1) // 2
    bar("c", 2) // 3
    bar("c", 2) // 3
}
```

这个 bar 背后会生成如下代码（伪代码，实际更复杂）：

```kotlin
var backend_bar = null
var backend_bar$0 = null // 第 0 个参数
var backend_bar$1 = null // 第 1 个参数

@Cacheable(cacheMode = CacheMode.TRACK_ARGS)
fun bar(param0: String, param1: Int) {
    if (param0 == backend_bar$0 && param1 == backend_bar$1) {
        return backend_bar
    } else {
        backend_bar$0 = param0 // 覆盖旧 param0
        backend_bar$1 = param1 // 覆盖旧 param1
        backend_bar = ++a // 计算新值
        return backend_bar
    }
}
```

当然框架背后的默认逻辑是线程安全的，会自动生成 synchronized + 双重判断代码来保证同样的输入只会有一个输出。

  


## Skiplang 与纯函数

在之前，我写过一篇纯函数的思考文档，其中提到了 skiplang：[一个 Kotlin 开发，对于纯函数的思考](https://eqyrx3fg3l.feishu.cn/docx/Xq1XdKaRXoaS14x04RocdSnHnug)

Skiplang 的宗旨就在其网站主页，A programming language to skip the things you have already computed，在纯函数的情况下，意味着得知输入状态，那么输出状态就是唯一确定的，这种情况就非常适合做缓存，如果输入值已经计算过，那么直接可以返回缓存的输出值。

事实上本文所介绍的 Cacheable 框架也是类似的思想，它起于我对 Lazy 的不满，最后的结果是进一步朝着纯函数演进。


## 最后

- 仓库地址：https://github.com/zsqw123/kotlin-cacheable
- Gradle Plugin Portal：https://plugins.gradle.org/plugin/host.bytedance.kotlin-cacheable
- 飞书文档原文：https://eqyrx3fg3l.feishu.cn/docx/BhKodOaLhom97txemxdcx9vanLH
- 个人飞书主页：https://eqyrx3fg3l.feishu.cn/docx/TkWidN8RtoLK4ix1NRRcWpdmnQf
