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 called
checkApplication
To 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$delegate
The variable has been assigned. by lazy, the initialization implementation logic is encapsulated in the Function0 type variableMainActivity$viewModel$2
middle.
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/MainViewModel
Return 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_VALUE
That 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 lazy
The 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.getValue
In 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!