Kotlin by lazy关键字深入探究实现原理

目录
  • 前言
  • ViewModel和ViewBinding变量初始化过程
  • by lazy关键字的字节码实现
  • by lazy关键字的Java实现

前言

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
     */
    @Deprecated
    public 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
    @MainThread
    public 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 0x12
  private final Lkotlin/Lazy; viewModel$delegate
  @Lorg/jetbrains/annotations/NotNull;() // invisible
  // access flags 0x12
  private final Lkotlin/Lazy; binding$delegate
  @Lorg/jetbrains/annotations/NotNull;() // invisible
  // access flags 0x1
  public <init>()V
   L0
    LINENUMBER 27 L0
    ALOAD 0
    INVOKESPECIAL androidx/appcompat/app/AppCompatActivity.<init> ()V
   L1
    LINENUMBER 28 L1
    ALOAD 0
    NEW com/devnn/demo/MainActivity$viewModel$2
    DUP
    ALOAD 0
    INVOKESPECIAL com/devnn/demo/MainActivity$viewModel$2.<init> (Lcom/devnn/demo/MainActivity;)V
    CHECKCAST kotlin/jvm/functions/Function0
    INVOKESTATIC kotlin/LazyKt.lazy (Lkotlin/jvm/functions/Function0;)Lkotlin/Lazy;
    PUTFIELD com/devnn/demo/MainActivity.viewModel$delegate : Lkotlin/Lazy;
   L2
    LINENUMBER 32 L2
    ALOAD 0
    NEW com/devnn/demo/MainActivity$binding$2
    DUP
    ALOAD 0
    INVOKESPECIAL com/devnn/demo/MainActivity$binding$2.<init> (Lcom/devnn/demo/MainActivity;)V
    CHECKCAST kotlin/jvm/functions/Function0
    INVOKESTATIC kotlin/LazyKt.lazy (Lkotlin/jvm/functions/Function0;)Lkotlin/Lazy;
    PUTFIELD com/devnn/demo/MainActivity.binding$delegate : Lkotlin/Lazy;
   L3
    LINENUMBER 27 L3
    RETURN
   L4
    LOCALVARIABLE this Lcom/devnn/demo/MainActivity; L0 L4 0
    MAXSTACK = 4
    MAXLOCALS = 1
  // access flags 0x12
  private final getViewModel()Lcom/devnn/demo/MainViewModel;
   L0
    LINENUMBER 28 L0
    ALOAD 0
    GETFIELD com/devnn/demo/MainActivity.viewModel$delegate : Lkotlin/Lazy;
    ASTORE 1
    ALOAD 1
    INVOKEINTERFACE kotlin/Lazy.getValue ()Ljava/lang/Object; (itf)
    CHECKCAST com/devnn/demo/MainViewModel
   L1
    LINENUMBER 28 L1
    ARETURN
   L2
    LOCALVARIABLE this Lcom/devnn/demo/MainActivity; L0 L2 0
    MAXSTACK = 1
    MAXLOCALS = 2
  // access flags 0x12
  private final getBinding()Lcom/devnn/demo/databinding/ActivityMainBinding;
   L0
    LINENUMBER 32 L0
    ALOAD 0
    GETFIELD com/devnn/demo/MainActivity.binding$delegate : Lkotlin/Lazy;
    ASTORE 1
    ALOAD 1
    INVOKEINTERFACE kotlin/Lazy.getValue ()Ljava/lang/Object; (itf)
    CHECKCAST com/devnn/demo/databinding/ActivityMainBinding
   L1
    LINENUMBER 32 L1
    ARETURN
   L2
    LOCALVARIABLE this Lcom/devnn/demo/MainActivity; L0 L2 0
    MAXSTACK = 1
    MAXLOCALS = 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.functions
public 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)? = initializer
    private var _value: Any? = UNINITIALIZED_VALUE
    override val value: T
        get() {
            if (_value === UNINITIALIZED_VALUE) {
                _value = initializer!!()
                initializer = null
            }
            @Suppress("UNCHECKED_CAST")
            return _value as T
        }
    override fun isInitialized(): Boolean = _value !== UNINITIALIZED_VALUE
    override 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 {
   @NotNull
   private final Lazy viewModel$delegate = LazyKt.lazy((Function0)(new Function0() {
      @NotNull
      public 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 method
      public Object invoke() {
         return this.invoke();
      }
   }));
   @NotNull
   private final Lazy binding$delegate = LazyKt.lazy((Function0)(new Function0() {
      @NotNull
      public final ActivityMainBinding invoke() {
         ActivityMainBinding var1 = ActivityMainBinding.inflate(MainActivity.this.getLayoutInflater());
         Intrinsics.checkNotNullExpressionValue(var1, "inflate(layoutInflater)");
         return var1;
      }
      // $FF: synthetic method
      // $FF: bridge method
      public 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关键字深入探究实现原理的文章就介绍到这了,更多相关Kotlin by lazy内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Kotlin对象的懒加载方式by lazy 与 lateinit 异同详解

    目录 前言 lateinit by lazy 总结 前言 属性或对象的延时加载是我们相当常用的,一般我们都是使用 lateinit 和 by lazy 来实现. 他们两者都是延时初始化,那么在使用时那么他们两者有什么区别呢? lateinit 见名知意,延时初始化的标记.lateinit var可以让我们声明一个变量并且不用马上初始化,在我们需要的时候进行手动初始化即可. 如果我们不初始化会怎样? private lateinit var name: String findViewById<Bu

  • Kotlin lateinit与by lazy案例详解

    lateinit 和 lazy 是 Kotlin 中的两种不同的延迟初始化的实现 lateinit 只用于变量 var,而 lazy 只用于常量 val lazy 应用于单例模式(if-null-then-init-else-return),而且当且仅当变量被第一次调用的时候,委托方法才会执行. lazy()是接受一个 lambda 并返回一个 Lazy <T> 实例的函数,返回的实例可以作为实现延迟属性的委托: 第一次调用 get() 会执行已传递给 lazy() 的 lambda 表达式并

  • Kotlin by lazy关键字深入探究实现原理

    目录 前言 ViewModel和ViewBinding变量初始化过程 by lazy关键字的字节码实现 by lazy关键字的Java实现 前言 kotlin的by lazy关键字是很常用的,它表示延时初始化变量,只在第一次使用时才给它初始化.那么它是如何实现这种功能的呢?这篇文章从字节码和Java语言的角度揭密它的实现原理. ViewModel和ViewBinding变量初始化过程 先举两个项目中最常见的例子:ViewModel和ViewBinding,了解一下为什么需要延时初始化. 看一段代

  • kotlin中object关键字的三种使用场景

    前言 object是Kotlin中的一个重要的关键字,也是Java中没有的.object主要有以下三种使用场景: 对象声明(Object Declaration) 伴生对象(Companion Object) 对象表达式(Object Expression) 下面就一一介绍它们所表示的含义.用法以及注意点,保证你在看完本篇之后就可以完全掌握object关键字的用法. 1. 对象声明(Object Declaration) 语法含义:将类的声明和定义该类的单例对象结合在一起(即通过object就实

  • Java同步关键字synchronize底层实现原理解析

    目录 1 字节码层实现 1.1 InterpreterRuntime::monitorenter 1.1.1 函数参数 JavaThread *thread 1.1.2 函数体 2 偏向锁 2.1 偏向锁的意义 2.2 偏向锁的获取 2.2.1 markOop mark = obj->mark() 2.2.2 判断mark是否为可偏向状态 2.2.3 判断mark中JavaThread的状态 2.2.4 通过CAS原子指令 2.2.5 如果执行CAS失败 2.3 偏向锁的撤销 2.4 轻量级锁

  • SpringBoot自动配置深入探究实现原理

    目录 一.什么是springboot自动配置 二.Starter组件 三.三大注解 四.@EnableAutoConfiguration 五.SpringFactoriesLoader 说明:在阅读本篇文章之前建议大家先详细学习一下spring的相关知识,有助于更深刻的理解spirngboot的配置原理. 一.什么是springboot自动配置 SpringBoot通过@EnableAutoConfiguration注解开启自动配置,对jar包下的spring.factories文件进行扫描,这

  • 详解Java中static关键字的使用和原理

    目录 概述 定义和使用格式 类变量 静态方法 调用格式 静态原理图解 静态代码块 概述 关于 static 关键字的使用,它可以用来修饰的成员变量和成员方法,被修饰的成员是属于类的,而不是单单是属 于某个对象的.也就是说,既然属于类,就可以不靠创建对象来调用了. 定义和使用格式 类变量 当 static 修饰成员变量时,该变量称为类变量.该类的每个对象都共享同一个类变量的值.任何对象都可以更改 该类变量的值,但也可以在不创建该类的对象的情况下对类变量进行操作. 类变量:使用 static关键字修

  • GoLang并发机制探究goroutine原理详细讲解

    目录 1. 进程与线程 2. goroutine原理 3. 并发与并行 3.1 在1个逻辑处理器上运行Go程序 3.2 goroutine的停止与重新调度 3.3 在多个逻辑处理器上运行Go程序 通常程序会被编写为一个顺序执行并完成一个独立任务的代码.如果没有特别的需求,最好总是这样写代码,因为这种类型的程序通常很容易写,也很容易维护.不过也有一些情况下,并行执行多个任务会有更大的好处.一个例子是,Web 服务需要在各自独立的套接字(socket)上同时接收多个数据请求.每个套接字请求都是独立的

  • Kotlin如何直接使用控件ID原理详析

    前言 最近断断续续地把项目的界面部分的代码由JAva改成了Kotlin编写,并且如果应用了kotlin-android-extensions插件,一个显而易见的好处是再也不用写 findViewById()来实例化你的控件对象了,直接操作你在布局文件里的id即可,这一点我感觉比butterknife做的还简洁友好. Activity import android.support.v7.app.AppCompatActivity import android.os.Bundle import ko

  • 详解vue 自定义组件使用v-model 及探究其中原理

    1.首先我们来实现自定义组件中使用v-model 父组件中注册子组件 <template> <div id="app"> <p>{{name}}</p> <MyInput v-model="name"/> </div> </template> <script> import MyInput from './components/MyInput.vue' export de

  • 聊聊Kotlin 中 lateinit 和 lazy 的原理区别

    目录 lateinit 用法 原理 lazy 用法 原理 the end references 使用 Kotlin 进行开发,对于 latelinit 和 lazy 肯定不陌生.但其原理上的区别,可能鲜少了解过,借着本篇文章普及下这方面的知识. lateinit 用法 非空类型可以使用 lateinit 关键字达到延迟初始化. class InitTest() { lateinit var name: String ​ public fun checkName(): Boolean = name

随机推荐