前言:測試大佬突然發現頁面上有一塊UI在某些情況下無內容展示,但是接口有下發對應字段。這種偶現的bug就很頭禿了,排查一波也沒發現啥異常,就是個簡單的接口請求,然后在LiveData回調中更新UI。這樣看來問題出在LiveData了?正好本地也復現了這個問題,瘋狂刷新調用接口,UI也沒發生變化。
趕緊debug一下,發現LiveData.setValue()
此時卡在了dispatchingValue()
方法開頭處,mDispatchingValue
為true,直接return,沒有走后續的observer.onChange()
回調。mDispatchingValue
字段顧名思義,標記正在分發事件,此時繼續setValue()不會立即更新。mDispatchingValue
是個全局變量,找一下引用發現賦值處全在dispatchingValue()
方法中。
void dispatchingValue(@Nullable ObserverWrapper initiator) {
if (mDispatchingValue) {
mDispatchInvalidated = true;
return;
}
mDispatchingValue = true;
do {
mDispatchInvalidated = false;
if (initiator != null) {
considerNotify(initiator);
initiator = null;
} else {
for (Iterator<Map.Entry<Observer<? super T>, ObserverWrapper>> iterator =
mObservers.iteratorWithAdditions(); iterator.hasNext(); ) {
considerNotify(iterator.next().getValue());
if (mDispatchInvalidated) {
break;
}
}
}
} while (mDispatchInvalidated);
mDispatchingValue = false;
}
所以說正常流程下mDispatchingValue
初始值應該為false,然后在do
的入口處置為true,標記正在分發事件,分發事件后跳出循環再次置為fasle。有可能出現問題的地方就在do
中的分發事件,considerNotify()
就是我們的回調,回調中如果出現異常,執行邏輯跳到外部catch塊,mDispatchingValue
就沒法在最后重置為false,那么后續的setValue()
都會被return掉。
這里有的同學就會問了,LiveData回調中出現異常不就崩潰了嗎,崩潰了就能找到問題解決了啊,還哪有后續不分發事件的坑呢。but基礎框架大概率會把接口調用整個流程try catch住,我這邊用的協程,整個協程體都被try catch了所以不會崩潰。我這邊的case可以等同于把setValue()
方法try catch住,然后應該是某個接口字段的問題拋出異常,進而導致后續事件不再分發。
思考了一下,找到接口字段fix掉貌似是個不錯的選擇,但是吧,我們能相信后端嗎?就算和后端大佬遵守了彼此的約定,那我們能保證回調中永遠不出現異常嗎?回調中的業務代碼崩潰了其實也還好,起碼能快速定位然后解決,就怕被外部catch住了,然后一臉懵逼不走回調。
先來個兜底方案吧,在你確定回調被catch住不會崩潰進而引發這個bug的時候,不妨自己catch處理。
fun <T> MutableLiveData<T>.safeObserve(owner: LifecycleOwner, onChange: (T) -> Unit) {
this.observe(owner) {
try {
onChange(it)
} catch (e: Exception) {
e.printStackTrace()
}
}
}
思路很簡單,套一層try catch,即使回調中產生異常,dispatchingValue()
方法也會走完。
當然啦,該改的還是要改,具體是哪個字段沒下發有可能為空還是要保護一下的。我應該是被data class坑了,解析json反射給字段賦值不會走構造函數的默認值,最好都寫成可空類型。
如果你不希望發生崩潰,也不希望LiveData后續永遠不走回調,可以嘗試上面這個方案。搬磚式的把observe
都改為safeObserve
也很難受啊,ASM替換一下調用比較舒服,這個沒什么難度也就不展開了。ASM統一替換的話不要太暴力,搞個注解啥的做成可配才是王道。