Kotlin cacheable - 缓存一切函数
Kotlin cacheable - 缓存一切函数
之后文章会全部转到飞书文档发布,此网页可能存在更新不及时的问题,飞书文档首页 zsqw123 Homepage
背景
在 Kotlin 中,Lazy 是我们经常用到的操作,当我们需要用到时才创建对象,只需要一个 lazy 即可搞定,如下:
|
|
但稍微了解一些 Lazy 背后原理的同学都知道,Lazy 是不完美的,下面列举一下它带来的影响:
-
每个类会在 init 的时候创建所有的 Lazy 对象,性能其实不够极致
-
Lambda 会被翻译成匿名内部类,对包大小也不友好
-
Lazy 是无输入的,创建完成后,内部的对象永远不再会更改。
开发过 Intellij 相关内容(Kotlin 编译器 / IDEA 插件)的同学应该知道,Intellij 提供了 Compute 的函数来实现追踪修改的 cache 功能
Cacheable 框架
实现 Lazy
基于 Lazy 的这些问题,我能想到的最简单办法就是给每个 Lazy 对象创建一个 backend field 持有其数据,重写其 Kotlin getter 来实现缓存,伪代码如下:
|
|
我们可以看到,转换后的代码添加了一个幕后属性,并且在幕后属性为空的时候,执行创建操作并赋值幕后属性。当然在实际使用中,我会读取 Cacheable 注解中传递的参数来选择是否生成线程安全的 synchronized 初始化体。我选择了修改 Kotlin IR 来实现这个功能,只需要实现一个 KCP 即可,具体可以看仓库中代码。
追踪变动
我们也希望在函数参数变化的时候,动态决定要不要计算新的 value,下面是一个例子,在每次函数参数与上次不同时(通过 equals 方法),会重新计算函数返回值:
|
|
这个 bar 背后会生成如下代码(伪代码,实际更复杂):
|
|
当然框架背后的默认逻辑是线程安全的,会自动生成 synchronized + 双重判断代码来保证同样的输入只会有一个输出。
Skiplang 与纯函数
在之前,我写过一篇纯函数的思考文档,其中提到了 skiplang:一个 Kotlin 开发,对于纯函数的思考
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