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

软件发布|下载排行|最新软件

当前位置:首页IT学院IT技术

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

devnn   2022-11-16 我要评论

前言

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关键字实现原理就介绍到此。

Copyright 2022 版权所有 软件发布 访问手机版

声明:所有软件和文章来自软件开发商或者作者 如有异议 请与本站联系 联系我们