前言

kotlin的by lazy关键字是很常用的,它表示延时初始化变量,只在第一次使用时才给它初始化。那么它是如何实现这种功能的呢?这篇文章从字节码和Java语言的角度揭密它的实现原理。

ViewModel和ViewBinding变量初始化过程

先举两个项目中最常见的例子:ViewModel和ViewBinding,了解一下为什么需要延时初始化。

看一段代码:

class MainActivity : AppCompatActivity() {private val viewModel: MainViewModel by lazy {ViewModelProviders.of(this).get(MainViewModel::class.java)}private val binding: ActivityMainBinding by lazy {ActivityMainBinding.inflate(layoutInflater)}override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)Log.i("MainActivity", "onCreate")}
}

Jetpack库中的ViewModel和ViewBinding的使用是非常常见的,ViewModel和ViewBinding类型的变量都是需要延时初始化,不能在声明时初始化。ViewModel是因为内部需要依赖Activity的成员变量mApplication,而mApplication是在attach时给赋值的。ViewBinding的初始化需要依赖Window的layoutInflater变量,而Window变量也是在attach时赋值的。

先看ViewModel是如何初始化的,在以下ViewModelProviders.of方法里会调用checkApplication判断application是否为空,为空则抛出异常:

public class ViewModelProviders {/*** @deprecated This class should not be directly instantiated*/@Deprecatedpublic ViewModelProviders() {}private static Application checkApplication(Activity activity) {Application application = activity.getApplication();if (application == null) {throw new IllegalStateException("Your activity/fragment is not yet attached to "+ "Application. You can't request ViewModel before onCreate call.");}return application;}@NonNull@MainThreadpublic static ViewModelProvider of(@NonNull Fragment fragment, @Nullable Factory factory) {Application application = checkApplication(checkActivity(fragment));if (factory == null) {factory = ViewModelProvider.AndroidViewModelFactory.getInstance(application);}return new ViewModelProvider(fragment.getViewModelStore(), factory);}

mApplication是Activity的成员变量,它是在attach时赋值的:

 final void attach(Context context, ActivityThread aThread,Instrumentation instr, IBinder token, int ident,Application application, Intent intent, ActivityInfo info,CharSequence title, Activity parent, String id,NonConfigurationInstances lastNonConfigurationInstances,Configuration config, String referrer, IVoiceInteractor voiceInteractor,Window window, ActivityConfigCallback activityConfigCallback, IBinder assistToken) {attachBaseContext(context);...mWindow = new PhoneWindow(this, window, activityConfigCallback);...mApplication = application;...}

layoutInflater变量同理,它需要通过mWindow变量获取,而mWindow也是在attach里赋值的:

 public LayoutInflater getLayoutInflater() {return getWindow().getLayoutInflater();}

Activity的attach方法是早于onCreate方法执行的,所以在onCreate方法里是可以访问这两个变量。

所以,ViewModel和ViewBinding类型变量都需要延时初始化。

下面开始进入正题,by lazy关键字是如何实现延时初始化。

by lazy关键字的字节码实现

查看以上MainActivity的字节码内容如下:

public final class com/devnn/demo/MainActivity extends androidx/appcompat/app/AppCompatActivity {...省略无关字节码// access flags 0x12private final Lkotlin/Lazy; viewModel$delegate@Lorg/jetbrains/annotations/NotNull;() // invisible// access flags 0x12private final Lkotlin/Lazy; binding$delegate@Lorg/jetbrains/annotations/NotNull;() // invisible// access flags 0x1public <init>()VL0LINENUMBER 27 L0ALOAD 0INVOKESPECIAL androidx/appcompat/app/AppCompatActivity.<init> ()VL1LINENUMBER 28 L1ALOAD 0NEW com/devnn/demo/MainActivity$viewModel$2DUPALOAD 0INVOKESPECIAL com/devnn/demo/MainActivity$viewModel$2.<init> (Lcom/devnn/demo/MainActivity;)VCHECKCAST kotlin/jvm/functions/Function0INVOKESTATIC kotlin/LazyKt.lazy (Lkotlin/jvm/functions/Function0;)Lkotlin/Lazy;PUTFIELD com/devnn/demo/MainActivity.viewModel$delegate : Lkotlin/Lazy;L2LINENUMBER 32 L2ALOAD 0NEW com/devnn/demo/MainActivity$binding$2DUPALOAD 0INVOKESPECIAL com/devnn/demo/MainActivity$binding$2.<init> (Lcom/devnn/demo/MainActivity;)VCHECKCAST kotlin/jvm/functions/Function0INVOKESTATIC kotlin/LazyKt.lazy (Lkotlin/jvm/functions/Function0;)Lkotlin/Lazy;PUTFIELD com/devnn/demo/MainActivity.binding$delegate : Lkotlin/Lazy;L3LINENUMBER 27 L3RETURNL4LOCALVARIABLE this Lcom/devnn/demo/MainActivity; L0 L4 0MAXSTACK = 4MAXLOCALS = 1// access flags 0x12private final getViewModel()Lcom/devnn/demo/MainViewModel;L0LINENUMBER 28 L0ALOAD 0GETFIELD com/devnn/demo/MainActivity.viewModel$delegate : Lkotlin/Lazy;ASTORE 1ALOAD 1INVOKEINTERFACE kotlin/Lazy.getValue ()Ljava/lang/Object; (itf)CHECKCAST com/devnn/demo/MainViewModelL1LINENUMBER 28 L1ARETURNL2LOCALVARIABLE this Lcom/devnn/demo/MainActivity; L0 L2 0MAXSTACK = 1MAXLOCALS = 2// access flags 0x12private final getBinding()Lcom/devnn/demo/databinding/ActivityMainBinding;L0LINENUMBER 32 L0ALOAD 0GETFIELD com/devnn/demo/MainActivity.binding$delegate : Lkotlin/Lazy;ASTORE 1ALOAD 1INVOKEINTERFACE kotlin/Lazy.getValue ()Ljava/lang/Object; (itf)CHECKCAST com/devnn/demo/databinding/ActivityMainBindingL1LINENUMBER 32 L1ARETURNL2LOCALVARIABLE this Lcom/devnn/demo/MainActivity; L0 L2 0MAXSTACK = 1MAXLOCALS = 2

观察字节码可以发现几点变化:

(1)、viewModel变量的类型被换成了kotlin.Lazy类型,变量名字也换成了viewModel$delegate。看名字也知道是用到了委托思想。

(2)、在MainActivity的init方法即构造方法中使用LazyKt的静态方法lazy,给viewModel$delegate变量赋值了。by lazy后面{}内初始化实现逻辑封装在了Function0类型变量MainActivity$viewModel$2中。

INVOKESTATIC kotlin/LazyKt.lazy (Lkotlin/jvm/functions/Function0;)Lkotlin/Lazy

注:LazyKt.lazy这个静态方法的入参类型是Function0,它代表零个参数(即没有参数)的回调:

package kotlin.jvm.functionspublic interface Function0<out R> : kotlin.Function<R> {public abstract operator fun invoke(): R
}

(3)、给MainActivity生成了一个get方法:getViewModel(),这个方法的返回类型正是我们需要的类型:com/devnn/demo/MainViewModel

通过字节码可以看到这个getViewModel()方法内部实现:

调用了viewModel$delegate(类型是kotlin.Lazy)变量的getValue()方法返回一个Object,强转成com/devnn/demo/MainViewModel再将其返回。

玄机就在这个Lazy的getValue方法。

然后继续看kotlin.Lazy的getValue的实现:

internal class UnsafeLazyImpl<out T>(initializer: () -> T) : Lazy<T>, Serializable {private var initializer: (() -> T)? = initializerprivate var _value: Any? = UNINITIALIZED_VALUEoverride val value: Tget() {if (_value === UNINITIALIZED_VALUE) {_value = initializer!!()initializer = null}@Suppress("UNCHECKED_CAST")return _value as T}override fun isInitialized(): Boolean = _value !== UNINITIALIZED_VALUEoverride fun toString(): String = if (isInitialized()) value.toString() else "Lazy value not initialized yet."private fun writeReplace(): Any = InitializedLazyImpl(value)
}

可以看到,当value是UNINITIALIZED_VALUE即未初始化时,就通过入参initializer(即Function0)初始化,并给value赋值,然后返回这个value。

这里有点类似于Java里的单例模式的懒汉模式。

到这时已经分析完了by lazy的字节码原理,大致过程就是将变量类型替换成了Lazy类型,然后通过Lazy类的getValue方法返回真实类型,getValue方法里通过判空来判断是否是首次访问。

关键还是通过委托的思想将变量初始化委托给了通用类型Lazy类。

ViewBinding延时初始化跟ViewModel是一样的,就不再分析了。

by lazy关键字的Java实现

kotlin的代码是可以转成Java代码的,我们查看一下它的Java代码,验证是否跟上面分析的一样:

public final class MainActivity extends AppCompatActivity {@NotNullprivate final Lazy viewModel$delegate = LazyKt.lazy((Function0)(new Function0() {@NotNullpublic final MainViewModel invoke() {ViewModel var1 = ViewModelProviders.of((FragmentActivity)MainActivity.this).get(MainViewModel.class);Intrinsics.checkNotNullExpressionValue(var1, "of(this).get(MainViewModel::class.java)");return (MainViewModel)var1;}// $FF: synthetic method// $FF: bridge methodpublic Object invoke() {return this.invoke();}}));@NotNullprivate final Lazy binding$delegate = LazyKt.lazy((Function0)(new Function0() {@NotNullpublic final ActivityMainBinding invoke() {ActivityMainBinding var1 = ActivityMainBinding.inflate(MainActivity.this.getLayoutInflater());Intrinsics.checkNotNullExpressionValue(var1, "inflate(layoutInflater)");return var1;}// $FF: synthetic method// $FF: bridge methodpublic Object invoke() {return this.invoke();}}));private final MainViewModel getViewModel() {Lazy var1 = this.viewModel$delegate;return (MainViewModel)var1.getValue();}private final ActivityMainBinding getBinding() {Lazy var1 = this.binding$delegate;return (ActivityMainBinding)var1.getValue();}

可以看到,跟上面的分析是一模一样的,它就是将字节码反编译成了Java代码而已。

Java的成员变量初始化是在构造方法(init方法)中完成的,有兴趣可以查看我的另一个篇文章: 从字节码角度理解kotlin构造函数、成员变量、init代码块执行顺序

关于Kotlin的by lazy关键字实现原理就介绍到此。

【Kotlin】by lazy关键字的实现原理相关推荐

  1. Kotlin 之 lateinit关键字 与lazy

    前言 lateinit是许多刚进入学习kotlin常常会掉入的坑之一.很多人为了保证java原有的语法,在定义变量和对象的过程中,会使用如下的定义方式. class Demo{private var ...

  2. 黑帽SEO关键字堆砌的原理以及作用

    讲讲黑帽SEO关键字堆砌的原理以及作用. 关键字堆砌,已经是一个很老的黑帽SEO技术了,是比较老的搜索引擎作弊手段,通过关键字堆砌,可以获得更好的效果.回到正题,关键字堆积,从字面上的意思非常好理解就 ...

  3. 深入理解volatile关键字---缓存一致性原理

    volatile关键字与缓存一致性 volatile这个关键字可能很多朋友都听说过,或许也都用过.在Java 5之前,它是一个备受争议的关键字,因为在程序中使用它往往会导致出人意料的结果.在Java ...

  4. 【对比Java学Kotlin】object 关键字

    两种用法 Kotlin 的 object 关键字有两种用法,一个是作为右值表达式的前缀,一个是作为类的前缀修饰符. object 表达式 object 表达式一般用于对现有类进行稍微修改.因为是临时使 ...

  5. 来,跟我一起撸Kotlin runBlocking/launch/join/async/delay 原理使用

    前言 协程系列文章: 一个小故事讲明白进程.线程.Kotlin 协程到底啥关系? 少年,你可知 Kotlin 协程最初的样子? 讲真,Kotlin 协程的挂起/恢复没那么神秘(故事篇) 讲真,Kotl ...

  6. Scala中lazy关键字的使用和理解

    Scala中lazy关键字的使用和理解 转载声明: 本文转自 Scala中lazy关键字的使用和理解 作者:br0x 转载仅为方便学习查看,一切权利属于原作者,如果带来不便请联系我删除. Scala中 ...

  7. 【scala】Scala中lazy关键字的使用和理解

    Scala中使用关键字lazy来定义惰性变量,实现延迟加载(懒加载). 惰性变量只能是不可变变量,并且只有在调用惰性变量时,才会去实例化这个变量. 在Java中,要实现延迟加载(懒加载),需要自己手动 ...

  8. Kotlin Jetpack 实战: 图解协程原理 | 开发者说·DTalk

    本文原作者: 朱涛,原文发布于: 朱涛的自习室 https://mp.weixin.qq.com/s/fN4cSg6jcFZo3Wb2_xcJVw 协程 (Coroutines),是 Kotlin「最 ...

  9. java实现关键词云_Java synchronized 关键字的实现原理

    数据同步需要依赖锁,那锁的同步又依赖谁?synchronized给出的答案是在软件层面依赖JVM,而Lock给出的方案是在硬件层面依赖特殊的CPU指令,大家可能会进一步追问:JVM底层又是如何实现sy ...

  10. synchronized关键字的底层原理以及JDK1.6之后的底层优化

    synchronized关键字底层原理属于JVM层面. 1. synchronized关键字修饰同步代码块,synchronized同步代码块的实现使用的是monitorenter和monitorex ...

最新文章

  1. vscode 插件设置
  2. 502 Bad Gateway nginx 解决
  3. 需要符合互联网时代需求的《飞秋》
  4. easyui php分页,easyui datagrid分页 4、easyUI-七种布局(layout)
  5. 共享计算机脱机访问计算机,让Windows7脱机共享访问更安全 -电脑资料
  6. getopt/getopt_long函数使用说明
  7. PolyCluster: Minimum Fragment Disagreement Clustering for Polyploid Phasing 多聚类:用于多倍体的最小碎片不一致聚类...
  8. 元子弹老师-吉他指弹右手技巧
  9. 史玉柱给创业者的五条建议
  10. python,在格式化字符串中使用半个大括号
  11. DPDK Rx flexible descriptor 在Intel E810 网卡中的使用
  12. 你试过吗?图像(细胞)分割
  13. liunx下rdesktop无法使用,安装remmina
  14. UIQ来电监听挂断原理和代码
  15. mysql数据库练习作业
  16. 高通batterydata电池曲线数据学习
  17. 从做深基础,曝光正在“变硬”的阿里云
  18. python无向带权图
  19. Kindeditor上传图片成功,但显示上传失败
  20. STP和RSTP详解-原理篇

热门文章

  1. 关于个人网贷查询系统网贷信用查询,公司开发图片整合技术
  2. Spire.PDF 教程:在C#中显示或隐藏PDF图层
  3. 笔记本电脑当作服务器外置显示器,我们为什么要给笔记本外接显示器,真的是多此一举?...
  4. 【LCA】BZOJ1776-[Usaco2010 Hol]cowpol 奶牛政坛
  5. MPB:西湖大学鞠峰组-​​微生物群落定量宏基因组和宏转录组
  6. web前端课程设计(HTML和CSS实现餐饮美食文化网站)静态HTML网页制作
  7. selenium 模拟登陆豆瓣,爬取武林外传的短评
  8. python 安装Cython
  9. gba模拟器ios_不越狱iOS设备安装GBA模拟器 GBA4iOS 方法
  10. Windows 2003 Server报 由于这台计算机没有远程桌面客户端访问许可证