SoFunction
Updated on 2025-04-10

Kotlin by lazy keyword in-depth exploration of implementation principles

Preface

kotlin's by lazy keyword is very commonly used. It means delay initialization variable and is only initialized when it is used for the first time. So how does it implement this function? This article reveals its implementation principles from the perspective of bytecode and Java language.

ViewModel and ViewBinding variable initialization process

Let’s first give two most common examples in projects: ViewModel and ViewBinding to understand why delayed initialization is required.

Look at a piece of code:

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

The use of ViewModel and ViewBinding in the Jetpack library is very common. Variables of ViewModel and ViewBinding type need to be initialized delay and cannot be initialized at declaration time. ViewModel is because it needs to rely on the member variable mApplication of the Activity internally, and mApplication is assigned to the value when attached. The initialization of ViewBinding requires Window's layoutInflater variable, and the Window variable is also assigned when attached.

First, see how ViewModel is initialized, as followsThe method will be calledcheckApplicationTo determine whether the application is empty, if it is empty, an exception is thrown:

public class ViewModelProviders {
    /**
     * @deprecated This class should not be directly instantiated
     */
    @Deprecated
    public ViewModelProviders() {
    }
    private static Application checkApplication(Activity activity) {
        Application application = ();
        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 = (application);
        }
        return new ViewModelProvider((), factory);
    }

mApplication is a member variable of the Activity, which is assigned when attached:

 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;
        ...
   }

The layoutInflater variable is the same as that, it needs to be obtained through the mWindow variable, and mWindow is also assigned in the attach:

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

The Activity attach method was executed earlier than the onCreate method, so these two variables can be accessed in the onCreate method.

Therefore, both ViewModel and ViewBinding type variables need to be initialized delayed.

Let’s get to the topic, how to achieve delay initialization by lazy keyword.

by bytecode implementation of lazy keyword

Check the bytecode content of MainActivity above as follows:

public final class com/devnn/demo/MainActivity extends androidx/appcompat/app/AppCompatActivity {
  ...Omit irrelevant byte code
  // 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/ (Lkotlin/jvm/functions/Function0;)Lkotlin/Lazy;
    PUTFIELD com/devnn/demo/$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/ (Lkotlin/jvm/functions/Function0;)Lkotlin/Lazy;
    PUTFIELD com/devnn/demo/$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/$delegate : Lkotlin/Lazy;
    ASTORE 1
    ALOAD 1
    INVOKEINTERFACE kotlin/ ()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/$delegate : Lkotlin/Lazy;
    ASTORE 1
    ALOAD 1
    INVOKEINTERFACE kotlin/ ()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

Observe the bytecode to find several changes:

(1) The type of viewModel variable has been replacedType, variable name is also replaced with viewModel$delegate. I also know that the idea of ​​commission is used when looking at the name.

(2) Use LazyKt's static method lazy in the init method of MainActivity, i.e. the constructor method.viewModel$delegateThe variable has been assigned. by lazy, the initialization implementation logic is encapsulated in the Function0 type variableMainActivity$viewModel$2middle.

INVOKESTATIC kotlin/ (Lkotlin/jvm/functions/Function0;)Lkotlin/Lazy

Note: The parameter type of this static method is Function0, which represents a callback with zero parameters (i.e. no parameters):

package 
public interface Function0<out R> : <R> {
    public abstract operator fun invoke(): R
}

(3) Generate a get method for MainActivity:getViewModel(), the return type of this method is exactly the type we need:com/devnn/demo/MainViewModel

This can be seen through the bytecodegetViewModel()The internal implementation of the method:

CalledviewModel$delegate(type is) variable getValue() method returns an Object and force it tocom/devnn/demo/MainViewModelReturn it again.

The secret lies in this Lazy's getValue method.

Then continue to look at the implementation of 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()) () else "Lazy value not initialized yet."
    private fun writeReplace(): Any = InitializedLazyImpl(value)
}

You can see that when the value isUNINITIALIZED_VALUEThat is, when it is not initialized, it is initialized by entering the parameter initializer (i.e. Function0), assigning a value to the value, and then returning this value.

Here is a bit similar to the lazy mode of singleton mode in Java.

By this time the analysis has been completedby lazyThe principle of bytecode is roughly the process of replacing the variable type with the Lazy type, and then returning the real type through the getValue method of the Lazy class.getValueIn the method, determine whether it is the first visit by deciding the empty.

The key is to delegate variable initialization to the general type Lazy class through the idea of ​​delegating.

ViewBinding delay initialization is the same as ViewModel, so I will not analyze it anymore.

by Java implementation of lazy keyword

The code of kotlin can be converted into Java code. Let's check its Java code to verify whether it is the same as the one analyzed above:

public final class MainActivity extends AppCompatActivity {
   @NotNull
   private final Lazy viewModel$delegate = ((Function0)(new Function0() {
      @NotNull
      public final MainViewModel invoke() {
         ViewModel var1 = ((FragmentActivity)).get();
         (var1, "of(this).get(MainViewModel::)");
         return (MainViewModel)var1;
      }
      // $FF: synthetic method
      // $FF: bridge method
      public Object invoke() {
         return ();
      }
   }));
   @NotNull
   private final Lazy binding$delegate = ((Function0)(new Function0() {
      @NotNull
      public final ActivityMainBinding invoke() {
         ActivityMainBinding var1 = (());
         (var1, "inflate(layoutInflater)");
         return var1;
      }
      // $FF: synthetic method
      // $FF: bridge method
      public Object invoke() {
         return ();
      }
   }));
   private final MainViewModel getViewModel() {
      Lazy var1 = $delegate;
      return (MainViewModel)();
   }
   private final ActivityMainBinding getBinding() {
      Lazy var1 = $delegate;
      return (ActivityMainBinding)();
   }

As you can see, it is exactly the same as the above analysis, it just decompiles the bytecode into Java code.

Java member variable initialization is done in the constructor method (init method). If you are interested, you can check out another article of mine:Kotlin bytecode layer explores the execution order of constructors, member variables and init code blocks

This is all about Kotlin's by lazy keyword implementation principle.

This is the end of this article about in-depth exploration of the implementation principles of Kotlin by lazy keyword. For more related Kotlin by lazy content, please search for my previous articles or continue browsing the related articles below. I hope everyone will support me in the future!