Kotlin——你真的了解 by lazy吗

背景

kotlin中的语法糖by lazy相信都有用过,但是这里面的秘密却很少有人深究下去,还有网上充斥着大量的文章,却很少能说到本质的点上,所以本文以字节码的视角,揭开by lazy的秘密。

一个例子

class LazyClassTest {

    val lazyTest :Test by lazy {
        Log.i("hello","初始化") 1
        Test()
    }

    fun test(){
        Log.i("hello","$lazyTest")
        Log.i("hello","$lazyTest")
    }
}

如果执行test方法,请问代号为1的log会输出几次呢?答案是1次,明明我们在test方法中执行了两次lazyTest的获取,这其中有什么不为人知的事情吗!?其实这是kotlin在编译的时候给我们施加了魔法。

编译器背后的事情

为了看清楚编译器的事情,我们直接查看编译后的字节码,这里贴出来,后面解释

删除不必要的信息
 // access flags 0x18
  final static INNERCLASS com/example/newtestproject/LazyClassTest$lazyTest$2 null null

  // access flags 0x12
  private final Lkotlin/Lazy; lazyTest$delegate
  @Lorg/jetbrains/annotations/NotNull;() // invisible

  // access flags 0x1
  public <init>()V
   L0
    LINENUMBER 5 L0
    ALOAD 0
    INVOKESPECIAL java/lang/Object.<init> ()V
   L1
    LINENUMBER 7 L1
    ALOAD 0
    GETSTATIC com/example/newtestproject/LazyClassTest$lazyTest$2.INSTANCE : Lcom/example/newtestproject/LazyClassTest$lazyTest$2;
    CHECKCAST kotlin/jvm/functions/Function0
    INVOKESTATIC kotlin/LazyKt.lazy (Lkotlin/jvm/functions/Function0;)Lkotlin/Lazy;
    PUTFIELD com/example/newtestproject/LazyClassTest.lazyTest$delegate : Lkotlin/Lazy;
   L2
    LINENUMBER 5 L2
    RETURN
   L3
    LOCALVARIABLE this Lcom/example/newtestproject/LazyClassTest; L0 L3 0
    MAXSTACK = 2
    MAXLOCALS = 1

  // access flags 0x11
  public final getLazyTest()Lcom/example/newtestproject/Test;
  @Lorg/jetbrains/annotations/NotNull;() // invisible
   L0
    LINENUMBER 7 L0
    ALOAD 0
    GETFIELD com/example/newtestproject/LazyClassTest.lazyTest$delegate : Lkotlin/Lazy;
    ASTORE 1
    ALOAD 1
    INVOKEINTERFACE kotlin/Lazy.getValue ()Ljava/lang/Object; (itf)
    CHECKCAST com/example/newtestproject/Test
   L1
    LINENUMBER 7 L1
    ARETURN
   L2
    LOCALVARIABLE this Lcom/example/newtestproject/LazyClassTest; L0 L2 0
    MAXSTACK = 1
    MAXLOCALS = 2

  // access flags 0x11
  public final test()V
   L0
    LINENUMBER 13 L0
    LDC "hello"
    ALOAD 0
    INVOKEVIRTUAL com/example/newtestproject/LazyClassTest.getLazyTest ()Lcom/example/newtestproject/Test;
    INVOKESTATIC java/lang/String.valueOf (Ljava/lang/Object;)Ljava/lang/String;
    INVOKESTATIC android/util/Log.i (Ljava/lang/String;Ljava/lang/String;)I
    POP
   L1
    LINENUMBER 14 L1
    LDC "hello"
    ALOAD 0
    INVOKEVIRTUAL com/example/newtestproject/LazyClassTest.getLazyTest ()Lcom/example/newtestproject/Test;
    INVOKESTATIC java/lang/String.valueOf (Ljava/lang/Object;)Ljava/lang/String;
    INVOKESTATIC android/util/Log.i (Ljava/lang/String;Ljava/lang/String;)I
    POP
   L2
    LINENUMBER 15 L2
    RETURN
   L3
    LOCALVARIABLE this Lcom/example/newtestproject/LazyClassTest; L0 L3 0
    MAXSTACK = 2
    MAXLOCALS = 1

我们惊讶的发现,原本的类中居然多出了一个内部类com/example/newtestproject/LazyClassTestlazyTestlazyTestlazyTest2,命名这么长!没错,它就是编译的时候生成的“魔法的种子”,那么这里内部类有什么特别的地方吗?字节码层面是看不出来的,因为这个这只是编译时期的内容,我们在虚拟机运行的时候来看,它其实是一个实现了一个接口是Lazy的内部类

public interface Lazy<out T> {
    public abstract val value: T

    public abstract fun isInitialized(): kotlin.Boolean
}

lazy背后的延时加载

为什么用了lazy就有懒加载的效果呢?其实关键就是这个,我们在init阶段可以看到

    getstatic 'com/example/newtestproject/LazyClassTest$lazyTest$2.INSTANCE','Lcom/example/newtestproject/LazyClassTest$lazyTest$2;'
    checkcast 'kotlin/jvm/functions/Function0'
    INVOKESTATIC kotlin/LazyKt.lazy (Lkotlin/jvm/functions/Function0;)Lkotlin/Lazy;
    putfield 'com/example/newtestproject/LazyClassTest.lazyTest$delegate','Lkotlin/Lazy;'

在初始化的时候,只是调用了kotlin/LazyKt.lazy类的一个静态方法,针对属性复制的putfield指令,也只是对LazyClassTest.lazyTest$delegate这个内部类的一个Lkotlin/Lazy对象进行赋值,看起来其实跟我们的lazyTest变量毫无关系。真相是lazyTest具体的赋值操作被隐藏了而已。从这里就可以看到,为什么lazy是如何实现延时加载的!本质就是在初始化的时候只是生成一个内部类,不进行任何对目标对象进行赋值操作罢了!

获取操作

我们再观察一下对于lazyTest变量的访问操作,从字节码看到,每次对变量的获取都调用了LazyClassTest的getLazyTest方法!这个也是编译器生成的方法,具体可以看到

  public final com.example.newtestproject.Test getLazyTest() {
    aload 0
    getfield 'com/example/newtestproject/LazyClassTest.lazyTest$delegate','Lkotlin/Lazy;'
    astore 1
    aload 1
    INVOKEINTERFACE kotlin/Lazy.getValue ()Ljava/lang/Object; (itf)
    checkcast 'com/example/newtestproject/Test'
    areturn
  }

天呐!我们越来越接近终点了,首先是通过getfield指令获取了一个Lkotlin/Lazy变量,这个不就是上面我们赋值的东西吗!然后调用了一个普通的方法getValue就结束了.

也就是说,每次对lazyTest变量的访问,都间接转发到了一个编译时生成的内部类中的一个特殊属性所调用的方法!看到这个,读者可能会思考,既然每次访问都是调用同一个方法,为什么我们by lazy时声明的lambad会只执行一次呢?

编译时的字节码已经不能给我们带来答案了,这个因为像java虚拟机这种,关于具体类的调用会在运行时确定这个特性所带来的(区别于c/cpp)。

运行时的魔法

那好,我们还有最后一个神奇,就是debug,我们最终会发现,在运行时by lazy的调用,其实最终都会转到如下代码的执行

private class SynchronizedLazyImpl<out T>(initializer: () -> T, lock: Any? = null) : Lazy<T>, Serializable {
    private var initializer: (() -> T)? = initializer
    @Volatile private var _value: Any? = UNINITIALIZED_VALUE
    // final field is required to enable safe publication of constructed instance
    private val lock = lock ?: this

    override val value: T
        get() {
            val _v1 = _value
            if (_v1 !== UNINITIALIZED_VALUE) {
                @Suppress("UNCHECKED_CAST")
                return _v1 as T
            }

            return synchronized(lock) {
                val _v2 = _value
                if (_v2 !== UNINITIALIZED_VALUE) {
                    @Suppress("UNCHECKED_CAST") (_v2 as T)
                } else {
                    val typedValue = initializer!!()
                    _value = typedValue
                    initializer = null
                    typedValue
                }
            }
        }

这个类位于LazyJVM中(kotlin1.5.10),我们就找到最终的秘密了,原来一开始的时候变量就是UNINITIALIZED_VALUE,经过一次赋值操作后,就会变成实际的T所指代的类型,下次再访问的时候,就直接满足if条件返回了!所以这就是一次赋值的秘密!

还有我们可以看到,默认的by lazy操作第一次赋值时,是采用了synchronized进行了加锁操作!

总结

我们已经全方位揭秘了by lazy的魔法面纱,相信也对这个语法糖有了自己更深的理解,之所以写这篇文,是因为好多网上资料要么是含糊不清要么是无法解释本质,这里作为一个记录分享

作者:Pika
链接:https://juejin.cn/post/7106320684275482631

最后

如果想要成为架构师或想突破20~30K薪资范畴,那就不要局限在编码,业务,要会选型、扩展,提升编程思维。此外,良好的职业规划也很重要,学习的习惯很重要,但是最重要的还是要能持之以恒,任何不能坚持落实的计划都是空谈。

如果你没有方向,这里给大家分享一套由阿里高级架构师编写的《Android八大模块进阶笔记》,帮大家将杂乱、零散、碎片化的知识进行体系化的整理,让大家系统而高效地掌握Android开发的各个知识点。
在这里插入图片描述
相对于我们平时看的碎片化内容,这份笔记的知识点更系统化,更容易理解和记忆,是严格按照知识体系编排的。

一、架构师筑基必备技能

1、深入理解Java泛型
2、注解深入浅出
3、并发编程
4、数据传输与序列化
5、Java虚拟机原理
6、高效IO
……

在这里插入图片描述

二、Android百大框架源码解析

1.Retrofit 2.0源码解析
2.Okhttp3源码解析
3.ButterKnife源码解析
4.MPAndroidChart 源码解析
5.Glide源码解析
6.Leakcanary 源码解析
7.Universal-lmage-Loader源码解析
8.EventBus 3.0源码解析
9.zxing源码分析
10.Picasso源码解析
11.LottieAndroid使用详解及源码解析
12.Fresco 源码分析——图片加载流程

在这里插入图片描述

三、Android性能优化实战解析

  • 腾讯Bugly:对字符串匹配算法的一点理解
  • 爱奇艺:安卓APP崩溃捕获方案——xCrash
  • 字节跳动:深入理解Gradle框架之一:Plugin, Extension, buildSrc
  • 百度APP技术:Android H5首屏优化实践
  • 支付宝客户端架构解析:Android 客户端启动速度优化之「垃圾回收」
  • 携程:从智行 Android 项目看组件化架构实践
  • 网易新闻构建优化:如何让你的构建速度“势如闪电”?

在这里插入图片描述

四、高级kotlin强化实战

1、Kotlin入门教程
2、Kotlin 实战避坑指南
3、项目实战《Kotlin Jetpack 实战》

  • 从一个膜拜大神的 Demo 开始

  • Kotlin 写 Gradle 脚本是一种什么体验?

  • Kotlin 编程的三重境界

  • Kotlin 高阶函数

  • Kotlin 泛型

  • Kotlin 扩展

  • Kotlin 委托

  • 协程“不为人知”的调试技巧

  • 图解协程:suspend

在这里插入图片描述

五、Android高级UI开源框架进阶解密

1.SmartRefreshLayout的使用
2.Android之PullToRefresh控件源码解析
3.Android-PullToRefresh下拉刷新库基本用法
4.LoadSir-高效易用的加载反馈页管理框架
5.Android通用LoadingView加载框架详解
6.MPAndroidChart实现LineChart(折线图)
7.hellocharts-android使用指南
8.SmartTable使用指南
9.开源项目android-uitableview介绍
10.ExcelPanel 使用指南
11.Android开源项目SlidingMenu深切解析
12.MaterialDrawer使用指南
在这里插入图片描述

六、NDK模块开发

1、NDK 模块开发
2、JNI 模块
3、Native 开发工具
4、Linux 编程
5、底层图片处理
6、音视频开发
7、机器学习

在这里插入图片描述

七、Flutter技术进阶

1、Flutter跨平台开发概述
2、Windows中Flutter开发环境搭建
3、编写你的第一个Flutter APP
4、Flutter开发环境搭建和调试
5、Dart语法篇之基础语法(一)
6、Dart语法篇之集合的使用与源码解析(二)
7、Dart语法篇之集合操作符函数与源码分析(三)

在这里插入图片描述

八、微信小程序开发

1、小程序概述及入门
2、小程序UI开发
3、API操作
4、购物商场项目实战……

在这里插入图片描述

全套视频资料:

一、面试合集
在这里插入图片描述
二、源码解析合集

在这里插入图片描述
三、开源框架合集

在这里插入图片描述
欢迎大家一键三连支持,若需要文中资料,直接点击文末CSDN官方认证微信卡片免费领取↓↓↓

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值