天天看點

Android Jetpack系列之LiveData

文章目錄

    • LiveData介紹
    • LiveData優點
    • LiveData使用舉例
      • 基礎用法
      • 進階用法
        • Transformations.map()修改資料源
        • Transformations.switchMap()切換資料源
    • 源碼解析
      • 發送資料setValue/postValue
      • 注冊觀察者Observer并監聽資料變化
        • LiveData.observe()
        • LiveData.observeForever()
      • LiveData實作類MutableLiveData
      • 資料切換/修改 Transformations.map()/switchMap()
    • 參考

LiveData介紹

LiveData是一種可觀察的資料存儲類。LiveData 具有生命周期感覺能力,遵循其他應用元件(如 Activity、Fragment 或 Service)的生命周期。這種感覺能力可確定 LiveData 僅更新處于活躍生命周期狀态的Observer,非活躍狀态下的Observer不會受到通知。

生命周期狀态可以通過Lifecycle提供,包括DESTROYED、INITIALIZED、CREATED、STARTED、RESUMED,當且僅當生命周期處于STARTED、RESUMED時為活躍狀态,其他狀态是非活躍狀态。

LiveData優點

  • 確定界面符合資料狀态

    LiveData 遵循觀察者模式。當資料發生變化時,LiveData 會通知 Observer 對象,那麼Observer回調的方法中就可以進行UI更新,即資料驅動。

  • 不會發生記憶體洩漏

    觀察者會綁定到 Lifecycle 對象,并在其關聯的生命周期遭到銷毀(如Activity進入ONDESTROY狀态)後進行自我清理。

  • 不會因 Activity 停止而導緻崩潰

    如果觀察者的生命周期處于非活躍狀态(如傳回棧中的 Activity),則它不會接收任何 LiveData 事件。

  • 不再需要手動處理生命周期

    界面元件隻是觀察相關資料,不會停止或恢複觀察。LiveData 将自動管理所有這些操作,因為它在觀察時可以感覺相關的生命周期狀态變化。

  • 資料始終保持最新狀态

    如果生命周期變為非活躍狀态,它會在再次變為活躍狀态時接收最新的資料。例如,曾經在背景的 Activity 會在傳回前台後立即接收最新的資料。

  • 配置更改時自動儲存資料

    如果由于配置更改(如裝置旋轉)而重新建立了 Activity 或 Fragment,它會立即接收最新的可用資料。

  • 共享資源

    使用單例模式擴充 LiveData 對象以封裝系統服務,以便在應用中共享它們。LiveData 對象連接配接到系統服務一次,然後需要相應資源的任何觀察者隻需觀察 LiveData 對象。

LiveData使用舉例

基礎用法

先上效果圖:

Android Jetpack系列之LiveData

在Activity中動态添加了一個Fragment,點選按鈕産生一個1000以内的随機值,并通過LiveData.setValue發送出去,在Fragment中通過LiveData.observe進行資料觀察與接收,可以看到即使Activity中先發送的資料,Fragment中滞後注冊觀察者依然能收到資料,即LiveData發送的是粘性事件。

首先需要保證Activity和Fragment中的LiveData是同一個執行個體:

//LiveDataInstance.kt 使用object來聲明單例模式
object LiveDataInstance {
    //MutableLiveData是抽象類LiveData的具體實作類
    val INSTANCE = MutableLiveData<String>()
}
           

Activity中随機生成一個數并通過LiveData.setValue進行發送:

//LiveDataActivity.kt
class LiveDataActivity : AppCompatActivity() {
    lateinit var mTextView: TextView
    var mFragment: LiveDataFragment? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_live_data)
        mTextView = findViewById(R.id.tv_text)
        addLiveDataFragment()
    }

    fun updateValue(view: View) {
        sendData(produceData())
    }

    //随機更新一個整數
    private fun produceData(): String {
        val randomValue = (0..1000).random().toString()
        mTextView.text = "Activity中發送:$randomValue"
        return randomValue
    }

    //通過setValue發送更新
    private fun sendData(randomValue: String) {
        LiveDataInstance.INSTANCE.value = randomValue
    }

    //添加Fragment
    fun addFragment(view: View) {
        addLiveDataFragment()
    }

    //移除Fragment
    fun removeFragment(view: View) {
        delLiveDataFragment()
    }

    private fun addLiveDataFragment() {
        val fragment = supportFragmentManager.findFragmentById(R.id.fl_content)
        if (fragment != null) {
            Toast.makeText(this, "請勿重複添加", Toast.LENGTH_SHORT).show()
            return
        }

        if (mFragment == null) {
            mFragment = LiveDataFragment.newInstance()
        }
        supportFragmentManager
            .beginTransaction()
            .add(R.id.fl_content, mFragment!!)
            .commitAllowingStateLoss()
    }

    private fun delLiveDataFragment() {
        val fragment = supportFragmentManager.findFragmentById(R.id.fl_content)
        if (fragment == null) {
            Toast.makeText(this, "沒有Fragment", Toast.LENGTH_SHORT).show()
            return
        }
        supportFragmentManager.beginTransaction().remove(fragment).commitAllowingStateLoss()
    }
}
           

Fragment動态添加到Activity中,并通過LiveData.observe注冊觀察者并監聽資料變化:

//LiveDataFragment.kt
class LiveDataFragment : Fragment() {
    lateinit var mTvObserveView: TextView

    //資料觀察者 資料改變時在onChange()中進行重新整理
    private val changeObserver = Observer<String> { value ->
        value?.let {
            Log.e(JConsts.LIVE_DATA, "observer:$value")
            mTvObserveView.text = value
        }
    }

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        return inflater.inflate(R.layout.live_data_fragment, container, false)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        mTvObserveView = view.findViewById(R.id.tv_observe)
    }

    override fun onActivityCreated(savedInstanceState: Bundle?) {
        super.onActivityCreated(savedInstanceState)
        //通過LiveData.observe注冊觀察者,監聽資料變化
        LiveDataInstance.INSTANCE.observe(this, changeObserver)
    }

    companion object {
        fun newInstance() = LiveDataFragment()
    }
}
           

上面就是一個LiveData的基本用法了,很簡單,Activity/Fragment中共用LiveData執行個體,在Activity中通過點選按鈕生成一個随機數并通過LiveData.setValue發送資料(如果在子線程中發送,需要使用postValue),然後Fragment中通過LiveData.observe注冊觀察者并監聽資料變化。

改一下代碼:

//LiveDataActivity.kt
 override fun onStop() {
     super.onStop()
     val data = produceData()
     Log.e(JConsts.LIVE_DATA, "onStop():$data")
     sendData(data)
 }

 //LiveDataFragment.kt
 private val changeObserver = Observer<String> { value ->
     value?.let {
         Log.e(JConsts.LIVE_DATA, "observer:$value")
         mTvObserveView.text = value
     }
 }
           

點選Home鍵,Activity的onStop會觸發,并通過LiveData.setValue發送資料,看下列印日志:

2021-07-13 17:07:08.662 1459-1459/com.example.jetpackstudy E/LIVEDATA: onStop():742
           

Activity中在onStop中重新生成了一個随機值并發送了出去,但是在Fragment中的Observer中并沒有收到資料,這是為什麼呢?還記得LiveData的能力嗎,它是能感覺生命周期的,并且隻會更新處于活躍狀态下的Observer(STARTED、RESUMED狀态),是以在onStop中發送的事件,Fragment作為觀察者已經不在活躍狀态下了,并不會收到通知,當我們App切回前台時,Observer重新回到活躍狀态,是以會收到Activity之前發送的事件:

2021-07-13 17:12:47.433 5850-5850/com.example.jetpackstudy E/LIVEDATA: observer:742
           

如果想讓Observer不管在什麼狀态下都能馬上收到資料變化的通知,可以使用LiveData.observeForever來注冊并監聽資料變化:

//LiveDataFragment.kt
 private val changeObserver = Observer<String> { value ->
     value?.let {
         Log.e(JConsts.LIVE_DATA, "observer:$value")
         mTvObserveView.text = value
     }
 }

//observeForever不管Observer是否處于活躍狀态都會立馬相應資料變化
//注意這裡隻需要傳一個Observer即可,不需要傳入LifecycleOwner,因為不需要考慮Observer是否處于活躍狀态
LiveDataInstance.INSTANCE.observeForever(changeObserver)

override fun onDestroy() {
    super.onDestroy()
    //需要手動移除觀察者
    LiveDataInstance.INSTANCE.observeForever(changeObserver)
}
           

上述代碼重新實驗,點選Home鍵将App切到背景:

2021-07-13 17:29:56.848 15679-15679/com.example.jetpackstudy E/LIVEDATA: onStop():878
2021-07-13 17:29:56.849 15679-15679/com.example.jetpackstudy E/LIVEDATA: observer:878
           

可以看到通過LiveData.observeForever注冊的Observer即使不在活躍狀态也是會立馬相應資料變化的,這裡要注意一點,LiveData.observeForever注冊的Observer并不會自動解除注冊,需要我們手動處理。

進階用法

Transformations.map()修改資料源

先上效果圖:

Android Jetpack系列之LiveData
//LiveDataFragment.kt
//資料觀察者 資料改變時在onChange()中進行重新整理
private val changeObserver = Observer<String> { value ->
    value?.let {
        Log.e(JConsts.LIVE_DATA, "transform observer:$value")
        mTvObserveView.text = value
    }
}

//Transformations.map()改變接收的data
val transformLiveData = Transformations.map(LiveDataInstance.INSTANCE) { "Transform:$it" }

//觀察者監聽的時候傳入了LifecycleOwner 用以監聽生命周期變化
transformLiveData.observe(this, changeObserver)
           

可以看到在Activity中發送的資料源是“xxx”,Fragment中經過Transformations.map變換後變成"Transform:xxx",通過Transformations.map()可以對接收的資料源進行修改。

Transformations.switchMap()切換資料源

Android Jetpack系列之LiveData
//LiveDataInstance.kt
object LiveDataInstance {
    val INSTANCE = MutableLiveData<String>()
    val INSTANCE2 = MutableLiveData<String>()
    val SWITCH = MutableLiveData<Boolean>()
}
           

注:一般LiveData都是與ViewModel結合使用的,本文主要介紹LiveData,是以使用了單例

//LiveDataActivity.kt
mBtnSwitch = findViewById(R.id.btn_switch)
mBtnSwitch.setOnCheckedChangeListener { _, isChecked ->
//發送開關狀态 用以在Transformations.switchMap中切換資料源
LiveDataInstance.SWITCH.value = isChecked
}

//通過setValue發送更新
private fun sendData(randomValue: String, isLeft: Boolean) {
  if (isLeft) {
    LiveDataInstance.INSTANCE.value = randomValue
  } else {
    LiveDataInstance.INSTANCE2.value = randomValue
  }
}
           
//LiveDataFragment.kt
//資料觀察者 資料改變時在onChange()中進行重新整理
private val changeObserver = Observer<String> { value ->
    value?.let {
        Log.e(JConsts.LIVE_DATA, "transform observer:$value")
        mTvObserveView.text = value
    }
}

//Transformations.switchMap()切換資料源
val switchMapLiveData =
   Transformations.switchMap(LiveDataInstance.SWITCH) { switchRight ->
       if (switchRight) {
            LiveDataInstance.INSTANCE2
        } else {
            LiveDataInstance.INSTANCE
        }
     }
switchMapLiveData.observe(this, changeObserverTransform)
           

例子中有兩個資料源:LiveDataInstance.INSTANCE、LiveDataInstance.INSTANCE2,當Switch開關切換時,通過Transformations.switchMap()可以來回切換資料源,Observer中也會更新對應的資料。

源碼解析

發送資料setValue/postValue

//LiveData.java
    //setValue發送資料,隻能在主線程中使用
    protected void setValue(T value) {
        assertMainThread("setValue");
        mVersion++;
        mData = value;
        dispatchingValue(null);
    }

    //postValue發送資料,可以在子線程中使用
    protected void postValue(T value) {
        boolean postTask;
        synchronized (mDataLock) {
            postTask = mPendingData == NOT_SET;
            mPendingData = value;
        }
        if (!postTask) {
            return;
        }
 ArchTaskExecutor.getInstance().postToMainThread(mPostValueRunnable);
    }

private final Runnable mPostValueRunnable = new Runnable() {
        @SuppressWarnings("unchecked")
        @Override
        public void run() {
            Object newValue;
            synchronized (mDataLock) {
                newValue = mPendingData;
                mPendingData = NOT_SET;
            }
            setValue((T) newValue);
        }
    };
           

可以看到setValue/postValue都可以發送資料,差別是postValue還可以在子線程中發送資料,本質上postValue通過Handler将事件發送到Main線程中,最終也是調用了setValue發送事件,是以隻看setValue()方法,該方法最後調用了dispatchingValue()方法并傳入了一個參數null,繼續看該方法:

void dispatchingValue(@Nullable ObserverWrapper initiator) {
        if (mDispatchingValue) {
            mDispatchInvalidated = true;
            return;
        }
        mDispatchingValue = true;
        do {
            mDispatchInvalidated = false;
            if (initiator != null) {
                //2、通過observe()的方式會調用這裡
                considerNotify(initiator);
                initiator = null;
            } else {
                //1、通過setValue/postValue的方式會調用這裡,周遊所有觀察者并進行分發
                for (Iterator<Map.Entry<Observer<? super T>, ObserverWrapper>> iterator =
                        mObservers.iteratorWithAdditions(); iterator.hasNext(); ) {
                    considerNotify(iterator.next().getValue());
                    if (mDispatchInvalidated) {
                        break;
                    }
                }
            }
        } while (mDispatchInvalidated);
        mDispatchingValue = false;
    }

    private void considerNotify(ObserverWrapper observer) {
        if (!observer.mActive) {
            //觀察者不在活躍狀态 直接傳回
            return;
        }
        //如果是observe(),則是在STARTED、RESUMED狀态時活躍;如果是ObserveForever(),則認為一直是活躍狀态
        if (!observer.shouldBeActive()) {
            observer.activeStateChanged(false);
            return;
        }
        //Observer中的Version必須小于LiveData中的Version,防止重複發送
        if (observer.mLastVersion >= mVersion) {
            return;
        }
        observer.mLastVersion = mVersion;
        //回調Observer的onChange方法并接收資料
        observer.mObserver.onChanged((T) mData);
    }
           

因為傳入的參數是null,是以最終走到了1處,周遊所有的觀察者并回調Observer的onChange方法接收資料,這樣就完成了一次資料的傳遞。2處是單獨調用一個觀察者并回調其onChange方法接收資料,是執行observe()方法的時候執行的,具體等後面分析。

注冊觀察者Observer并監聽資料變化

LiveData.observe()

@MainThread
public void observe(@NonNull LifecycleOwner owner, @NonNull Observer<? super T> observer) {
    assertMainThread("observe");
    if (owner.getLifecycle().getCurrentState() == DESTROYED) {
        //如果目前觀察者處于DESTROYED狀态,直接傳回
        return;
    }
    //将LifecycleOwner、Observer包裝成LifecycleBoundObserver
    LifecycleBoundObserver wrapper = new LifecycleBoundObserver(owner, observer);
    //ObserverWrapper是LifecycleBoundObserver的父類
    ObserverWrapper existing = mObservers.putIfAbsent(observer, wrapper);
    //如果mObservers中存在該Observer且跟傳進來的LifecycleOwner不同,直接抛異常,一個Observer隻能對應一個LifecycleOwner
    if (existing != null && !existing.isAttachedTo(owner)) {
        throw new IllegalArgumentException("Cannot add the same observer"
                + " with different lifecycles");
    }
    //如果已經存在Observer且跟傳進來的LifecycleOwner是同一個,直接傳回
    if (existing != null) {
        return;
    }
    //通過Lifecycle添加觀察者
    owner.getLifecycle().addObserver(wrapper);
}
           

可以看到最後observe()将Observer加入到Lifecycle裡去了,并通過onStateChanged()回調來監聽LifecycleOwner生命周期的變化,主要看onStateChanged()方法:

class LifecycleBoundObserver extends ObserverWrapper implements LifecycleEventObserver {
    @NonNull
    final LifecycleOwner mOwner;

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

    @Override
    boolean shouldBeActive() {
        return mOwner.getLifecycle().getCurrentState().isAtLeast(STARTED);
    }

    @Override
    public void onStateChanged(@NonNull LifecycleOwner source,
            @NonNull Lifecycle.Event event) {
        //Observer對應的LifecycleOwner是DESTROYED狀态,直接删除該Observer,是以LiveData有自動解除Observer的能力
        if (mOwner.getLifecycle().getCurrentState() == DESTROYED) {
            removeObserver(mObserver);
            return;
        }
        //
        activeStateChanged(shouldBeActive());
    }

    @Override
    boolean isAttachedTo(LifecycleOwner owner) {
        return mOwner == owner;
    }

    @Override
    void detachObserver() {
        mOwner.getLifecycle().removeObserver(this);
    }
}

//ObserverWrapper.java
void activeStateChanged(boolean newActive) {
    if (newActive == mActive) {
        return;
    }
    mActive = newActive;
    boolean wasInactive = LiveData.this.mActiveCount == 0;
    LiveData.this.mActiveCount += mActive ? 1 : -1;
    if (wasInactive && mActive) {
        //觀察者數量從0變為1時
        onActive();
    }
    if (LiveData.this.mActiveCount == 0 && !mActive) {
        //觀察者數量從1變為0時
        onInactive();
    }
    if (mActive) {
        //觀察者為活躍狀态,進行分發
        dispatchingValue(this);
    }
}
           

onActive()在觀察者數量從0變為1時執行;onInactive()在觀察者數量從1變為0時執行。最後如果目前觀察者是活躍狀态,直接執行dispatchingValue(this),this是目前ObserverWrapper對象,還記得dispatchingValue()方法嗎,前面講這個方法的時候留了個疑問,這裡就會執行前面講的該方法裡2處的代碼,用以分發事件并在Observer的onChange()方法裡接收并處理事件。

LiveData.observeForever()

@MainThread
public void observeForever(@NonNull Observer<? super T> observer) {
    assertMainThread("observeForever");
    AlwaysActiveObserver wrapper = new AlwaysActiveObserver(observer);
    ObserverWrapper existing = mObservers.putIfAbsent(observer, wrapper);
    if (existing instanceof LiveData.LifecycleBoundObserver) {
        throw new IllegalArgumentException("Cannot add the same observer"
                + " with different lifecycles");
    }
    if (existing != null) {
        return;
    }
    wrapper.activeStateChanged(true);
}
           

observeForever()中不需要傳LifecycleOwner參數,因為observeForever()認為是一直活躍的狀态,是以不需要監聽LifecycleOwner的生命周期,最後是直接執行了wrapper.activeStateChanged(true)方法,後續的邏輯跟上面observe()一樣了。這裡注意一點,observeForever()注冊的觀察者當處于DESTROYED的時候并不會自動删除,需要手動删除之。

LiveData實作類MutableLiveData

public class MutableLiveData<T> extends LiveData<T> {

    public MutableLiveData(T value) {
        super(value);
    }

    public MutableLiveData() {
        super();
    }

    @Override
    public void postValue(T value) {
        super.postValue(value);
    }

    @Override
    public void setValue(T value) {
        super.setValue(value);
    }
}
           

MutableLiveData是抽象類LiveData的具體實作類。

資料切換/修改 Transformations.map()/switchMap()

//Transformations.java
public static <X, Y> LiveData<Y> map(
        @NonNull LiveData<X> source,
        @NonNull final Function<X, Y> mapFunction) {
    final MediatorLiveData<Y> result = new MediatorLiveData<>();
    result.addSource(source, new Observer<X>() {
        @Override
        public void onChanged(@Nullable X x) {
            result.setValue(mapFunction.apply(x));
        }
    });
    return result;
}
           

從源碼的注釋上,看到了這麼一句話

This method is analogous to {@link io.reactivex.Observable#map}

,哦,原來用法是跟RxJava中的Map操作符類似。第一個參數是源LiveData< X>,第2個參數是個Funtion< X,Y>,目的就是将LiveData< X>變換為LiveData< Y>,然後再重新發送事件。map()方法裡new了一個MediatorLiveData并執行了addSource()方法,看看這個方法怎麼實作的:

//MediatorLiveData.java
public class MediatorLiveData<T> extends MutableLiveData<T> {
    private SafeIterableMap<LiveData<?>, Source<?>> mSources = new SafeIterableMap<>();

    @MainThread
    public <S> void addSource(@NonNull LiveData<S> source, @NonNull Observer<? super S> onChanged) {
        //将源LiveData及Observer包裝成Source
        Source<S> e = new Source<>(source, onChanged);
        Source<?> existing = mSources.putIfAbsent(source, e);
        //如果源LiveData中已經有Observer且跟傳進來的不一緻,直接抛異常
        if (existing != null && existing.mObserver != onChanged) {
            throw new IllegalArgumentException(
                    "This source was already added with the different observer");
        }
        if (existing != null) {
            return;
        }
        if (hasActiveObservers()) {
            //判斷有活躍觀察者時
            e.plug();
        }
    }

    @MainThread
    public <S> void removeSource(@NonNull LiveData<S> toRemote) {
        Source<?> source = mSources.remove(toRemote);
        if (source != null) {
            source.unplug();
        }
    }

    private static class Source<V> implements Observer<V> {
        final LiveData<V> mLiveData;
        final Observer<? super V> mObserver;
        int mVersion = START_VERSION;

        Source(LiveData<V> liveData, final Observer<? super V> observer) {
            mLiveData = liveData;
            mObserver = observer;
        }

        void plug() {
            //通過observeForever添加觀察者,有變動時就會回調下面的onChange()方法
            mLiveData.observeForever(this);
        }

        void unplug() {
            mLiveData.removeObserver(this);
        }

        @Override
        public void onChanged(@Nullable V v) {
            if (mVersion != mLiveData.getVersion()) {
                mVersion = mLiveData.getVersion();
                mObserver.onChanged(v);
            }
        }
    }
}
           

首先将源LiveData及Observer包裝成Source,經過了幾次判斷,最後執行到了Source#plug()方法,裡面通過observeForever添加觀察者,有變動時就會回調Source#onChange()方法,而這個方法裡又會回調傳進來的Observer#onChange()方法,即執行到了map()中傳入的Observer的onChange()方法,裡面通過setValue發送了轉換之後的資料格式,這樣就完成了整個的資料轉換格式。那麼再看switchMap()就簡單了:

public static <X, Y> LiveData<Y> switchMap(
        @NonNull LiveData<X> source,
        @NonNull final Function<X, LiveData<Y>> switchMapFunction) {
    final MediatorLiveData<Y> result = new MediatorLiveData<>();
    result.addSource(source, new Observer<X>() {
        LiveData<Y> mSource;

        @Override
        public void onChanged(@Nullable X x) {
            LiveData<Y> newLiveData = switchMapFunction.apply(x);
            if (mSource == newLiveData) {
                return;
            }
            if (mSource != null) {
                result.removeSource(mSource);
            }
            mSource = newLiveData;
            if (mSource != null) {
                result.addSource(mSource, new Observer<Y>() {
                    @Override
                    public void onChanged(@Nullable Y y) {
                        result.setValue(y);
                    }
                });
            }
        }
    });
    return result;
}
           

可以看到switchMap()中實作方式跟map()基本一緻,隻不過map()改變的是資料,而switchMap()改變的是資料源,可以對資料源進行切換。Transformations還有個distinctUntilChanged方法簡單看一下:

public static <X> LiveData<X> distinctUntilChanged(@NonNull LiveData<X> source) {
    final MediatorLiveData<X> outputLiveData = new MediatorLiveData<>();
    outputLiveData.addSource(source, new Observer<X>() {

        boolean mFirstTime = true;

        @Override
        public void onChanged(X currentValue) {
            final X previousValue = outputLiveData.getValue();
            if (mFirstTime
                    || (previousValue == null && currentValue != null)
                    || (previousValue != null && !previousValue.equals(currentValue))) {
                mFirstTime = false;
                outputLiveData.setValue(currentValue);
            }
        }
    });
    return outputLiveData;
}
           

也很簡單,隻有當資料源發生改變時,Observer才會相應,即發送重複的資料時,除第一次之外的資料都會被忽略。

最後畫一下類圖:

Android Jetpack系列之LiveData

參考

【1】https://developer.android.com/topic/libraries/architecture/livedata?hl=zh_cn

【2】Android LiveData 使用詳解

繼續閱讀