Jetpack - defects of LiveData component and Countermeasures

1, Foreword

In order to solve the problem of chaotic architecture design since the development of android app, Google launched the family bucket solution of jetpack MVVM. As the core of the whole solution, LiveData, with its life cycle security, memory security and other advantages, even has the trend to gradually replace EventBus and RxJava as Android state distribution components.

The app team of the official website mall also encountered some difficulties in the deep use of LiveData, especially in the use of LiveData by observers. We summarize and share these experiences here.

2, How many callbacks can the Observer receive

2.1 why do you receive up to 2 notifications

This is a typical case. When debugging the message bus scenario, we usually print some log logs at the message receiver to facilitate us to locate the problem. However, log printing sometimes brings some confusion to our problem location. See the following example.

We first define a minimalist ViewModel:

public class TestViewModel extends ViewModel {
    private MutableLiveData<String> currentName;
    public MutableLiveData<String> getCurrentName() {
        if (currentName == null) {
            currentName = new MutableLiveData<String>();
        }
        return currentName;
    }
}

Then take a look at our activity code;

public class JavaTestLiveDataActivity extends AppCompatActivity {
    
    private TestViewModel model;
 
    private String test="12345";
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_java_test_live_data);
        model = new ViewModelProvider(this).get(TestViewModel.class);
        test3();       
        model.getCurrentName().setValue("3");
    }
    private void test3() {
 
        for (int i = 0; i < 10; i++) {
            model.getCurrentName().observe(this, new Observer<String>() {
                @Override
                public void onChanged(String s) {
                    Log.v("ttt", "s:" + s);
                }
            });
        }
    }
}

You can think about what the result of this program will be? We created a Livedata and observed the Livedata 10 times. Each time, we created a different Observer object. It seems that we bound 10 observers to a data source. When we modify this data source, we should have 10 notifications. Run to see the execution results:

2021-11-21 15:20:07.662 27500-27500/com.smart.myapplication V/ttt: s:3
2021-11-21 15:20:07.662 27500-27500/com.smart.myapplication V/ttt: s:3

Strange, why do I register 10 observers, but only receive 2 callback notifications? Try another way?

We add some content to the Log code, such as printing the hashCode and then looking at the execution results:

2021-11-21 15:22:59.377 27912-27912/com.smart.myapplication V/ttt: s:3  hashCode:217112568
2021-11-21 15:22:59.377 27912-27912/com.smart.myapplication V/ttt: s:3  hashCode:144514257
2021-11-21 15:22:59.377 27912-27912/com.smart.myapplication V/ttt: s:3  hashCode:72557366
2021-11-21 15:22:59.377 27912-27912/com.smart.myapplication V/ttt: s:3  hashCode:233087543
2021-11-21 15:22:59.377 27912-27912/com.smart.myapplication V/ttt: s:3  hashCode:22021028
2021-11-21 15:22:59.377 27912-27912/com.smart.myapplication V/ttt: s:3  hashCode:84260109
2021-11-21 15:22:59.377 27912-27912/com.smart.myapplication V/ttt: s:3  hashCode:94780610
2021-11-21 15:22:59.377 27912-27912/com.smart.myapplication V/ttt: s:3  hashCode:240593619
2021-11-21 15:22:59.377 27912-27912/com.smart.myapplication V/ttt: s:3  hashCode:207336976
2021-11-21 15:22:59.378 27912-27912/com.smart.myapplication V/ttt: s:3  hashCode:82154761

The result is normal this time. In fact, there are similar problems in the debugging of many message buses.

In fact, for the Log system, if it determines that the time stamps are consistent, the subsequent Log contents are also consistent, then it will not print the contents repeatedly. We must pay attention to this detail here, otherwise it will affect our judgment of the problem many times. Go back to the code we didn't add hashCode before. Take a closer look and you will understand: only two logs have been printed, but the notification has been received 10 times. Why are two logs printed? Because your timestamp is consistent, the subsequent content is also consistent.

2.2 strange compilation optimization

It's not over yet. See the figure below:

The above code will turn gray when running in android studio. I believe many people with code cleanliness will know why at a glance. This is not the lambda of Java 8. The ide automatically prompts us to optimize the writing method, and the mouse click will automatically optimize it, which is convenient for thieves.

The gray is gone, the code becomes concise, and the kpi is waving to me. Run it and try:

2021-11-21 15:31:50.386 29136-29136/com.smart.myapplication V/ttt: s:3

Strange, why is there only one Log this time? Is it the Log system? Let me add a timestamp:

Take another look at the execution results:

2021-11-21 15:34:33.559 29509-29509/com.smart.myapplication V/ttt: s:3 time:1637480073559

Strange, why only one log is printed? I've add ed the for loop 10 times, Observer. Is lambda the problem? Well, we can type the number of observers to see what went wrong. Look at the source code, as shown in the figure below: our observers actually exist in the map. We can know the reason by taking out the size of the map.

The reflection takes this size. Note that the LiveData we usually use is MutableLiveData, and this value is in LiveData, so it is getSuperclass().

private void hook(LiveData liveData) throws Exception {
       Field map = liveData.getClass().getSuperclass().getDeclaredField("mObservers");
       map.setAccessible(true);
       SafeIterableMap safeIterableMap = (SafeIterableMap) map.get(liveData);
       Log.v("ttt", "safeIterableMap size:" + safeIterableMap.size());
   }

Take another look at the execution results:

2021-11-21 15:40:37.010 30043-30043/com.smart.myapplication V/ttt: safeIterableMap size:1
2021-11-21 15:40:37.013 30043-30043/com.smart.myapplication V/ttt: s:3 time:1637480437013

Sure enough, the map size here is 1, not 10, so you must only receive one notice. So here's the problem. I obviously added 10 observers to the for loop. Why does my observer become one as soon as it is changed to lambda? If something goes wrong, let's decompile (directly decompile our debug app with jadx) and have a look.

private void test3() {
        for (int i = 0; i < 10; i++) {
            this.model.getCurrentName().observe(this, $$Lambda$JavaTestLiveDataActivity$zcrCJYfWItRTy4AC_xWfANwZkzE.INSTANCE);
        }
}
 
public final /* synthetic */ class $$Lambda$JavaTestLiveDataActivity$zcrCJYfWItRTy4AC_xWfANwZkzE implements Observer {
    public static final /* synthetic */ $$Lambda$JavaTestLiveDataActivity$zcrCJYfWItRTy4AC_xWfANwZkzE INSTANCE = new $$Lambda$JavaTestLiveDataActivity$zcrCJYfWItRTy4AC_xWfANwZkzE();
 
    private /* synthetic */ $$Lambda$JavaTestLiveDataActivity$zcrCJYfWItRTy4AC_xWfANwZkzE() {
    }
 
    public final void onChanged(Object obj) {
        Log.v("ttt", "s:" + ((String) obj));
    }
}

It has been clearly seen that because the Java8 lambda writing method is used here, the compiler acts wisely in the compilation process and automatically helps us optimize Chengdu. Chengdu is the same static observer, not 10, which explains why the map size is 1. We can delete the writing method of lambda and look at the decompilation results.

The last question remains. Does the lamda optimization work regardless of any scenario? Let's try another way:

private String outer = "123456";
 
private void test3() {
  for (int i = 0; i < 10; i++) {
   model.getCurrentName().observe(this, s -> Log.v("ttt", "s:" + s + outer));
  }
}

Note that although we also use lambda in this writing method, we introduce external variables, which is different from the previous writing method of lambda. Take a look at the decompilation results of this writing method;

private void test3() {
        for (int i = 0; i < 10; i++) {
            this.model.getCurrentName().observe(this, new Observer() {
                public final void onChanged(Object obj) {
                    JavaTestLiveDataActivity.this.lambda$test33$0$JavaTestLiveDataActivity((String) obj);
                }
            });
        }
}

You can rest assured when you see the new keyword. This writing method can bypass the optimization of Java 8 lambda compilation.

1.3 is there a hole in kotlin's lambda writing

Considering that most people now use the Kotlin language, let's also try to see if Kotlin's lambda writing method has the same pit as Java 8's lambda?

Look at the way lambda is written in Kotlin:

fun test2() {
      val liveData = MutableLiveData<Int>()
      for (i in 0..9) {
          liveData.observe(this,
              { t -> Log.v("ttt", "t:$t") })
      }
      liveData.value = 3
  }

Take another look at the decompilation results:

public final void test2() {
        MutableLiveData liveData = new MutableLiveData();
        int i = 0;
        do {
            int i2 = i;
            i++;
            liveData.observe(this, $$Lambda$KotlinTest$6ZY8yysFE1G_4okj2E0STUBMfmc.INSTANCE);
        } while (i <= 9);
        liveData.setValue(3);
    }
 
public final /* synthetic */ class $$Lambda$KotlinTest$6ZY8yysFE1G_4okj2E0STUBMfmc implements Observer {
    public static final /* synthetic */ $$Lambda$KotlinTest$6ZY8yysFE1G_4okj2E0STUBMfmc INSTANCE = new $$Lambda$KotlinTest$6ZY8yysFE1G_4okj2E0STUBMfmc();
 
    private /* synthetic */ $$Lambda$KotlinTest$6ZY8yysFE1G_4okj2E0STUBMfmc() {
    }
 
    public final void onChanged(Object obj) {
        KotlinTest.m1490test2$lambda3((Integer) obj);
    }
}

It seems that Kotlin's lambda compilation is as radical as Java8 lambda compilation. It helps you optimize into an object by default on the basis of the for loop. Similarly, let's take a look at allowing this lambda to access external variables to see if there is any "negative optimization".

val test="12345"
fun test2() {
    val liveData = MutableLiveData<Int>()
    for (i in 0..9) {
        liveData.observe(this,
            { t -> Log.v("ttt", "t:$t $test") })
    }
    liveData.value = 3
}

See the result of decompilation:

public final void test2() {
       MutableLiveData liveData = new MutableLiveData();
       int i = 0;
       do {
           int i2 = i;
           i++;
           liveData.observe(this, new Observer() {
               public final void onChanged(Object obj) {
                   KotlinTest.m1490test2$lambda3(KotlinTest.this, (Integer) obj);
               }
           });
       } while (i <= 9);
       liveData.setValue(3);
   }

Everything is normal. Finally, let's see if the non lambda writing method of ordinary Kotlin is the same as that of Java?

fun test1() {
       val liveData = MutableLiveData<Int>()
       for (i in 0..9) {
           liveData.observe(this, object : Observer<Int> {
               override fun onChanged(t: Int?) {
                   Log.v("ttt", "t:$t")
               }
           })
       }
       liveData.value = 3
}

See the result of decompilation:

public final void test11() {
        MutableLiveData liveData = new MutableLiveData();
        int i = 0;
        do {
            int i2 = i;
            i++;
            liveData.observe(this, new KotlinTest$test11$1());
        } while (i <= 9);
        liveData.setValue(3);
}

Everything is normal. At this point, we can draw a conclusion.

For the scenario of using lambda in the for loop, when you do not use external variables or functions in your lambda, either the Java 8 compiler or the Kotlin compiler will help you optimize to use the same lambda by default.

The starting point of the compiler is good. Of course, different objects of new in the for loop will lead to a certain degree of performance degradation (after all, everything from new will be gc in the end), but this optimization may not meet our expectations and may even cause our misjudgment in a certain scenario, so we must be careful when using it.

2, Why does LiveData receive messages before Observe

2.1 analyze the source code and find the cause

Let's take an example:

fun test1() {
        val liveData = MutableLiveData<Int>()
        Log.v("ttt","set live data value")
        liveData.value = 3
        Thread{
            Log.v("ttt","wait start")
            Thread.sleep(3000)
            runOnUiThread {
                Log.v("ttt","wait end start observe")
                liveData.observe(this,
                    { t -> Log.v("ttt", "t:$t") })
            }
        }.start()
 
}

The meaning of this code is that I first updated the value of a livedata to 3, and then 3s later I registered an observer with livedata. It should be noted here that I update the livedata value first and register the observer after a period of time. At this time, theoretically, I should not receive the livedata message. Because you sent the message first, I observed it later, but the execution result of the program is:

2021-11-21 16:27:22.306 32275-32275/com.smart.myapplication V/ttt: set live data value
2021-11-21 16:27:22.306 32275-32388/com.smart.myapplication V/ttt: wait start
2021-11-21 16:27:25.311 32275-32275/com.smart.myapplication V/ttt: wait end start observe
2021-11-21 16:27:25.313 32275-32275/com.smart.myapplication V/ttt: t:3

This is weird and does not conform to the design of a common message bus framework. Let's see what's going on with the source code?

Every time we observe, we will create a wrapper to see what the wrapper does.

Note that this wrapper has an onStateChanged method, which is the core of the whole event distribution. Let's remember this entry for the time being, and then go back to our previous observe method. The last line calls the addObserver method. Let's see what's done in this method.

Finally, the process will go to the dispatchEvent method and continue to follow.

In fact, this mlife cycleobserver is the LifecycleBoundObserver object from the new method of observe at the beginning, that is, the variable of the wrapper. After a series of calls, the onStateChanged method will eventually go to the considerNotify method shown in the following figure.

The entire considerNotify method has only one function.

It is to judge the values of mLastVersion and mVersion. If the value of mLastVersion is less than the value of mVersion, the onchaged method of observer will be triggered, that is, it will be called back to our observer method < strong = "" >.

Let's see how these two values change. First look at this mVersion;

You can see that the default value is start_version is - 1. But every time setValue, the value will be increased by 1.

The initial value of mLastVersion in observer is - 1.

In conclusion:

  • The initial value of mVersion for Livedata is - 1.
  • After a setValue, her value becomes 0.
  • An ObserverWrapper will be created each time you observe.
  • There is an mLastVersion in the Wrapper. The value is - 1. The function call of observe will eventually go through a series of processes to the considerNotify method. At this time, the mVersion of LiveData is 0.
  • 0 is obviously larger than the mLastVersion-1 of observer, so the listener function of observer will be triggered at this time.

2.2 be careful with ActivityViewModels

This feature of Livedata can lead to disastrous consequences in some scenarios. For example, in the scenario of single Activity and multiple fragments, it is very inconvenient for Activity fragments to realize data synchronization without jetpack MVVM components, but with jetpack MVVM components, it will become very easy to implement this mechanism. You can see the following examples on the official website:

class SharedViewModel : ViewModel() {
    val selected = MutableLiveData<Item>()
 
    fun select(item: Item) {
        selected.value = item
    }
}
 
class MasterFragment : Fragment() {
 
    private lateinit var itemSelector: Selector
 
   
    private val model: SharedViewModel by activityViewModels()
 
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        itemSelector.setOnClickListener { item ->
            // Update the UI
        }
    }
}
 
class DetailFragment : Fragment() {
    private val model: SharedViewModel by activityViewModels()
 
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        model.selected.observe(viewLifecycleOwner, Observer<Item> { item ->
            // Update the UI
        })
    }
}

Just let two fragments share this set of ActivityViewModel. It is very convenient to use, but it will cause some serious problems in some scenarios. In this scenario, we have an activity that displays ListFragment by default. After clicking ListFragment, we will jump to DetailFragment and look at the code below:

class ListViewModel : ViewModel() {
    private val _navigateToDetails = MutableLiveData<Boolean>()
 
    val navigateToDetails : LiveData<Boolean>
        get() = _navigateToDetails
 
    fun userClicksOnButton() {
        _navigateToDetails.value = true
    }
}

Take another look at the core ListFragment;

class ListFragment : Fragment() {
     
    private val model: ListViewModel by activityViewModels()
 
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
         
    }
 
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        model.navigateToDetails.observe(viewLifecycleOwner, { t ->
            if (t) {
                parentFragmentManager.commit {
                    replace<DetailFragment>(R.id.fragment_container_view)
                    addToBackStack("name")
                }
            }
        })
    }
 
    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        // Inflate the layout for this fragment
        return inflater.inflate(R.layout.fragment_list, container, false).apply {
            findViewById<View>(R.id.to_detail).setOnClickListener {
                model.userClicksOnButton()
            }
        }
    }
}

It can be seen that our implementation mechanism is that after clicking the button, we call the userClicksOnButton method of viewModel to change the livedata value of navigateToDetails to true, and then listen to the livedata value. If it is true, we will jump to the fragment of Detail.

At first glance, this process is OK. After clicking it, we can jump to the DetailFragment. However, when we click the return button on the DetailFragment page, we will theoretically return to the ListFragment, but the actual execution result is that we will jump to the DetailFragment immediately after returning to the ListFragment.

Why is this? The problem actually occurs in the Fragment life cycle. After you press the return key, the onViewCreated of ListFragment will be executed again. Then this time, you observe. The value before Livedata is true, so the process of jumping to DetailFragment will be triggered. As a result, your page can no longer return to the list page.

2.3 solution 1: introducing the middle tier

As the saying goes, all problems in the computer field can be solved by introducing an intermediate layer. Here, too, we can try the idea of "a message is consumed only once" to solve the above problems. For example, we wrap the value of LiveData into one layer:

class ListViewModel : ViewModel() {
    private val _navigateToDetails = MutableLiveData<Event<Boolean>>()
 
    val navigateToDetails : LiveData<Event<Boolean>>
        get() = _navigateToDetails
 
 
    fun userClicksOnButton() {
        _navigateToDetails.value = Event(true)
    }
}
 
 
open class Event<out T>(private val content: T) {
 
    var hasBeenHandled = false
        private set // Only external reading is allowed, and external writing is not allowed
 
    /**
     * The value obtained through this function can only be consumed once
     */
    fun getContentIfNotHandled(): T? {
        return if (hasBeenHandled) {
            null
        } else {
            hasBeenHandled = true
            content
        }
    }
 
    /**
     * If you want to consume the previous value, you can call this method directly
     */
    fun peekContent(): T = content
}

In this way, when listening, we just call getContentIfNotHandled():

model.navigateToDetails.observe(viewLifecycleOwner, { t ->
           t.getContentIfNotHandled()?.let {
               if (it){
                   parentFragmentManager.commit {
                       replace<DetailFragment>(R.id.fragment_container_view)
                       addToBackStack("name")
                   }
               }
           }
       })

2.4 solution 2: observe method of Hook LiveData

As we analyzed earlier, every time we observe, the value of mLastVersion is less than the value of mVersion, which is the root cause of the problem. Then we use reflection to set the value of mLastVersion equal to version every time we observe.

class SmartLiveData<T> : MutableLiveData<T>() {
    override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
        super.observe(owner, observer)
        //get livedata version
        val livedataVersion = javaClass.superclass.superclass.getDeclaredField("mVersion")
        livedataVersion.isAccessible = true
        // Gets the value of livedata version
        val livedataVerionValue = livedataVersion.get(this)
        // Get mObservers Filed
        val mObserversFiled = javaClass.superclass.superclass.getDeclaredField("mObservers")
        mObserversFiled.isAccessible = true
        // Get the mObservers object
        val objectObservers = mObserversFiled.get(this)
        // Get the class SafeIterableMap to which the mObservers object belongs
        val objectObserversClass = objectObservers.javaClass
        val methodGet = objectObserversClass.getDeclaredMethod("get", Any::class.java)
        methodGet.isAccessible = true
        //LifecycleBoundObserver
        val objectWrapper = (methodGet.invoke(objectObservers, observer) as Map.Entry<*, *>).value
        //ObserverWrapper
        val mLastVersionField = objectWrapper!!.javaClass.superclass.getDeclaredField("mLastVersion")
        mLastVersionField.isAccessible = true
        //Assign the value of mVersion to mLastVersion to make it equal
        mLastVersionField.set(objectWrapper, livedataVerionValue)
 
    }
}

2.5 solution 3: use kotlin flow

If you are still using Kotlin, the solution to this problem is simpler and even the process becomes controllable. At this year's Google I/O conference, Yigit clearly pointed out in Jetpack's AMA that Livedata exists to take care of Java users and will continue to be maintained in the short term (what does it mean? Everyone's own products). As a substitute for Livedata, Flow will gradually become the mainstream in the future (after all, Kotlin will gradually become the mainstream now). If Flow is used, The above situation can be easily solved.

Overwrite viewModel

class ListViewModel : ViewModel() {
    val _navigateToDetails = MutableSharedFlow<Boolean>()
    fun userClicksOnButton() {
        viewModelScope.launch {
            _navigateToDetails.emit(true)
        }
    }
}

Then rewrite the monitoring method;

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        lifecycleScope.launch {
            model._navigateToDetails.collect {
                if (it) {
                    parentFragmentManager.commit {
                        replace<DetailFragment>(R.id.fragment_container_view)
                        addToBackStack("name")
                    }
                }
            }
        }
    }

We focus on SharedFlow, the constructor of heat flow;

Its actual function is: when a new subscriber collects (it can be understood that collect is observe in Livedata), it sends several (replay) data that has been sent before collecting to it. The default value is 0. Therefore, the above code will not receive the previous message. Here, you can try changing the replay to 1 to reproduce the previous Livedata problem. Compared with the previous two solutions, this solution is better. The only disadvantage is that Flow does not support Java and only supports Kotlin.

3, Summary

On the whole, even with Kotlin Flow, LiveData is still an indispensable part of the current Android client architecture components. After all, its life cycle security and memory security are too fragrant, which can effectively reduce the burden of our ordinary business development. When using it, we can avoid the pit by focusing on three aspects:

  • Carefully use the lambda smart tips given by Android Studio
  • Pay more attention to whether you really need to Observe the messages before registering to listen
  • Be careful when using ActivityViewModel between Activity and Fragment.

Author: vivo Internet front end team - Wu Yue

Keywords: Java Android jetpack

Added by Sakujou on Tue, 18 Jan 2022 19:35:11 +0200