源起是同事的一次反馈,在提测期间报了一个 Kotlin.Lazy 的空指针异常,Lazy 的定义如下:
class TestA{ ... val xxxx:Service? by lazy{ xxxService() } ... } 复制代码
看起来很平常的 by lazy 为何会报空指针?在深入 lazy 源码查看的时候,并未发现任何可疑点,由于当时的代码逻辑涉及到并发调用,也查看了 by lazy 的初始化,默认实现是 SynchronizedLazyImpl,已经做了线程安全操作。
为了避免太多代码的干扰,我们将涉及到 by lazy 使用的地方都拷贝到了一个 Test 类中,然后通过 Decompile 反编译成 Java 代码来查看是否是 kotlin 的问题。
Kotlin 代码如下:
class TestA { init { .... initView() } private fun initView() { // 调用 Service 方法 service?.getName() } private val service: AService? by lazy { AService() } } 复制代码
反编译后的 Java 代码:
public final class TestA { private final Lazy service$delegate; private final void initView() { // 1、获取 service 实例 AService var10000 = this.getService(); if (var10000 != null) { var10000.getName(); } } private final AService getService() { Lazy var1 = this.service$delegate; Object var3 = null; // 2、调用 Lazy 的 getValue 方法 .return (AService)var1.getValue(); } public TestA() { this.initView(); // 3、初始化 Lazy 实例 .this.service$delegate = LazyKt.lazy((Function0)null.INSTANCE); } } 复制代码
通过代码的反编译立马查到问题:
- 在 TestA 的构造方法中,先执行 initView 方法获取 AService 的实例
- 但 getService 方法中的 Lazy 还没有初始化,却直接调用了 getValue 方法触发空指针异常
- 在 initView 结束之后再做 Lazy 的初始化,这时候已经晚了,异常已经出现了
那如何解决这问题呢?只需将 by lazy 提到了 init 代码块的前面,如下:
class TestA { private val service: AService? by lazy { AService() } init { initView() } ... } 复制代码
反编译结果:
public final class TestA { private final Lazy service$delegate; .... public TestA() { // 1、初始化 Lazy 实例 .this.service$delegate = LazyKt.lazy((Function0)null.INSTANCE); // 2、再调用 getService 方法 .this.initView(); } } 复制代码
- 构造终于是先初始化 Lazy 对象
- 再调用 initView 方法,这时候方法内的 Lazy.getValue 就能被正常调用了
是不是有点违背常识?为什么在方法里调用一个变量还会涉及到变量放置的位置,Kotlin 这高级语法糖恐怕连 C 都不如吧(嘲笑一番,哈哈)。
那 Kotlin 真的没有对其做语法检查吗?其实是有的,我改变下代码给大家看下:
IDE 会提示当前 service 未初始化,但该提示仅限在 init 代码块中调用 lazy 的时候提示,如果在 init 中调用一个中间方法,然后再从中间方法调用 lazy,该提示校验将会失效。
又被 Kotlin 语法糖坑惨的一天!!!
看到大家在公众号的反馈,我觉得有必要再总结一下:
1、kotlin 并没有规定 init 代码块不能调用 lazy,并且 lazy 与 init 本身就没冲突
2、在 init 代码块中直接调用 lazy ,Kt 会检测变量是否已被初始化,从这个点可以看出,Kt 团队是有意识到这个问题的,但可惜的是,在深度调用上,该检查失效了,导致问题被带到了运行时。Kt 团队为什么不能将检测做的更智能呢?
3、很多人对变量的申明都会放在方法上面,所以,基本上也不会遇到这个 case,从而认为这是基操。其实,基操这词太带有主观意识了,大家都知道代码中不能这么写,那为什么还要开发出 lint 工具呢?总不能靠基操来规范代码吧。
文章主要是介绍使用 Kt 时遇到的坑,项目中我们也在积极用 Kt,那些抹黑 Kt 的论调可以歇歇了,不带有论据的鼓吹才是真的给 Kt 抹黑