Rxjava解除訂閱①:自帶方式和RxLifeCycle

Rxjava解除訂閱三部曲:

前言

最近在維護(hù)老舊網(wǎng)絡(luò)庫(kù)的時(shí)候,發(fā)現(xiàn)網(wǎng)絡(luò)庫(kù)底層運(yùn)用到了Rxjava,而最近湊巧又給app加上了leakcanary檢測(cè)內(nèi)存泄漏唧躲,發(fā)現(xiàn)除了網(wǎng)絡(luò)庫(kù)的Rxjava泄漏之外干厚,還有些業(yè)務(wù)上濫用的Rxjava也存在泄漏的情況。有問題咱就得想辦法解決敬察,這個(gè)老舊的網(wǎng)絡(luò)庫(kù)有點(diǎn)年歲了,寫的是真不咋樣,奈何app里還有大量引用致燥,該維護(hù)的還是得維護(hù)。Rxjava作為近幾年非常流行的一個(gè)三方庫(kù)怖侦,功能就不用多說了篡悟,誰(shuí)用誰(shuí)知道。

正文

Rxjava是好用匾寝,可用不好很容易造成內(nèi)存泄漏搬葬。而且理論上Rxjava的每個(gè)操作符都可能會(huì)造成內(nèi)存泄漏。舉個(gè)例子艳悔,我們用Rx進(jìn)行網(wǎng)絡(luò)請(qǐng)求急凰,然后訂閱在主線程進(jìn)行ui更新。網(wǎng)絡(luò)請(qǐng)求是在分線程執(zhí)行,而且有延遲抡锈。當(dāng)請(qǐng)求沒有返回時(shí)疾忍,我們將這個(gè)頁(yè)面關(guān)閉了,后續(xù)分線程數(shù)據(jù)回來執(zhí)行ui更新床三,而Rx還持有外部類的引用一罩,這就造成了內(nèi)存泄漏。
解決的辦法Rx本身就給提供撇簿,這就是我們要講的第一種解除訂閱的方法:

  • Rxjava提供的Dispose.dispose();

1.Dispose.dispose()

這是Rxjava本身提供的一種接觸訂閱的方式聂渊,使用很簡(jiǎn)單,在頁(yè)面關(guān)閉的時(shí)候四瘫,或者在需要的時(shí)候調(diào)用下dispose()方法就可以了汉嗽。
如果最后的訂閱者是Consumer,那么會(huì)有一個(gè)返回值Dispose找蜜。那么在需要的時(shí)候饼暑,就可以調(diào)用Dispose.dispose()。但往往我們使用Rxjava的時(shí)候洗做,都需要對(duì)正常返回和異常返回做些通用處理弓叛,使用的往往是Observer,這樣做的結(jié)果就是沒有返回值竭望。
其實(shí)Observer也已經(jīng)給我們準(zhǔn)備好了解除訂閱的方式邪码,我們不妨看下Observer的源碼:

public interface Observer<T> {
    void onSubscribe(@NonNull Disposable d);
    void onNext(@NonNull T t);
    void onError(@NonNull Throwable e);
    void onComplete();
}

有個(gè)方法onSubscribe(@NonNull Disposable d),有一個(gè)Dispose的參數(shù)咬清,那么我們就可以接收這個(gè)變量闭专,在在需要的時(shí)候調(diào)用Dispose.dispose()
這個(gè)方法onSubscribe意味著當(dāng)subscribe方法被調(diào)用之前旧烧,就會(huì)拿到Dispose句柄影钉,此時(shí)Rxjava任何一個(gè)相關(guān)的操作符處理都還未執(zhí)行,調(diào)用dispose()方法后掘剪,完成解除訂閱平委。

擴(kuò)展: CompositeDisposable

上面講的是針對(duì)單個(gè)Dispose進(jìn)行訂閱解除,可往往實(shí)際使用中夺谁,我們可希望看到一堆Dispose的成員變量在頁(yè)面銷毀的時(shí)候扎堆解除訂閱廉赔。這時(shí)候就需要CompositeDisposable,簡(jiǎn)單的理解匾鸥,就是可以對(duì)Dispose進(jìn)行批量的處理蜡塌,類似于List集合,其內(nèi)部實(shí)現(xiàn)方法也很類似勿负,包括add馏艾,addAll,delete,remove琅摩,clear铁孵,dispose,isDisposed房资。
方法使用都很簡(jiǎn)單蜕劝,我們把Dispose1,Dispose2志膀,Dispose3使用add方法熙宇,添加到CompositeDisposable中鳖擒,在頁(yè)面銷毀時(shí)調(diào)用dispose進(jìn)行批量解除溉浙。
這里對(duì)dispose和clear方法進(jìn)行單獨(dú)說明下,dispose執(zhí)行后蒋荚,會(huì)改變CompositeDisposable的狀態(tài)為disposed戳稽,即已完成訂閱解除狀態(tài),而clear則只會(huì)批量解除訂閱期升,不會(huì)改變整個(gè)CompositeDisposabledisposed狀態(tài)惊奇。我們看下源碼就知道了:
dispose方法:

dispose

而clear方法:
clear

差異就在紅箭頭那邊。
而其他的方法都要去判斷disposed狀態(tài)播赁,已經(jīng)disposed的直接return颂郎,不會(huì)繼續(xù)執(zhí)行。

2.RxLifeCycle

接下來開始容为,就是比較騷的操作了乓序。RxLifeCycle,顧名思義就是對(duì)Rxjava生命周期管理坎背,也就是意味著替劈,Rxjava長(zhǎng)大,已經(jīng)學(xué)會(huì)了自己該何時(shí)進(jìn)行解除訂閱得滤。github直達(dá)鏈接陨献。

雖然沒有中文文檔,但摸索起來也不困難懂更,首先添加核心依賴:

implementation 'com.trello.rxlifecycle3:rxlifecycle:3.1.0'
implementation 'com.trello.rxlifecycle3:rxlifecycle-components:3.1.0'

RxLifeCycle使用需要繼承RxAppCompatActivity眨业,F(xiàn)ragment也需要繼承RxFragment,當(dāng)然還有一些其他擴(kuò)展沮协,比如RxDialogFragment等龄捡,大家自己去體驗(yàn)吧。
使用起來也是很簡(jiǎn)單皂股,直接看代碼吧:

        Observable.just(1)
                .compose(this.<Integer>bindToLifecycle())
                .subscribe();

或者:

        Observable.just(1)
                .compose(this.<Integer>bindUntilEvent(ActivityEvent.DESTROY))
                .subscribe();

核心使用就這兩個(gè)方法墅茉,這倆方法也是有些許的區(qū)別。

bindToLifecycle():自動(dòng)識(shí)別在合適的生命周期內(nèi)解除綁定。

bindUntilEvent(ActivityEvent):在指定的生命周期內(nèi)解除綁定就斤。

對(duì)于bindUntilEvent(ActivityEvent)很容易理解悍募,指定一個(gè)生命周期解除綁定,但對(duì)于bindToLifecycle()如何自動(dòng)識(shí)別生命周期有些疑問洋机,我們不妨寫個(gè)demo測(cè)試下效果如何:

    private void test() {
        subscribe = Observable.interval(0, 2, TimeUnit.SECONDS)
                .map(new Function<Long, Long>() {
                    @Override
                    public Long apply(Long aLong) throws Exception {
                        Log.d(TAG, "當(dāng)前發(fā)射數(shù)值:" + aLong);
                        return aLong;
                    }
                })
                .compose(this.<Long>bindToLifecycle())
                .subscribe(new Consumer<Long>() {
                    @Override
                    public void accept(Long aLong) throws Exception {
                        Log.d(TAG, "當(dāng)前接收數(shù)值:" + aLong);
                    }
                });
    }

每個(gè)2s發(fā)射一個(gè)數(shù)值坠宴,無限的發(fā),調(diào)用bindToLifecycle()绷旗,然后我們?cè)?code>onCreate方法調(diào)用喜鼓,最終打印效果:

D/zdu_Rxdemo: onCreate,subscribe.isDisposed():false
D/zdu_Rxdemo: 當(dāng)前發(fā)射數(shù)值:0
D/zdu_Rxdemo: 當(dāng)前接收數(shù)值:0
D/zdu_Rxdemo: onStart,subscribe.isDisposed():false
D/zdu_Rxdemo: onResume,subscribe.isDisposed():false
D/zdu_Rxdemo: 當(dāng)前發(fā)射數(shù)值:1
D/zdu_Rxdemo: 當(dāng)前接收數(shù)值:1
D/zdu_Rxdemo: 當(dāng)前發(fā)射數(shù)值:2
D/zdu_Rxdemo: 當(dāng)前接收數(shù)值:2
D/zdu_Rxdemo: onPause,subscribe.isDisposed():false
D/zdu_Rxdemo: 當(dāng)前發(fā)射數(shù)值:3
D/zdu_Rxdemo: 當(dāng)前接收數(shù)值:3
D/zdu_Rxdemo: onStop,subscribe.isDisposed():false
D/zdu_Rxdemo: onDestroy,subscribe.isDisposed():true

onDestroy方法中自動(dòng)解除訂閱了,而代碼中并沒有主動(dòng)去調(diào)用dispose方法衔肢,可見自動(dòng)解除訂閱生效了庄岖。

那如果在onStart方法訂閱的話,解除訂閱的生命周期又不一樣了:

D/zdu_Rxdemo: onStart,subscribe.isDisposed():false
D/zdu_Rxdemo: 當(dāng)前發(fā)射數(shù)值:0
D/zdu_Rxdemo: 當(dāng)前接收數(shù)值:0
D/zdu_Rxdemo: onResume,subscribe.isDisposed():false
D/zdu_Rxdemo: 當(dāng)前發(fā)射數(shù)值:1
D/zdu_Rxdemo: 當(dāng)前接收數(shù)值:1
D/zdu_Rxdemo: 當(dāng)前發(fā)射數(shù)值:2
D/zdu_Rxdemo: 當(dāng)前接收數(shù)值:2
D/zdu_Rxdemo: onPause,subscribe.isDisposed():false
D/zdu_Rxdemo: onStop,subscribe.isDisposed():true
D/zdu_Rxdemo: onDestroy,subscribe.isDisposed():true

onStop生命周期內(nèi)就被解除訂閱了角骤,那我們?cè)?code>onResume中訂閱的話隅忿,是不是就會(huì)在onPause中解除訂閱了呢?事實(shí)上確實(shí)是這樣的邦尊,日志就不打印了背桐,我們直接去看源碼實(shí)現(xiàn)。

剛剛我們調(diào)用的是RxAppCompatActivity的兩個(gè)方法:

    @NonNull
    @CheckResult
    public final <T> LifecycleTransformer<T> bindUntilEvent(@NonNull ActivityEvent event) {
        return RxLifecycle.bindUntilEvent(this.lifecycleSubject, event);
    }

    @NonNull
    @CheckResult
    public final <T> LifecycleTransformer<T> bindToLifecycle() {
        return RxLifecycleAndroid.bindActivity(this.lifecycleSubject);
    }

暫且現(xiàn)不管this.lifecycleSubject是什么蝉揍,我們繼續(xù)向下看源碼:

    public static <T, R> LifecycleTransformer<T> bindUntilEvent(@Nonnull final Observable<R> lifecycle,
                                                                @Nonnull final R event) {
        checkNotNull(lifecycle, "lifecycle == null");
        checkNotNull(event, "event == null");
        return bind(takeUntilEvent(lifecycle, event));
    }

    private static <R> Observable<R> takeUntilEvent(final Observable<R> lifecycle, final R event) {
        return lifecycle.filter(new Predicate<R>() {
            @Override
            public boolean test(R lifecycleEvent) throws Exception {
                return lifecycleEvent.equals(event);
            }
        });
    }

bindUntilEvent就很明確了链峭,底層用了filter過濾操作符,過濾了非指定生命周期又沾。但生命周期是怎么下發(fā)下來的呢弊仪?最后再做解釋。

public static <T> LifecycleTransformer<T> bindActivity(@NonNull Observable<ActivityEvent> lifecycle) {
        return RxLifecycle.bind(lifecycle, ACTIVITY_LIFECYCLE);
    }

bindToLifecycle的底層源碼最終跟takeUntilEvent的底層源碼一致捍掺,都指向了
RxLifecycle.bind方法撼短。我們就看下bind方法到底是執(zhí)行了什么?

先看bindUntilEvent

public static <T, R> LifecycleTransformer<T> bind(@Nonnull final Observable<R> lifecycle) {
        return new LifecycleTransformer<>(lifecycle);
    }

new了一個(gè)LifecycleTransformer挺勿,在看下內(nèi)部實(shí)現(xiàn):

public final class LifecycleTransformer<T> implements ObservableTransformer<T, T>,
                                                      FlowableTransformer<T, T>,
                                                      SingleTransformer<T, T>,
                                                      MaybeTransformer<T, T>,
                                                      CompletableTransformer
{
    final Observable<?> observable;

    LifecycleTransformer(Observable<?> observable) {
        checkNotNull(observable, "observable == null");
        this.observable = observable;
    }

    @Override
    public ObservableSource<T> apply(Observable<T> upstream) {
        return upstream.takeUntil(observable);
    }

    @Override
    public Publisher<T> apply(Flowable<T> upstream) {
        return upstream.takeUntil(observable.toFlowable(BackpressureStrategy.LATEST));
    }

    @Override
    public SingleSource<T> apply(Single<T> upstream) {
        return upstream.takeUntil(observable.firstOrError());
    }

    @Override
    public MaybeSource<T> apply(Maybe<T> upstream) {
        return upstream.takeUntil(observable.firstElement());
    }

    @Override
    public CompletableSource apply(Completable upstream) {
        return Completable.ambArray(upstream, observable.flatMapCompletable(Functions.CANCEL_COMPLETABLE));
    }
}

原來是一個(gè)實(shí)現(xiàn)ObservableTransformer等接口的類曲横,到這里也明白了為什么RxLifeCycle要用compose操作符,并且其內(nèi)部實(shí)現(xiàn)使用了takeUntil操作符不瓶,在符合條件后禾嫉,打斷上游鏈。這也說明了蚊丐,我們要在訂閱前一刻執(zhí)行這個(gè)自動(dòng)解除訂閱打斷上游鏈熙参,而對(duì)下游鏈沒有作用。

再看下bindToLifecyclebind實(shí)現(xiàn)麦备,與takeUntilEvent稍微有點(diǎn)不同的是它要自動(dòng)判斷生命周期:

@NonNull
    @CheckResult
    public static <T> LifecycleTransformer<T> bindActivity(@NonNull Observable<ActivityEvent> lifecycle) {
        return RxLifecycle.bind(lifecycle, ACTIVITY_LIFECYCLE);
    }

ACTIVITY_LIFECYCLE則是一個(gè)switch取值:

private static final Function<ActivityEvent, ActivityEvent> ACTIVITY_LIFECYCLE = new Function<ActivityEvent, ActivityEvent>() {
        public ActivityEvent apply(ActivityEvent lastEvent) throws Exception {
            switch(lastEvent) {
            case CREATE:
                return ActivityEvent.DESTROY;
            case START:
                return ActivityEvent.STOP;
            case RESUME:
                return ActivityEvent.PAUSE;
            case PAUSE:
                return ActivityEvent.STOP;
            case STOP:
                return ActivityEvent.DESTROY;
            case DESTROY:
                throw new OutsideLifecycleException("Cannot bind to Activity lifecycle when outside of it.");
            default:
                throw new UnsupportedOperationException("Binding to " + lastEvent + " not yet implemented");
            }
        }
    };

這就印證了我們之前的demo孽椰,在create的時(shí)候返回是ActivityEvent.DESTORY昭娩,對(duì)應(yīng)START返回的就是STOP生命周期等等。

public static <T, R> LifecycleTransformer<T> bind(@Nonnull Observable<R> lifecycle,  @Nonnull final Function<R, R> correspondingEvents) {
        checkNotNull(lifecycle, "lifecycle == null");
        checkNotNull(correspondingEvents, "correspondingEvents == null");
        return bind(takeUntilCorrespondingEvent(lifecycle.share(), correspondingEvents));
    }

    private static <R> Observable<Boolean> takeUntilCorrespondingEvent(final Observable<R> lifecycle,
                                                                       final Function<R, R> correspondingEvents) {
        return Observable.combineLatest(
            lifecycle.take(1).map(correspondingEvents),
            lifecycle.skip(1),
            new BiFunction<R, R, Boolean>() {
                @Override
                public Boolean apply(R bindUntilEvent, R lifecycleEvent) throws Exception {
                    return lifecycleEvent.equals(bindUntilEvent);
                }
            })
            .onErrorReturn(Functions.RESUME_FUNCTION)
            .filter(Functions.SHOULD_COMPLETE);
    }

這個(gè)takeUntilCorrespondingEvent就是生命周期判斷黍匾,內(nèi)部使用了combineLatest操作符栏渺,簡(jiǎn)單的說就是該操作符接收多個(gè)Observable以及一個(gè)函數(shù)作為參數(shù),并且函數(shù)的簽名為這些Observable發(fā)射的數(shù)據(jù)類型锐涯。當(dāng)以上的任意一個(gè)Observable發(fā)射數(shù)據(jù)之后磕诊,會(huì)去取其它Observable 最近一次發(fā)射的數(shù)據(jù),回調(diào)到函數(shù)當(dāng)中纹腌,但是該函數(shù)回調(diào)的前提是所有的Observable都至少發(fā)射過一個(gè)數(shù)據(jù)項(xiàng)霎终。

看不懂上面的沒有關(guān)系,takeUntilCorrespondingEvent的作用就是篩選過濾生命周期升薯,那么問題又來了莱褒,這個(gè)生命周期到底是哪發(fā)射來的呢?

其實(shí)是用了 BehaviorSubject覆劈。在特定條件下保礼,Subject既可以發(fā)送事件,也可以接收事件责语,而BehaviorSubject接收到訂閱前的最后一條數(shù)據(jù)和訂閱后的所有數(shù)據(jù)。BehaviorSubject在Activity的每個(gè)生命周期都發(fā)射了一個(gè)生命周期事件:

    @CallSuper
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        this.lifecycleSubject.onNext(ActivityEvent.CREATE);
    }

    @CallSuper
    protected void onStart() {
        super.onStart();
        this.lifecycleSubject.onNext(ActivityEvent.START);
    }
......

其他生命周期也類似目派,不再貼出來了坤候。如此,完整的RxLifeCycle源碼執(zhí)行分析到此為止企蹭。

結(jié)語(yǔ)

RxLifeCycle做為自動(dòng)解除綁定的一個(gè)三方庫(kù)白筹,源碼實(shí)現(xiàn)比較簡(jiǎn)單易讀,一定程度上可以幫助我們解決Rxjava內(nèi)存泄漏的問題谅摄,但不可否認(rèn)的說徒河,它也有弊端:

  • 基類需要繼承RxAppCompatActivity和RxFragment等,這也是最大的弊端送漠。這如今我們的Activity都已經(jīng)封裝好了一個(gè)完整的基類顽照,但要用RxLifeCycle,又需要換成這個(gè)闽寡,限制太大代兵。

  • 對(duì)應(yīng)MVP框架結(jié)構(gòu)來說,無法在P層使用爷狈,只能在V層才能使用植影,不符合MVP結(jié)構(gòu)。
    當(dāng)然也有更好的解決方式涎永,那就是下一篇要講的 Rxjava解除訂閱②:AutoDispose

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末思币,一起剝皮案震驚了整個(gè)濱河市鹿响,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌谷饿,老刑警劉巖抢野,帶你破解...
    沈念sama閱讀 218,284評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異各墨,居然都是意外死亡指孤,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,115評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門贬堵,熙熙樓的掌柜王于貴愁眉苦臉地迎上來恃轩,“玉大人,你說我怎么就攤上這事黎做〔骢耍” “怎么了?”我有些...
    開封第一講書人閱讀 164,614評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵蒸殿,是天一觀的道長(zhǎng)筷厘。 經(jīng)常有香客問我,道長(zhǎng)宏所,這世上最難降的妖魔是什么酥艳? 我笑而不...
    開封第一講書人閱讀 58,671評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮爬骤,結(jié)果婚禮上充石,老公的妹妹穿的比我還像新娘。我一直安慰自己霞玄,他們只是感情好骤铃,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,699評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著坷剧,像睡著了一般惰爬。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上惫企,一...
    開封第一講書人閱讀 51,562評(píng)論 1 305
  • 那天撕瞧,我揣著相機(jī)與錄音,去河邊找鬼雅任。 笑死风范,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的沪么。 我是一名探鬼主播硼婿,決...
    沈念sama閱讀 40,309評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼禽车!你這毒婦竟也來了寇漫?” 一聲冷哼從身側(cè)響起刊殉,我...
    開封第一講書人閱讀 39,223評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎州胳,沒想到半個(gè)月后记焊,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,668評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡栓撞,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,859評(píng)論 3 336
  • 正文 我和宋清朗相戀三年遍膜,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片瓤湘。...
    茶點(diǎn)故事閱讀 39,981評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡瓢颅,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出弛说,到底是詐尸還是另有隱情挽懦,我是刑警寧澤,帶...
    沈念sama閱讀 35,705評(píng)論 5 347
  • 正文 年R本政府宣布木人,位于F島的核電站信柿,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏醒第。R本人自食惡果不足惜渔嚷,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,310評(píng)論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望淘讥。 院中可真熱鬧圃伶,春花似錦、人聲如沸蒲列。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,904評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)蝗岖。三九已至,卻和暖如春榔至,著一層夾襖步出監(jiān)牢的瞬間抵赢,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,023評(píng)論 1 270
  • 我被黑心中介騙來泰國(guó)打工唧取, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留铅鲤,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,146評(píng)論 3 370
  • 正文 我出身青樓枫弟,卻偏偏與公主長(zhǎng)得像邢享,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子淡诗,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,933評(píng)論 2 355