关于java:JetpackLiveData组件的缺陷以及应对策略

6次阅读

共计 14062 个字符,预计需要花费 36 分钟才能阅读完成。

一、前言

为了解决 Android-App 开发以来始终存在的架构设计凌乱的问题,谷歌推出了 Jetpack-MVVM 的全家桶解决方案。作为整个解决方案的外围 -LiveData,以其生命周期平安,内存平安等长处,甚至有逐渐取代 EventBus,RxJava 作为 Android 端状态散发组件的趋势。

官网商城 app 团队在深度应用 LiveData 的过程中,也遇到了一些艰难,尤其是在 LiveData 的观察者应用上踩到了不少坑,咱们把这些教训在这里做一次总结与分享。

二、Observer 到底能够接管多少次回调

2.1 为什么最多收到 2 个告诉

这是一个典型的案例,在调试音讯总线的场景时,咱们通常会在音讯的接收者那里打印一些 log 日志不便咱们定位问题,然而日志的打印有时候也会给咱们的问题定位带来肯定的迷惑性,能够看上面的例子。

咱们首先定义一个极简的 ViewModel:

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

而后看下咱们的 activity 代码;

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

大家能够想一下,这段程序运行的后果会是多少?咱们创立了一个 Livedata,而后对这个 Livedata Observe 了 10 次,每次都是 new 出不同的 Observer 对象,看上去咱们对一个数据源做了 10 个观察者的绑定。当咱们批改这个数据源的时候,咱们理当有 10 条告诉。运行一下看看执行后果:

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

奇怪,为什么我明明注册了 10 个观察者,然而只收到了 2 个回调告诉?换种写法试试?

咱们在 Log 的代码里减少一部分内容比方打印下 hashCode 再看下执行后果:

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

这次后果就失常了, 其实对于很多音讯总线的调试都有相似的问题。

实际上对于 Log 零碎来说,如果他断定工夫戳统一的状况下,前面的 Log 内容也统一,那么他就不会反复打印内容了。这里肯定要留神这个细节,否则在很多时候,会影响咱们对问题的判断。再回到咱们之前没有增加 hashCode 的代码,再认真看看也就明确了:只是 Log 打印了两条而已,然而告诉是收到了 10 次的,为啥打印两条?因为你的工夫戳统一,后续的内容也统一。

2.2 奇怪的编译优化

事件到这还没完结,看下图:

上述的代码跑在 android studio 外面会变灰,置信很多有代码洁癖的人一看就晓得为啥,这不就是 Java8 的 lambda 嘛,ide 主动给提醒给咱们让咱们优化一下写法呗,而且鼠标一点就主动优化了,贼不便。

灰色没有了,代码变的简洁了,kpi 在向我招手了,运行一下试试:

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

奇怪,为啥这次只有一个日志了?难道还是 Log 日志零碎的起因?那我加个工夫戳试试:

再看下执行后果:

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

奇怪,为什么还是只打印了一条 log?我这里 for 循环 add 了 10 次观察者呀。难道是 lambda 导致的问题?嗯,咱们能够把 Observer 的数量打进去看看,看看到底是哪里出了问题。看下源码,如下图所示:咱们的观察者实际上都是存在这个 map 外面的,咱们取出来这个 map 的 size 就能够晓得起因了。

反射取一下这个 size,留神咱们平时应用的 LiveData 是 MutableLiveData,而这个值是在 LiveData 里,所以是 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());
   }

再看下执行后果:

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

果然这里的 map size 是 1,并不是 10,那必定只能收到 1 条告诉了。那么问题来了,我明明是 for 循环增加了 10 个观察者啊,为啥一改成 lambda 的写法,我的观察者就变成 1 个了?遇事不决咱们反编译 (用 jadx 间接反编译咱们的 debug app) 一下看看。

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

曾经很清晰的看进去,这里因为应用了 Java8 lambda 的写法,所以编译器在编译的过程中自作聪明了一下,主动帮咱们优化成都是增加的同一个动态的观察者,并不是 10 个,这就解释了为什么会呈现 map size 为 1 的状况了。咱们能够再把 lambda 的写法删除掉,再看看反编译的后果就失常了。

还剩最初一个问题,这个 lamda 的优化是不分任何场景始终失效的嘛?咱们换个写法试试:

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

留神看,咱们这种写法尽管也是用了 lambda,然而咱们引入了内部变量,和之前的 lambda 的写法是不一样的,看下这种写法反编译的后果;

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

看到 new 关键字就释怀了,这种写法就能够绕过 Java8 lambda 编译的优化了。

1.3 Kotlin 的 lambda 写法会有坑吗

思考到当初大多数人都会应用 Kotlin 语言,咱们也试试看 Kotlin 的 lamda 写法会不会也和 Java8 的 lambda 一样会有这种坑?

看下 Kotlin 中 lambda 的写法:

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

再看下反编译的后果:

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

看来 Kotlin 的 lambda 编译和 Java8 lambda 的编译是一样激进的,都是在 for 循环的根底上 默认帮你优化成一个对象了。同样的,咱们也看看让这个 lambda 拜访内部的变量,看看还有没有这个“负优化”了。

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
}

看下反编译的后果:

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

一切正常了。最初咱们再看看 一般 Kotlin 的非 lambda 写法 是不是和 Java 的非 lambda 写法一样呢?

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
}

看下反编译的后果:

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

一切正常,到这里咱们就能够下一个论断了。

对于 for 循环两头应用 lambda 的场景,当你的 lambda 中没有应用内部的变量或者函数的时候,那么不论是 Java8 的编译器还是 Kotlin 的编译器都会默认帮你优化成应用同一个 lambda。

编译器的出发点是好的,for 循环中 new 不同的对象,当然会导致肯定水平的性能降落(毕竟 new 进去的货色最初都是要 gc 的),但这种优化往往可能不合乎咱们的预期,甚至有可能在某种场景下造成咱们的误判,所以应用的时候肯定要小心。

二、LiveData 为何会收到 Observe 之前的音讯

2.1 剖析源码找起因

咱们来看一个例子:

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()}

这段代码的意思是我先更新了一个 livedata 的值为 3,而后 3s 之后我 livedata 注册了一个观察者。这里要留神了,我是先更新的 livedata 的值,过了一段时间当前才注册的观察者,那么此时,实践上我应该是收不到 livedata 音讯的。因为你是先发的音讯,我前面才察看的,但程序的执行后果却是:

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

这个就很诡异了,而且不合乎一个咱们常见的音讯总线框架的设计。来看看源码到底是咋回事?

每次 observe 的时候咱们会创立一个 wrapper,看下这个 wrapper 是干啥的。

留神这个 wrapper 有一个 onStateChanged 办法,这是整个事件散发的外围,咱们暂且记住这个入口,再回到咱们之前的 observe 办法,最初一行是调用了 addObserver 办法,咱们看看这个办法里做了啥。

最终流程会走到这个 dispatchEvent 办法里,持续跟。

这个 mLifeCycleObserver 其实就是咱们一开始 observe 那个办法里 new 进去的 LifecycleBoundObserver 对象了,也就是那个 wrapper 的变量。这个 onStateChanged 办法通过一系列的调用最终会走到如下图所示的 considerNotify 办法。

而整个 considerNotify 办法的作用只有一个。

就是判断 mLastVersion 和 mVersion 的值,如果 mLastVersion 的值 <mversion 的值,那么就会触发 observer 的 onchaged 办法了,也就是会回调到咱们的观察者办法外面 <strong=””>。

咱们来看看这 2 个值咋变动的。首先看这个 mVersion;

能够看进去这个值默认值就是 start_version 也就是 -1。然而每次 setValue 的时候这个值都会加 1。

而咱们 observer 外面的 mLastVersion 它的初始值就是 -1。

最初总结一下:

  • Livedata 的 mVersion 初始值是 -1。
  • 通过一次 setValue 当前她的值就变成了 0。
  • 后续每次 observe 的时候会创立一个 ObserverWrapper。
  • Wrapper 她外面有一个 mLastVersion 这个值是 -1,observe 的函数调用最终会通过一系列的流程走到 considerNotify 办法中此时 LiveData 的 mVersion 是 0。
  • 0 显然是大于 observer 的 mLastVersion- 1 的,所以此时就肯定会触发 observer 的监听函数了。

2.2 配合 ActivityViewModels 要小心

Livedata 的这种个性,在某些场景下会引发灾难性的结果,比如说,单 Activity 多 Fragment 的场景下,在没有 Jetpack-mvvm 组件之前,要让 Activity-Fragment 实现数据同步是很不不便的,然而有了 Jetpack-mvvm 组件之后,要实现这套机制会变的非常容易。能够看下官网上的例子:

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
        })
    }
}

只有让 2 个 fragment 之间共享这套 ActivityViewModel 即可。应用起来很不便,然而某些场景下却会导致一些重大问题。来看这个场景,咱们有一个 activity 默认显 ListFragment,点击了 ListFragment 当前咱们会跳转到 DetailFragment,来看下代码:

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

再看下外围的 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()
            }
        }
    }
}

能够看进去咱们的实现机制就是点击了按钮当前咱们调用 viewModel 的 userClicksOnButton 办法将 navigateToDetails 这个 livedata 的值改成 true,而后监听这个 LiveData 值,如果是 true 的话就跳转到 Detail 这个详情的 fragment。

这个流程初看是没问题的,点击当前的确能跳转到 DetailFragment,然而当咱们在 DetailFragment 页面点击了返回键当前,实践上会回到 ListFragment,但理论的执行后果是回到 ListFragment 当前马上又跳到 DetailFragment 了。

这是为啥?问题其实就呈现在 Fragment 生命周期这里,当你按了返回键当前,ListFragment 的 onViewCreated 又一次会被执行,而后这次你 observe 了,Livedata 之前的值是 true,于是又会触发跳转到 DetailFragment 的流程。导致你的页面再也回不到列表页了。

2.3 解决方案一:引入中间层

俗话说的好,计算机领域中的所有问题都能够通过引入一个中间层来解决。这里也一样,咱们能够尝试“一个音讯只被生产一次”的思路来解决上述的问题。例如咱们将 LiveData 的值包一层:

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}

这样咱们在做监听的时候只有调用 getContentIfNotHandled()这个办法即可:

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

2.4 解决方案二:Hook LiveData 的 observe 办法

前文咱们剖析过,每次 observe 的时候,mLastVersion 的值小于 mVersion 的值 是问题产生的本源,那咱们利用反射,每次 observer 的时候将 mLastVersion 的值设置成与 version 相等不就行了么。

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 解决方案三:应用 Kotlin-Flow

如果你还在应用 Kotlin,那么此问题的解决方案则更加简略,甚至连过程都变的可控。在往年的谷歌 I / O 大会中,Yigit 在 Jetpack 的 AMA 中明确指出了 Livedata 的存在就是为了关照 Java 的使用者,短期内会持续保护(含意是什么大家本人品品),作为 Livedata 的替代品 Flow 会在今后慢慢成为支流(毕竟当初 Kotlin 慢慢成为支流),那如果应用了 Flow,上述的状况则能够迎刃而解。

改写 viewModel

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

而后改写下监听的形式即可;

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")
                    }
                }
            }
        }
    }

咱们重点看 SharedFlow 这个热流的构造函数;

他的理论作用就是:当有新的订阅者 collect 的时候(能够了解为 collect 就是 Livedata 中的 observe),发送几个(replay)collect 之前曾经发送过的数据给它, 默认值是 0。所以咱们上述的代码是不会收到之前的音讯的。大家在这里能够试一下 把这个 replay 改成 1,即可复现之前 Livedata 的问题。相比于后面两种解决方案,这个计划更加优良,惟一的毛病就是 Flow 不反对 Java,仅反对 Kotlin。

三、总结

整体上来说,即便当初有了 Kotlin Flow,LiveData 也仍旧是目前 Android 客户端架构组件中不可短少的一环,毕竟它的生命周期平安和内存平安切实是太香,能够无效升高咱们平时业务开发中的累赘,在应用他的时候咱们只有关注 3 个方面即可避坑:

  • 审慎应用 Android Studio 给出的 lambda 智能提醒
  • 多关注是否真的须要 Observe 在注册监听之前的音讯
  • Activity 与 Fragment 之间应用 ActivityViewModel 时要小心解决。

作者:vivo 互联网前端团队 -Wu Yue

正文完
 0