I. Introduction
In order to solve the problem of architectural design confusion that has existed since the development of Android-App, Google has launched the whole family bucket solution of Jetpack-MVVM. As the core of the whole solution - LiveData, with its advantages of life cycle safety and memory safety, it even has the trend of gradually replacing EventBus and RxJava as the Android-side state distribution component.
The official website mall app team also encountered some difficulties in the process of using LiveData in depth, especially in the use of LiveData observers who stepped on a lot of pitfalls. We will summarize and share these experiences here.
2. How many callbacks can the Observer receive?
2.1 Why receive at most 2 notifications
This is a typical case. When debugging the scene of the message bus, we usually print some logs on the receiver of the message to help us locate the problem. However, the printing of the log will sometimes bring some problems to our problem location. Confusing, see the example below.
We start by defining 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 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 it, what will be the result of running this program? We created a Livedata, and then observed the Livedata 10 times, each time a different Observer object was created. It seems that we have bound 10 observers to a data source. When we modify this data source, we should have 10 notifications. Run it to see the execution result:
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 I registered 10 observers, but only received 2 callback notifications? Try another way of writing?
We add some content to the Log code, such as printing the hashCode and then looking at the execution result:
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
This time the result is normal. In fact, there are similar problems for debugging of many message buses.
In fact, for the Log system, if he determines that the timestamps are consistent, the content of the subsequent Log is also consistent, then he will not print the content repeatedly. We must pay attention to this detail here, otherwise, in many cases, it will affect our judgment on the problem. Going back to the code that we did not add hashCode before, we will understand it after a closer look: it’s just that the Log has printed two pieces, but the notification has been received 10 times, why print two pieces? Because your timestamps are the same, the subsequent content is also the same.
2.2 Strange compilation optimizations
It's not over yet, look at the picture below:
The above code will be grayed out when running in android studio. I believe that many people with code cleanliness will know why at a glance. Isn't this the lambda of Java8, ide automatically gives us a prompt to let us optimize the writing method, and click the mouse It is automatically optimized, which is very convenient.
The gray is gone, the code has become more concise, kpi is beckoning 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 because of the Log log system? Then I add a timestamp and try:
Take a look at the execution result:
2021-11-21 15:34:33.559 29509-29509/com.smart.myapplication V/ttt: s:3 time:1637480073559
Strange, why is only one log printed? I have added 10 observers to the for loop here. Could it be the problem caused by the lambda? Well, we can type out the number of Observers and see what went wrong. Take a look at the source code, as shown in the following figure: Our observers actually exist in this map, and we can know the reason by taking out the size of this map.
Take this size by reflection. 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 a look at the execution result:
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 there must be only 1 notification. So the question is, I obviously added 10 observers to the for loop. Why does my observer become 1 when I change it to lambda? Let's decompile (decompile our debug app directly with jadx) and take a look when we are in doubt.
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 is used here, the compiler is smart in the process of compiling, and automatically helps us optimize the same static observers added in Chengdu, not 10, which is Explains why the map size is 1. We can delete the writing of lambda again, and then look at the result of decompilation and it will be normal.
There is one last question left. Does this lamda optimization take effect regardless of any scenario? Let's try writing it differently:
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 way of writing, we have introduced external variables, which is different from the previous way of writing lambda. Let's see the result of decompilation of this way of writing;
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 way of writing can bypass the optimization of Java8 lambda compilation.
1.3 Will there be any pits in Kotlin's lambda writing?
Considering that most people use the Kotlin language now, let's also try to see if Kotlin's lamda writing method will have the same pit as Java8's lambda?
Take a look at how 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 a look at the decompiled result:
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's lambda compilation, and it optimizes it into an object by default based on the for loop. Similarly, let's look at letting this lambda access external variables to see if there is still this "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
}
Take a look at the decompiled result:
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 works fine. Finally, let's see if ordinary Kotlin's non-lambda writing is the same as Java's non-lambda writing?
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
}
Take a look at the decompiled result:
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, here we can draw a conclusion.
For the scenario of using lambda in the middle of the for loop, when your lambda does not use external variables or functions, then both the Java8 compiler and the Kotlin compiler will optimize you to use the same lambda by default.
The starting point of the compiler is a good one. Of course, new objects in the for loop will lead to a certain degree of performance degradation (after all, the things that come out of new need to be gc in the end), but this optimization may often not meet our expectations. It may even cause our misjudgment in certain scenarios, so be careful when using it.
2. Why does LiveData receive previous messages from Observe?
2.1 Analyze the source code to find the reason
Let's look at 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 update the value of a livedata to 3, and then after 3s, my livedata registers an observer. It should be noted here that I am an observer who updated the value of livedata first and registered after a period of time. At this time, in theory, 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 very weird, and it 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 and see what the wrapper does.
Note that this wrapper has an onStateChanged method, which is the core of the entire event distribution. Let's remember this entry for now, and then go back to our previous observe method. The last line is to call the addObserver method. Let's see what is done in this method.
The final process will go to this dispatchEvent method and continue to follow.
This mLifeCycleObserver is actually the LifecycleBoundObserver object that was new in the observe method at the beginning, which is the variable of the wrapper. This onStateChanged method will eventually go to the considerNotify method as shown in the following figure after a series of calls.
And the whole considerNotify method has only one function.
It is to judge the values of mLastVersion and mVersion. If the value of mLastVersion < the value of mversion, then the onchaged method of the 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;
It can be seen that the default value of this value is start_version, which is -1. But this value will be incremented by 1 each time setValue.
And the initial value of mLastVersion in our observer is -1.
Finally to sum up:
- The initial value of mVersion for Livedata is -1.
- After a setValue, her value becomes 0.
- An ObserverWrapper is created every subsequent observe.
- Wrapper has a mLastVersion in it. 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 mVVersion of LiveData is 0.
- 0 is obviously greater than the observer's mLastVersion-1, so the observer's listener function will be triggered at this time.
2.2 Be careful with ActivityViewModels
This feature of Livedata can lead to catastrophic consequences in some scenarios. For example, in the scenario of a single Activity with multiple Fragments, it is very inconvenient to let Activity-Fragment achieve data synchronization before the Jetpack-mvvm component. , but with the Jetpack-mvvm component, it will be very easy to implement this mechanism. Take a look at the example 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 this set of ActivityViewModel be shared between 2 fragments. It is very convenient to use, but it can cause some serious problems in some scenarios. Looking at this scene, we have an activity that displays ListFragment by default. After clicking ListFragment, we will jump to DetailFragment and look at the code:
class ListViewModel : ViewModel() {
private val _navigateToDetails = MutableLiveData<Boolean>()
val navigateToDetails : LiveData<Boolean>
get() = _navigateToDetails
fun userClicksOnButton() {
_navigateToDetails.value = true
}
}
Look at the core ListFragment again;
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 the button is clicked, we call the userClicksOnButton method of the viewModel to change the value of the livedata of navigateToDetails to true, and then monitor the value of the LiveData. If it is true, jump to the fragment of the detail of Detail.
At first glance, this process is no problem. After clicking, it can jump to DetailFragment, but when we click the return button on the DetailFragment page, in theory, it will return to ListFragment, but the actual execution result is to jump back to ListFragment immediately. to DetailFragment.
Why is this? The problem actually occurs here in the Fragment life cycle. When you press the return key, the onViewCreated of ListFragment will be executed again, and then this time you observe, the previous value of Livedata is true, so it will trigger the jump to DetailFragment again. Process. As a result, your page never returns to the list page.
2.3 Solution 1: Introduce the middle layer
As the saying goes, all problems in computing can be solved by introducing an intermediate layer. Here too, we can try the idea of "a message is only consumed once" to solve the above problem. For example, we wrap the value of LiveData in 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 // 只允许外部读 不允许外部写这个值
/**
* 通过这个函数取的value 只能被消费一次
*/
fun getContentIfNotHandled(): T? {
return if (hasBeenHandled) {
null
} else {
hasBeenHandled = true
content
}
}
/**
* 如果想消费之前的value 那就直接调用这个方法即可
*/
fun peekContent(): T = content
}
In this way, we only need to call the method getContentIfNotHandled() when we are listening:
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: The observe method of Hook LiveData
As we have analyzed earlier, each 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 to be equal to version every time we observe, right?
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
// 获取livedata version的值
val livedataVerionValue = livedataVersion.get(this)
// 取 mObservers Filed
val mObserversFiled = javaClass.superclass.superclass.getDeclaredField("mObservers")
mObserversFiled.isAccessible = true
// 取 mObservers 对象
val objectObservers = mObserversFiled.get(this)
// 取 mObservers 对象 所属的class SafeIterableMap
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
//将 mVersion的值 赋值给 mLastVersion 使其相等
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. In 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 maintain it in the short term (meaning what everyone's own products), as a replacement for Livedata Flow will gradually become mainstream in the future (after all, Kotlin is gradually becoming mainstream), so if Flow is used, the above situation can be solved easily.
Override viewModel
class ListViewModel : ViewModel() {
val _navigateToDetails = MutableSharedFlow<Boolean>()
fun userClicksOnButton() {
viewModelScope.launch {
_navigateToDetails.emit(true)
}
}
}
Then rewrite the way to monitor;
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 the constructor of SharedFlow, the heat flow;
Its actual function is: when there is a new subscriber collect (it can be understood that collect is the observe in Livedata), send several (replay) data that collect has sent to it before, the default value is 0. So our above code will not receive the previous message. You can try changing this replay to 1 here 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, only Kotlin.
3. Summary
On the whole, even with Kotlin Flow now, LiveData is still an indispensable part of the current Android client architecture components. After all, its life cycle safety and memory safety are very fragrant, which can effectively reduce our normal business development. When using it, we only need to pay attention to 3 aspects to avoid pitfalls:
- Use the lambda intellisense given by Android Studio with caution
- Pay more attention to whether you really need to observe the message before registering to listen
- Be careful when using ActivityViewModel between Activity and Fragment.
Author: vivo Internet front-end team - Wu Yue
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。