天天看點

LiveData 解答、源碼分析

引子

LiveData 是能感覺生命周期的,可觀察的,粘性的,資料持有者。LiveData 用于以“資料驅動”方式更新界面。

換一種描述方式:LiveData 緩存了最新的資料并将其傳遞給正活躍的元件。

關于資料驅動的詳解可以點選我是怎麼把業務代碼越寫越複雜的 | MVP - MVVM - Clean Architecture。

這一篇就 LiveData 的面試題做一個歸總、分析、解答。

1. LiveData 如何感覺生命周期的變化?

先總結,再分析:

  • Jetpack 引入了 Lifecycle,讓任何元件都能友善地感覺界面生命周期的變化。隻需實作 LifecycleEventObserver 接口并注冊給生命周期對象即可。
  • LiveData 的資料觀察者在内部被包裝成另一個對象(實作了 LifecycleEventObserver 接口),它同時具備了資料觀察能力和生命周期觀察能力。

正常的觀察者模式中,隻要被觀察者發生變化,就會無條件地通知所有觀察者。比如

java.util.Observable

public class Observable {
    private boolean changed = false;
    private Vector<Observer> obs;
    public void notifyObservers(Object arg) {
        Object[] arrLocal;
        synchronized (this) {
            if (!hasChanged())
                return;
            arrLocal = obs.toArray();
            clearChanged();
        }
        // 無條件地周遊所有觀察者并通知
        for (int i = arrLocal.length-1; i>=0; i--)
            ((Observer)arrLocal[i]).update(this, arg);
    }
}
// 觀察者
public interface Observer {
    void update(Observable o, Object arg);
}
           

LiveData 在正常的觀察者模式上附加了條件,若生命周期未達标,即使資料發生變化也不通知觀察者。這是如何實作的?

生命周期

生命周期是一個對象從建構到消亡過程中的各個狀态的統稱。

比如 Activity 的生命周期用如下函數依次表達:

onCreate()
onStart()
onResume()
onPause()
onStop()
onDestroy()
複制代碼
           

要觀察生命周期就不得不繼承 Activity 重寫這些方法,想把生命周期的變化分發給其他元件就很麻煩。

于是 Jetpack 引入了 Lifecycle,以讓任何元件都可友善地感覺生命周期的變化:

public abstract class Lifecycle {AtomicReference<>();
    // 添加生命周期觀察者
    public abstract void addObserver(LifecycleObserver observer);
    // 移除生命周期觀察者
    public abstract void removeObserver(LifecycleObserver observer);
    // 擷取目前生命周期狀态
    public abstract State getCurrentState();
    // 生命周期事件
    public enum Event {
        ON_CREATE,
        ON_START,
        ON_RESUME,
        ON_PAUSE,
        ON_STOP,
        ON_DESTROY,
        ON_ANY;
    }
    // 生命周期狀态
    public enum State {
        DESTROYED,
        INITIALIZED,
        CREATED,
        STARTED,
        RESUMED;
    }
    // 判斷至少到達了某生命周期狀态
    public boolean isAtLeast(State state) {
        return compareTo(state) >= 0;
    }
}
複制代碼
           

Lifecycle 即是生命周期對應的類,提供了添加/移除生命周期觀察者的方法,在其内部還定義了全部生命周期的狀态及對應事件。

生命周期狀态是有先後次序的,分别對應着由小到大的 int 值。

生命周期擁有者

描述生命周期的對象已經有了,如何擷取這個對象需要個統一的接口(不然直接在 Activity 或者 Fragment 中新增一個方法嗎?),這個接口叫

LifecycleOwner

public interface LifecycleOwner {
    Lifecycle getLifecycle();
}
複制代碼
           

Activity 和 Fragment 都實作了這個接口。

隻要拿到 LifecycleOwner,就能拿到 Lifecycle,然後就能注冊生命周期觀察者。

生命周期 & 資料觀察者

生命周期觀察者是一個接口:

// 生命周期觀察者(空接口,用于表征一個類型)
public interface LifecycleObserver {}
// 生命周期事件觀察者
public interface LifecycleEventObserver extends LifecycleObserver {
    void onStateChanged(LifecycleOwner source, Lifecycle.Event event);
}
複制代碼
           

要觀察生命周期隻要實作

LifecycleEventObserver

接口,并注冊給

LifeCycle

即可。

除了生命周期觀察者外,LiveData 場景中還有一個資料觀察者:

// 資料觀察者
public interface Observer<T> {
    // 資料發生變化時回調
    void onChanged(T t);
}
複制代碼
           

資料觀察者 會和 生命周期擁有者 進行綁定:

public abstract class LiveData<T> {
    // 資料觀察者容器
    private SafeIterableMap<Observer<? super T>, ObserverWrapper> mObservers =
            new SafeIterableMap<>();
            
    public void observe(
        LifecycleOwner owner, // 被綁定的生命周期擁有者
        Observer<? super T> observer // 資料觀察者
    ) {
        ...
        // 将資料觀察者包裝成 LifecycleBoundObserver
        LifecycleBoundObserver wrapper = new LifecycleBoundObserver(owner, observer);
        // 存儲觀察者到 map 結構
        ObserverWrapper existing = mObservers.putIfAbsent(observer, wrapper);
        ...
        // 注冊生命周期觀察者。
        owner.getLifecycle().addObserver(wrapper);
    }
}
複制代碼
           

在觀察 LiveData 時,需傳入兩個參數,生命周期擁有者和資料觀察者。這兩個對象經過

LifecycleBoundObserver

的包裝被綁定在了一起:

class LifecycleBoundObserver extends ObserverWrapper implements LifecycleEventObserver {
    // 持有生命周期擁有者
    final LifecycleOwner mOwner;

    LifecycleBoundObserver(LifecycleOwner owner, Observer<? super T> observer) {
        super(observer);
        mOwner = owner;
    }
    // 生命周期變化回調
    @Override
    public void onStateChanged(LifecycleOwner source, Lifecycle.Event event) { 
        ...
        activeStateChanged(shouldBeActive())
        ...
    }
}

// 觀察者包裝類型
private abstract class ObserverWrapper {
    // 持有原始資料觀察者
    final Observer<? super T> mObserver;
    // 注入資料觀察者
    ObserverWrapper(Observer<? super T> observer) {mObserver = observer;}
    // 嘗試将最新值分發給目前資料觀察者
    void activeStateChanged(boolean newActive) {...}
    ...
}
複制代碼
           

LifecycleBoundObserver 實作了

LifecycleEventObserver

接口,并且它被注冊給了綁定的生命周期對象,遂具備了生命周期感覺能力。同時它還持有了資料觀察者,是以它還具備了資料觀察能力。

2. LiveData 是如何避免記憶體洩漏的?

先總結,再分析:

  • LiveData 的資料觀察者通常是匿名内部類,它持有界面的引用,可能造成記憶體洩漏。
  • LiveData 内部會将資料觀察者進行封裝,使其具備生命周期感覺能力。當生命周期狀态為 DESTROYED 時,自動移除觀察者。

記憶體洩漏是因為長生命周期的對象持有了短生命周期對象,阻礙了其被回收。

觀察 LiveData 資料的代碼通常這樣寫:

class LiveDataActivity : AppCompatActivity() {
    private val viewModel by lazy {
        ViewModelProviders.of([email protected]).get(MyViewModel::class.java)
    }
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        viewModel.livedata.observe([email protected]) {
            // 觀察 LiveData 資料更新(匿名内部類)
        }
    }
}
複制代碼
           

Observer 作為界面的匿名内部類,它會持有界面的引用,同時 Observer 被 LiveData 持有,LivData 被 ViewModel 持有,而 ViewModel 的生命周期比 Activity 長。(為啥比它長,可以點選這裡)。

最終的持有鍊如下:NonConfigurationInstances 持有 ViewModelStore 持有 ViewModel 持有 LiveData 持有 Observer 持有 Activity。

是以得在界面生命周期結束的時候移除 Observer,這件事情,LiveData 幫我們做了。

在 LiveData 内部 Observer 會被包裝成

LifecycleBoundObserver

class LifecycleBoundObserver extends ObserverWrapper 
    implements LifecycleEventObserver {
    final LifecycleOwner mOwner;

    LifecycleBoundObserver(LifecycleOwner owner, Observer<? super T> observer) {
        super(observer);
        mOwner = owner;
    }

    @Override
    public void onStateChanged(LifecycleOwner source, Lifecycle.Event event) {
        // 擷取目前生命周期
        Lifecycle.State currentState = mOwner.getLifecycle().getCurrentState();
        // 若生命周期為 DESTROYED 則移除資料觀察者并傳回
        if (currentState == DESTROYED) {
            removeObserver(mObserver);
            return
        }
        ...
    }
    ...
}
複制代碼
           

3. LiveData 是粘性的嗎?若是,它是怎麼做到的?

先總結,再分析:

  • LiveData 的值被存儲在内部的字段中,直到有更新的值覆寫,是以值是持久的。
  • 兩種場景下 LiveData 會将存儲的值分發給觀察者。一是值被更新,此時會周遊所有觀察者并分發之。二是新增觀察者或觀察者生命周期發生變化(至少為 STARTED),此時隻會給單個觀察者分發值。
  • LiveData 的觀察者會維護一個“值的版本号”,用于判斷上次分發的值是否是最新值。該值的初始值是-1,每次更新 LiveData 值都會讓版本号自增。
  • LiveData 并不會無條件地将值分發給觀察者,在分發之前會經曆三道坎:1. 資料觀察者是否活躍。2. 資料觀察者綁定的生命周期元件是否活躍。3. 資料觀察者的版本号是否是最新的。
  • “新觀察者”被“老值”通知的現象叫“粘性”。因為新觀察者的版本号總是小于最新版号,且添加觀察者時會觸發一次老值的分發。

如果把 sticky 翻譯成“持久的”,會更好了解一些。資料是持久的,意味着它不是轉瞬即逝的,不會因為被消費了就不見了,它會一直在那。而且當新的觀察者被注冊時,持久的資料會将最新的值分發給它。

“持久的資料”是怎麼做到的?

顯然是被存起來了。以更新 LiveData 資料的方法為切入點找找線索:

public abstract class LiveData<T> {
    // 存儲資料的字段
    private volatile Object mData;
    // 值版本号
    private int mVersion;
    // 更新值
    protected void setValue(T value) {
        assertMainThread("setValue");
        // 版本号自增
        mVersion++;
        // 存儲值
        mData = value;
        // 分發值
        dispatchingValue(null);
    }
}
複制代碼
           

setValue() 是更新 LiveData 值時必然會調用的一個方法,即使是通過 postValue() 更新值,最終也會走這個方法。

LiveData 持有一個版本号字段,用于辨別“值的版本”,就像軟體版本号一樣,這個數字用于判斷“目前值是否是最新的”,若版本号小于最新版本号,則表示目前值需要更新。

LiveData 用一個 Object 字段

mData

存儲了“值”。是以這個值會一直存在,直到被更新的值覆寫。

LiveData 分發值即是通知資料觀察者:

public abstract class LiveData<T> {
    // 用鍵值對方式持有一組資料觀察者
    private SafeIterableMap<Observer<? super T>, ObserverWrapper> mObservers =
            new SafeIterableMap<>();
    void dispatchingValue(ObserverWrapper initiator) {
            ...
            // 指定分發給單個資料觀察者
            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());
                }
            }
            ...
    }
    
    // 真正地分發值
    private void considerNotify(ObserverWrapper observer) {
        // 1. 若觀察者不活躍則不分發給它
        if (!observer.mActive) {
            return;
        }
        // 2. 根據觀察者綁定的生命周期再次判斷它是否活躍,若不活躍則不分發給它
        if (!observer.shouldBeActive()) {
            observer.activeStateChanged(false);
            return;
        }
        // 3. 若值已經是最新版本,則不分發
        if (observer.mLastVersion >= mVersion) {
            return;
        }
        // 更新觀察者的最新版本号
        observer.mLastVersion = mVersion;
        // 真正地通知觀察者
        observer.mObserver.onChanged((T) mData);
    }

}
複制代碼
           

分發值有兩種情況:“分發給單個觀察者”和“分發給所有觀察者”。當 LiveData 值更新時,需分發給所有觀察者。

所有的觀察者被存在一個 Map 結構中,分發的方式是通過周遊 Map 并逐個調用

considerNotify()

。在這個方法中需要跨過三道坎,才能真正地将值分發給資料觀察者,分别是:

  1. 資料觀察者是否活躍。
  2. 資料觀察者綁定的生命周期元件是否活躍。
  3. 資料觀察者的版本号是否是最新的。

跨過三道坎後,會将最新的版本号存儲在觀察者的 mLastVersion 字段中,即版本号除了儲存在

LiveData.mVersion

,還會在每個觀察者中儲存一個副本

mLastVersion

,最後才将之前暫存的

mData

的值分發給資料觀察者。

每個資料觀察者都和一個元件的生命周期對象綁定(見第一節),當元件生命周期發生變化時,會嘗試将最新值分發給該資料觀察者。

每一個資料觀察者都會被包裝(見第一節),包裝類型為

ObserverWrapper

// 原始資料觀察者
public interface Observer<T> {
    void onChanged(T t);
}

// 觀察者包裝類型
private abstract class ObserverWrapper {
    // 持有原始資料觀察者
    final Observer<? super T> mObserver;
    // 目前觀察者是否活躍
    boolean mActive;
    // 目前觀察者最新值版本号,初始值為 -1
    int mLastVersion = START_VERSION;
    // 注入原始觀察者
    ObserverWrapper(Observer<? super T> observer) {mObserver = observer;}
    // 當資料觀察者綁定的元件生命周期變化時,嘗試将最新值分發給目前觀察者
    void activeStateChanged(boolean newActive) {
        // 若觀察者活躍狀态未變,則不分發值
        if (newActive == mActive) {
            return;
        }
        // 更新活躍狀态
        mActive = newActive;
        // 若活躍,則将最新值分發給目前觀察者
        if (mActive) {
            dispatchingValue(this);
        }
    }
    // 是否活躍,供子類重寫
    abstract boolean shouldBeActive();
}
複制代碼
           

觀察者的包裝類型通過組合的方式持有了一個原始觀察者,并在此基礎上為其擴充了活躍狀态和版本号的概念。

觀察者包裝類型是抽象的,是否活躍由子類定義:

class LifecycleBoundObserver extends ObserverWrapper implements LifecycleEventObserver {
    final LifecycleOwner mOwner;

    LifecycleBoundObserver(LifecycleOwner owner, Observer<? super T> observer) {
        super(observer);
        mOwner = owner;
    }

    // 當與觀察者綁定的生命周期元件至少為STARTED時,表示觀察者活躍
    @Override
    boolean shouldBeActive() {
        return mOwner.getLifecycle().getCurrentState().isAtLeast(STARTED);
    }

    @Override
    public void onStateChanged( LifecycleOwner source, Lifecycle.Event event) {
        Lifecycle.State currentState = mOwner.getLifecycle().getCurrentState();
        // 當生命周期狀态發生變化,則嘗試将最新值分發給資料觀察者
        while (prevState != currentState) {
            prevState = currentState;
            // 調用父類方法,進行分發
            activeStateChanged(shouldBeActive());
            currentState = mOwner.getLifecycle().getCurrentState();
        }
    }
}
複制代碼
           

總結一下,LiveData 有兩次機會通知觀察者,與之對應的有兩種分發值的方式:

  1. 當值更新時,周遊所有觀察者将最新值分發給它們。
  2. 當與觀察者綁定元件的生命周期發生變化時,将最新的值分發給指定觀察者。

假設這樣一種場景:LiveData 的值被更新了一次,随後它被添加了一個新的資料觀察者,與之綁定元件的生命周期也正好發生了變化(變化到RESUMED),即資料更新在添加觀察者之前,此時更新值會被分發到新的觀察者嗎?

會!首先,更新值會被存儲在 mData 字段中。

其次,在添加觀察者時會觸發一次生命周期變化:

// androidx.lifecycle.LifecycleRegistry
public void addObserver(@NonNull LifecycleObserver observer) {
    State initialState = mState == DESTROYED ? DESTROYED : INITIALIZED;
    ObserverWithState statefulObserver = new ObserverWithState(observer, initialState);
    ...
    // 将生命周期事件分發給新進的觀察者
    statefulObserver.dispatchEvent(lifecycleOwner, upEvent(statefulObserver.mState));
    ...
}

// LifecycleBoundObserver 又被包了一層
static class ObserverWithState {
    State mState;
    GenericLifecycleObserver mLifecycleObserver;

    ObserverWithState(LifecycleObserver observer, State initialState) {
        mLifecycleObserver = Lifecycling.getCallback(observer);
        mState = initialState;
    }

    void dispatchEvent(LifecycleOwner owner, Event event) {
        State newState = getStateAfter(event);
        mState = min(mState, newState);
        // 分發生命周期事件給 LifecycleBoundObserver
        mLifecycleObserver.onStateChanged(owner, event);
        mState = newState;
    }
}
複制代碼
           

最後,這次嘗試必然能跨過三道坎,因為建立觀察者版本号總是小于 LiveData 的版本号(-1 < 0,LiveData.mVersion 經過一次值更新後自增為0)。

這種“新觀察者”會被“老值”通知的現象稱為粘性。

4. 粘性的 LiveData 會造成什麼問題?怎麼解決?

購物車-結算場景:假設有一個購物車界面,點選結算後跳轉到結算界面,結算界面可以回退到購物車界面。這兩個界面都是 Fragment。

結算界面和購物車界面通過共享ViewModel的方式共享商品清單:

class MyViewModel:ViewModel() {
    // 商品清單
    val selectsListLiveData = MutableLiveData<List<String>>()
    // 更新商品清單
    fun setSelectsList(goods:List<String>){
       selectsListLiveData.value = goods
    }
}
複制代碼
           

下面是倆 Fragment 界面依托的 Activity

class StickyLiveDataActivity : AppCompatActivity() {
    // 用 DSL 建構視圖
    private val contentView by lazy {
        ConstraintLayout {
            layout_id = "container"
            layout_width = match_parent
            layout_height = match_parent
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(contentView)
        // 加載購物車界面
        supportFragmentManager.beginTransaction()
            .add("container".toLayoutId(), TrolleyFragment())
            .commit()
    }
}
複制代碼
           

其中使用了 DSL 方式聲明性地建構了布局,詳細介紹可以點選Android性能優化 | 把建構布局用時縮短 20 倍(下)

購物車頁面如下:

class TrolleyFragment : Fragment() {
    // 擷取與宿主 Activity 綁定的 ViewModel
    private val myViewModel by lazy { 
        ViewModelProvider(requireActivity()).get(MyViewModel::class.java) 
    }

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        return ConstraintLayout {
            layout_width = match_parent
            layout_height = match_parent
            // 向購物車添加兩件商品
            onClick = {
                myViewModel.setSelectsList(listOf("meet","water"))
            }

            TextView {
                layout_id = "balance"
                layout_width = wrap_content
                layout_height = wrap_content
                text = "balance"
                gravity = gravity_center
                // 跳轉結算頁面
                onClick = {
                    parentFragmentManager.beginTransaction()
                        .replace("container".toLayoutId(), BalanceFragment())
                        .addToBackStack("trolley")
                        .commit()
                }
            }
        }
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        // 觀察商品清單變化
        myViewModel.selectsListLiveData.observe(viewLifecycleOwner) { goods ->
            // 若商品清單超過2件商品,則 toast 提示已滿
            goods.takeIf { it.size >= 2 }?.let {
                Toast.makeText(context,"購物車已滿",Toast.LENGTH_LONG).show()
            }
        }
    }
}
複制代碼
           

在 onViewCreated() 中觀察購物車的變化,如果購物車超過 2 件商品,則 toast 提示。

下面是結算頁面:

class BalanceFragment:Fragment() {
    private val myViewModel by lazy { 
        ViewModelProvider(requireActivity()).get(MyViewModel::class.java) 
    }

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        return ConstraintLayout {
            layout_width = match_parent
            layout_height = match_parent
        }
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        // 結算界面擷取購物清單的方式也是觀察商品 LiveData
        myViewModel.selectsListLiveData.observe(viewLifecycleOwner) {...}
    }
}
複制代碼
           

跑一下 demo,當跳轉到結算界面後,點選傳回購物車,toast 會再次提示購物車已滿。

因為在跳轉結算頁面之前,購物車清單 LiveData 已經被更新過。當購物車頁面重新展示時,

onViewCreated()

會再次執行,這樣一個新觀察者被添加,因為 LiveData 是粘性的,是以上一次購物車清單會分發給新觀察者,這樣 toast 邏輯再一次被執行。

解決方案一:帶消費記錄的值

// 一次性值
open class OneShotValue<out T>(private val value: T) {
    // 值是否被消費
    private var handled = false
    // 擷取值,如果值未被處理則傳回,否則傳回空
    fun getValue(): T? {
        return if (handled) {
            null
        } else {
            handled = true
            value
        }
    }
    // 擷取上次被處理的值
    fun peekValue(): T = value
}
複制代碼
           

在值的外面套一層,新增一個标記位辨別是否被處理過。

用這個方法重構下 ViewModel:

class MyViewModel:ViewModel() {
    // 已選物品清單
    val selectsListLiveData = MutableLiveData<OneShotValue<List<String>>>()
    // 更新已選物品
    fun setSelectsList(goods:List<String>){
       selectsListLiveData.value = OneShotValue(goods)
    }
}
複制代碼
           

觀察購物車的邏輯也要做修改:

class TrolleyFragment : Fragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        myViewModel.selectsListLiveData.observe(viewLifecycleOwner) { goods ->
            goods.getValue()?.takeIf { it.size >= 2 }?.let {
                Toast.makeText(context,"購物車滿了",Toast.LENGTH_LONG).show()
            }
        }
    }
}
複制代碼
           

重複彈 toast 的問題是解決了,但引出了一個新的問題:當購物車滿彈出 toast 時,購物車清單已經被消費掉了,導緻結算界面就無法再消費了。

這時候隻能用

peekValue()

來擷取已經被消費的值:

class BalanceFragment:Fragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        myViewModel.selectsListLiveData.observe(viewLifecycleOwner) {
            val list = it.peekValue()// 使用 peekValue() 擷取購物車清單
        }
    }
}
複制代碼
           

bug 全解完了。但不覺得這樣處理有一些擰巴嗎?

用“一次性值”封裝 LiveData 的值,以去除其粘性。使用該方案得甄别出哪些觀察者需要粘性值,哪些觀察者需要非粘性事件。當觀察者很多的時候,就很難招架了。若把需要粘性處理和非粘性處理的邏輯寫在一個觀察者中,就 GG,還得建立觀察者将它們分開。

解決方案二:帶有最新版本号的觀察者

通知觀察者前需要跨過三道坎(詳見第三節),其中有一道坎是版本号的比對。若建立的觀察者版本号小于最新版本号,則表示觀察者落後了,需要将最新值分發給它。

LiveData 源碼中,建立觀察者的版本号總是 -1。

// 觀察者包裝類型
private abstract class ObserverWrapper {
    // 目前觀察者最新值版本号,初始值為 -1
    int mLastVersion = START_VERSION;
    ...
}
複制代碼
           

若能夠讓建立觀察者的版本号被最新版本号指派,那版本号對比的那道坎就過不了,新值就無法分發到建立觀察者。

是以得通過反射修改 mLastVersion 字段。

該方案除了傾入性強之外,把 LiveData 粘性徹底破壞了。但有的時候,我們還是想利用粘性的。。。

解決方案三:SingleLiveEvent

這是谷歌給出的一個解決方案,源碼可以點選這裡

public class SingleLiveEvent<T> extends MutableLiveData<T> {
    // 标志位,用于表達值是否被消費
    private final AtomicBoolean mPending = new AtomicBoolean(false);

    public void observe(LifecycleOwner owner, final Observer<T> observer) {
        // 中間觀察者
        super.observe(owner, new Observer<T>() {
            @Override
            public void onChanged(@Nullable T t) {
                // 隻有當值未被消費過時,才通知下遊觀察者
                if (mPending.compareAndSet(true, false)) {
                    observer.onChanged(t);
                }
            }
        });
    }

    public void setValue(@Nullable T t) {
        // 當值更新時,置标志位為 true
        mPending.set(true);
        super.setValue(t);
    }

    public void call() {
        setValue(null);
    }
}
複制代碼
           

專門設立一個 LiveData,它不具備粘性。它通過新增的“中間觀察者”,攔截上遊資料變化,然後在轉發給下遊。攔截之後通常可以做一點手腳,比如增加一個标記位

mPending

是否消費過的判斷,若消費過則不轉發給下遊。

在資料驅動的 App 界面下,存在兩種值:1. 非暫态資料 2. 暫态資料

demo 中用于提示“購物車已滿”的資料就是“暫态資料”,這種資料是一次性的,轉瞬即逝的,可以消費一次就扔掉。

demo 中購物車中的商品清單就是“非暫态資料”,它的生命周期要比暫态資料長一點,在購物車界面和結算界面存活的期間都應該能被重複消費。

SingleLiveEvent 的設計正是基于對資料的這種分類方法,即暫态資料使用 SingleLiveEvent,非暫态資料使用正常的 LiveData。

這樣塵歸塵土歸土的解決方案是符合現實情況的。将 demo 改造一下:

class MyViewModel : ViewModel() {
    // 非暫态購物車清單 LiveData
    val selectsListLiveData = MutableLiveData<List<String>>()
    // 暫态購物車清單 LiveData
    val singleListLiveData = SingleLiveEvent<List<String>>()
    // 更新購物車清單,同時更新暫态和非暫态
    fun setSelectsList(goods: List<String>) {
        selectsListLiveData.value = goods
        singleListLiveData.value = goods
    }
}
複制代碼
           

在購物車界面做相應的改動:

class TrolleyFragment : Fragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        // 隻觀察非暫态購物車清單
        myViewModel.singleListLiveData.observe(viewLifecycleOwner) { goods ->
            goods.takeIf { it.size >= 2 }?.let {
                Toast.makeText(context,"full",Toast.LENGTH_LONG).show()
            }
        }
    }
}
複制代碼
           

但該方案有局限性,若為 SingleLiveEvent 添加多個觀察者,則當第一個觀察者消費了資料後,其他觀察者就沒機會消費了。因為

mPending

是所有觀察者共享的。

解決方案也很簡單,為每個中間觀察者都持有是否消費過資料的标記位:

open class LiveEvent<T> : MediatorLiveData<T>() {
    // 持有多個中間觀察者
    private val observers = ArraySet<ObserverWrapper<in T>>()

    @MainThread
    override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
        observers.find { it.observer === observer }?.let { _ ->
            return
        }
        // 建構中間觀察者
        val wrapper = ObserverWrapper(observer)
        observers.add(wrapper)
        super.observe(owner, wrapper)
    }

    @MainThread
    override fun observeForever(observer: Observer<in T>) {
        observers.find { it.observer === observer }?.let { _ ->
            return
        }
        val wrapper = ObserverWrapper(observer)
        observers.add(wrapper)
        super.observeForever(wrapper)
    }

    @MainThread
    override fun removeObserver(observer: Observer<in T>) {
        if (observer is ObserverWrapper && observers.remove(observer)) {
            super.removeObserver(observer)
            return
        }
        val iterator = observers.iterator()
        while (iterator.hasNext()) {
            val wrapper = iterator.next()
            if (wrapper.observer == observer) {
                iterator.remove()
                super.removeObserver(wrapper)
                break
            }
        }
    }

    @MainThread
    override fun setValue(t: T?) {
        // 通知所有中間觀察者,有新資料
        observers.forEach { it.newValue() }
        super.setValue(t)
    }

    // 中間觀察者
    private class ObserverWrapper<T>(val observer: Observer<T>) : Observer<T> {
        // 标記目前觀察者是否消費了資料
        private var pending = false

        override fun onChanged(t: T?) {
            // 保證隻向下遊觀察者分發一次資料
            if (pending) {
                pending = false
                observer.onChanged(t)
            }
        }

        fun newValue() {
            pending = true
        }
    }
}
複制代碼
           

解決方案四:Kotlin Flow

限于篇幅原因及主題的原因(主題是 LiveData),直接給出代碼(目前做法有問題),關于 LiveData vs Flow 的詳細分析可以點選如何把業務代碼越寫越複雜?(二)| Flow 替換 LiveData 重構資料鍊路,更加 MVI

class MyViewModel : ViewModel() {
    // 商品清單流
    val selectsListFlow = MutableSharedFlow<List<String>>()
    // 更新商品清單
    fun setSelectsList(goods: List<String>) {
        viewModelScope.launch {
            selectsListFlow.emit(goods)
        }
    }
}
複制代碼
           

購物車代碼如下:

class TrolleyFragment : Fragment() {
    private val myViewModel by lazy { 
        ViewModelProvider(requireActivity()).get(MyViewModel::class.java) 
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // 1.先産生資料
        myViewModel.setSelectsList(listOf("food_meet", "food_water", "book_1"))
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        // 2.再訂閱商品清單流
        lifecycleScope.launch {
            myViewModel.selectsListFlow.collect { goods ->
                goods.takeIf { it.size >= 2 }?.let {
                    Log.v("ttaylor", "購物車滿")
                }
            }
        }
    }
}
複制代碼
           

資料生産在訂閱之前,訂閱後并不會列印 log。

如果這樣修改 SharedFlow 的建構參數,則可以讓其變得粘性:

class MyViewModel : ViewModel() {
    val selectsListFlow = MutableSharedFlow<List<String>>(replay = 1)
}
複制代碼
           

replay = 1 表示會将最新的那個資料通知給新進的訂閱者。

這隻是解決了粘性/非粘性之間友善切換的問題,并未解決仍需多個流的問題。帶下一篇繼續深入分析。

5. 什麼情況下 LiveData 會丢失資料?

先總結,再分析:

在高頻資料更新的場景下使用 LiveData.postValue() 時,會造成資料丢失。因為“設值”和“分發值”是分開執行的,之間存在延遲。值先被緩存在變量中,再向主線程抛一個分發值的任務。若在這延遲之間再一次調用 postValue(),則變量中緩存的值被更新,之前的值在沒有被分發之前就被擦除了。

下面是 LiveData.postValue() 的源碼:

public abstract class LiveData<T> {
    // 暫存值字段
    volatile Object mPendingData = NOT_SET;
    private final Runnable mPostValueRunnable = new Runnable() {
        @Override
        public void run() {
            Object newValue;
            synchronized (mDataLock) {
                // 同步地擷取暫存值
                newValue = mPendingData;
                mPendingData = NOT_SET;
            }
            // 分發值
            setValue((T) newValue);
        }
    };
    
    protected void postValue(T value) {
        boolean postTask;
        synchronized (mDataLock) {
            postTask = mPendingData == NOT_SET;
            // 暫存值
            mPendingData = value;
        }
        ...
        // 向主線程抛 runnable
        ArchTaskExecutor.getInstance().postToMainThread(mPostValueRunnable);
    }
}
複制代碼
           

6. 在 Fragment 中使用 LiveData 需注意些什麼?

先總結,再分析:

在 Fragment 中觀察 LiveData 時使用

viewLifecycleOwner

而不是

this

。因為 Fragment 和 其中的 View 生命周期不完全一緻。LiveData 内部判定生命周期為 DESTROYED 時,才會移除資料觀察者。存在一種情況,當 Fragment 之間切換時,被替換的 Fragment 不執行 onDestroy(),當它再次展示時會再次訂閱 LiveData,于是乎就多出一個訂閱者。

還是購物-結算的場景:購物車和結算頁都是兩個 Fragment,将商品清單存在共享 ViewMode 的 LiveData 中,購物車及結算頁都觀察它,結算頁除了用它列出購物清單之外,還可以通過更改商品數量來修改 LiveData。當從結算頁傳回購物車頁面時,購物車界面得重新整理商品數量。

上述場景,若購物車頁面觀察 LiveData 時使用

this

會發生什麼?

// 購物車界面
class TrolleyFragment : Fragment() {
    private val myViewModel by lazy { 
        ViewModelProvider(requireActivity()).get(MyViewModel::class.java) 
    }
    
    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        return ConstraintLayout {
            layout_width = match_parent
            layout_height = match_parent
            onClick = {
                parentFragmentManager.beginTransaction()
                    .replace("container".toLayoutId(), BalanceFragment())
                    .addToBackStack("trolley")// 将購物車頁面添加到 back stack
                    .commit()
            }
        }
    }
    
    // 不得不增加這個注釋,因為 this 會飄紅
    @SuppressLint("FragmentLiveDataObserve")
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        // 将 this 作為生命周期擁有者傳給 LiveData
        myViewModel.selectsListLiveData.observe(this, object : Observer<List<String>> {
            override fun onChanged(t: List<String>?) {
                Log.v("ttaylor", "商品數量發生變化")
            }
        })
    }
}
複制代碼
           

這樣寫

this

會飄紅,AndroidStudio 不推薦使用它作為生命周期擁有者,不得不加 @SuppressLint("FragmentLiveDataObserve")

結算界面修改商品數量的代碼如下:

// 結算界面
class BalanceFragment:Fragment() {
    private val myViewModel by lazy { 
        ViewModelProvider(requireActivity()).get(MyViewModel::class.java) 
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        // 模拟結算界面修改商品數量
        myViewModel.selectsListLiveData.value = listOf("數量+1")
    }
}
複制代碼
           

當從結算頁傳回購物車時,“商品數量發生變化” 會列印兩次,如果再進一次結算頁并傳回購物車,就會列印三次。

若換成

viewLifecycleOwner

就不會有這個煩惱。因為使用 replace 更換 Fragment 時,

Fragment.onDestroyView()

會執行,即 Fragment 對應 View 的生命周期狀态會變為 DESTROYED。

LiveData 内部會将生命周期為 DESTROYED 的資料觀察者移除(詳見第二節)。當再次傳回購物車時,onViewCreated() 重新執行,LiveData 會添加一個新的觀察者。一删一增,整個過程 LiveData 始終隻有一個觀察者。又因為 LiveData 是粘性的,即使修改商品數量發生在觀察之前,最新的商品數量還是會被分發到新觀察者。(詳見第三節)

但當使用 replace 更換 Fragment 并将其壓入 back stack 時,

Fragment.onDestroy()

不會調用(因為被壓棧了,并未被銷毀)。這導緻 Fragment 的生命周期狀态不會變為 DESTROYED,是以 LiveData 的觀察者不會被自動移除。當重新傳回購物車時,又添加了新的觀察者。如果不停地在購物車和結算頁間橫跳,則觀察者資料會不停地增加。

在寫 demo 的時候遇到一個坑:

// 購物車界面
class TrolleyFragment : Fragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        // 故意使用 object 文法
        myViewModel.selectsListLiveData.observe(this, object : Observer<List<String>> {
            override fun onChanged(t: List<String>?) {
                Log.v("ttaylor", "商品數量發生變化")
            }
        })
    }
}
複制代碼
           

在建構 Observer 執行個體的時候,我特意使用了 Kotlin 的 object 文法,其實明明可以使用 lambda 将其寫得更簡潔:

class TrolleyFragment : Fragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        myViewModel.selectsListLiveData.observe(this) {
            Log.v("ttaylor", "商品數量發生變化")
        }
    }
}
複制代碼
           

如果這樣寫,那 bug 就無法複現了。。。。

因為 java 編譯器會擅作主張地将同樣的 lambda 優化成靜态的,可以提升性能,不用每次都重新建構内部類。但不巧的是 LiveData 在添加觀察者時會校驗是否已存在,若存在則直接傳回:

// `androidx.lifecycle.LiveData
public void observe( LifecycleOwner owner,  Observer<? super T> observer) {
    ...
    LifecycleBoundObserver wrapper = new LifecycleBoundObserver(owner, observer);
    // 調用 map 結構的寫操作,若 key 已存在,則傳回對應 value
    ObserverWrapper existing = mObservers.putIfAbsent(observer, wrapper);
    ...
    // 已存在則直接傳回
    if (existing != null) {
        return;
    }
    owner.getLifecycle().addObserver(wrapper);
}
複制代碼
           

這樣的話,Fragment 界面之間反複橫跳也不會新增觀察者。

7. 如何變換 LiveData 資料及注意事項?

先總結,再分析:

androidx.lifecycle.Transformations

類提供了三個變換 LiveData 資料的方法,最常用的是

Transformations.map()

,它使用

MediatorLiveData

作為資料的中間消費者,并将變換後的資料傳遞給最終消費者。需要注意的是,資料變化操作都發生在主線程,主線程有可能被耗時操作阻塞。解決方案是将 LiveData 資料變換操作異步化,比如通過

CoroutineLiveData

還是購物-結算的場景:購物車和結算頁都是兩個 Fragment,将商品清單存在 LiveData 中,購物車及結算頁都觀察它。結算界面對打折商品有一個特殊的 UI 展示。

此時就可以将商品清單 LiveData 進行一次變換(過濾)得到一個新的打折商品清單:

class MyViewModel : ViewModel() {
    // 商品清單
    val selectsListLiveData = MutableLiveData<List<String>>()
    // 打折商品清單
    val foodListLiveData = Transformations.map(selectsListLiveData) { list ->
        list.filter { it.startsWith("discount") }
    }
}
複制代碼
           

每當商品清單發生變化,打折商品清單都會收到通知,并過濾出新的打折商品。打折商品清單是一個新的 LiveData,可以單獨被觀察。

其中的過濾清單操作發生在主線程,如果業務略複雜,資料變換操作耗時的話,可能阻塞主線程。

如何将 LiveData 變換資料異步化?

LiveData 的 Kotlin 擴充包裡提供了一個将 LiveData 和協程結合的産物:

class MyViewModel : ViewModel() {
    // 商品清單
    val selectsListLiveData = MutableLiveData<List<String>>()
    // 用異步方式擷取打折商品清單
    val asyncLiveData = selectsListLiveData.switchMap { list ->
        // 将源 LiveData 中的值轉換成一個 CoroutineLiveData
        liveData(Dispatchers.Default) {
            emit( list.filter { it.startsWith("discount") } )
        }
    }
}
複制代碼
           

其中的

switchMap()

是 LiveData 的擴充方法,它是對

Transformations.switchMap()

的封裝,用于友善鍊式調用:

public inline fun <X, Y> LiveData<X>.switchMap(
    crossinline transform: (X) -> LiveData<Y>
): LiveData<Y> = Transformations.switchMap(this) { transform(it) }
複制代碼
           

switchMap() 内部将源 LiveData 的每個值都轉換成一個新的 LiveData 并訂閱。

liveData

是一個頂層方法,用于建構

CoroutineLiveData

public fun <T> liveData(
    context: CoroutineContext = EmptyCoroutineContext,
    timeoutInMs: Long = DEFAULT_TIMEOUT,
    block: suspend LiveDataScope<T>.() -> Unit
): LiveData<T> = CoroutineLiveData(context, timeoutInMs, block)
複制代碼
           

CoroutineLiveData 将更新 LiveData 值的操作封裝到一個挂起方法中,可以通過協程上下文指定執行的線程。

使用 CoroutineLiveData 需要添加如下依賴:

implementation  "androidx.lifecycle:lifecycle-livedata-ktx:2.3.1"
複制代碼
           

參考

Jetpack MVVM 七宗罪之四: 使用 LiveData/StateFlow 發送 Events

作者:唐子玄

連結:https://juejin.cn/post/7085037365101592612

來源:稀土掘金

著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。