響應式程式設計在Android 中的一些探索

舒大飛發表於2019-03-04

    響應式在前端領域已經變得十分流行,很多主流框架都採用響應式來進行頁面的展示重新整理。本文主要是探索一下響應式在移動端Android上的一些實踐,包括對響應式思想的理解,以及目前Android上實現響應式的一些手段,最後聊聊響應式在Android開發上的一些應用。如果你也在傳統的開發模式過程中隨著專案程式碼的增加以及業務邏輯複雜度的增加發現諸如大量的繁瑣回撥、不可控的記憶體洩漏、空指標等問題,希望本文的一些分享可以給你帶來一點點新的選擇。

本文主要包括以下幾部分:

  1. 響應式程式設計思想
  2. 響應式的實現手段:訂閱釋出模式、LiveData、RxJava
  3. 響應式的應用:MVVM、事件匯流排
  4. 總結

1.響應式程式設計思想

概念:響應式程式設計是一種通過非同步和資料流來構建事物關係的程式設計模型。

如何更加通俗易懂的理解?

其實我們的業務開發中本身蘊含著各種各樣的事物關係,以載入一個列表為例,列表資料為空時展示怎樣的介面,列表有資料時如何展示,資料載入失敗時怎麼展示,這就是列表資料與列表介面之間的事物關係。

而我們通常編碼模式,“人”在其中扮演了過重的角色,我們既要負責去取資料,取完資料還要負責把資料傳送給介面展示,操碎了心。某種意義上這是一種順序性思維的程式設計,我要做什麼,然後做什麼,最後做什麼,按部就班編寫就好了。具體如下圖:

傳統編碼模式

響應式程式設計則從一開始就明確構建了事物與事物之間的關係解脫了"人",之後一個事物發生變化另一個事物就自動響應。提前構建好事物與事物之間的關係,減輕"人"的角色。畫個圖,如下:

響應式程式設計

2.實現響應式的手段

上面說的響應式主要還是一種程式設計思想,而如何來實現這樣一種思想呢?當然訂閱釋出模式是基礎,而像RxJava、LiveData等的出現,讓響應式程式設計的實現手段變得更加的豐富。

一. 訂閱釋出模式

訂閱釋出模式是實現響應式的基礎,這種模式我們都很熟悉了,主要是通過把觀察者的回撥註冊進被觀察者來實現二者的訂閱關係,當被觀察者notify的時候,則所有的觀察就會自動響應。這種模式也實現了觀察者和被觀察者的解耦。

二.LiveData

LiveData是google釋出的lifecycle-aware components中的一個元件,除了能實現資料和View的繫結響應之外,它最大的特點就是具備生命週期感知功能,這使得他具備以下幾個優點:

  • 解決記憶體洩漏問題。由於LiveData會在Activity/Fragment等具有生命週期的lifecycleOwner onDestory的時候自動解綁,所以解決了可能存在的記憶體洩漏問題。之前我們為了避免這個問題,一般有註冊繫結的地方都要解綁,而LiveData利用生命週期感知功能解決了這一問題。
  • 解決常見的View空異常。我們通常在一個非同步任務回來後需要更新View,而此時頁面可能已經被回收,導致經常會出現View空異常,而LiveData由於具備生命週期感知功能,在介面可見的時候才會進行響應,如介面更新等,如果在介面不可見的時候發起notify,會等到介面可見的時候才進行響應更新。所以就很好的解決了空異常的問題。

LiveData的實現上可以說是訂閱釋出模式+生命週期感知,對於Activity/Fragment等LifecycleOwner來說LiveData是觀察者,監聽者生命週期,而同時LiveData又是被觀察者,我們通過觀察LiveData,實現資料和View的關係構建。

LiveData

LiveData是粘性的,這是你在使用前需要知道的,以免因為粘性造成一些問題,使用EventBus的時候我們知道有一種事件模式是粘性的,特點就是訊息可以在observer註冊之前傳送,當observer註冊時,依然可接收到之前傳送的這個訊息。而LiveData天生就是粘性的,下面會講解為什麼他是粘性的,以及如果在一些業務場景上不想要LiveData是粘性的該怎麼做。

LiveData的實現原理

單純的貼原始碼,分析原始碼可能比較枯燥,所以下面就儘量以丟擲問題,然後解答的方式來解析LiveData的原理。

1.LiveData是如何做到感知Activity/Fragment的生命週期?

lifecycle-aware compents的核心就是生命週期感知,要明白LiveData為什麼能感知生命週期,就要知道Google的這套生命週期感知背後的原理是什麼,下面是我基於之前lifeycycle這套東西剛出來時候對原始碼進行的一個分析總結(現在的最新程式碼可能和之前有點出入,但是原理上基本是一樣的):

首先Activity/Fragment是LifecycleOwner(26.1.0以上的support包中Activity已經預設實現了LifecycleOwner介面),內部都會有一個LifecycleRegistry存放生命週期State、Event等。而真正核心的操作是,每個Activity/Fragment在啟動時都會自動新增進來一個Headless Fragment(無介面的Fragment),由於新增進來的Fragment與Activity的生命週期是同步的,所以當Activity執行相應生命週期方法的時候,同步的也會執行Headless Fragment的生命週期方法,由於這個這個Headless Fragment對我們開發者來說是隱藏的,它會在執行自己生命週期方法的時候更新Activity的LifecycleRegistry裡的生命週期State、Event, 並且notifyStateChanged來通知監聽Activity生命週期的觀察者。這樣就到達了生命週期感知的功能,所以其實是一個隱藏的Headless Fragment來實現了監聽者能感知到Activity的生命週期。

基於這套原理,只要LiveData註冊了對Activity/Fragment的生命週期監聽,也就擁有了感知生命週期的能力。從LiveData的原始碼裡體現如下:

@MainThread
    public void observe(@NonNull LifecycleOwner owner, @NonNull Observer<T> observer) {
        ``````
        ``````
        owner.getLifecycle().addObserver(wrapper);//註冊對Activity/Fragment生命週期的監聽
    }
複製程式碼

下面附一張當時對Google lifecycle-aware原理進行原始碼分析隨手畫的圖:

Google lifecycle-aware原理

所以到這裡我們基本上已經知道了生命週期感知這套東西的原理,接下來我們就可以來看看LiveData的實現原理了,下我把LiveData的原始碼抽象為一張流程圖來展示,下面的其他問題都可以在這張圖中找到答案

響應式程式設計在Android 中的一些探索

可以看到,在LiveData所依附的Activity/Fragment生命週期發生改變或者通過setValue()改變LiveData資料的時候都會觸發notify,但是觸發後,真正要走到最終的響應(即我們註冊進去的onChanged()回撥)則中間要經歷很多判斷條件,這也是為什麼LiveData能具有自己那些特點的原因.

2.LiveData為什麼可以避免記憶體洩漏?

  通過上面,我們可以知道,當Activity/Fragment的生命週期發生改變時,LiveData中的監聽都會被回撥,所以避免記憶體洩漏就變得十分簡單,可以看上圖,當LiveData監聽到Activity onDestory時則removeObserve,使自己與觀察者自動解綁。這樣就避免了記憶體洩漏。 原始碼上體現如下:

@Override
        public void onStateChanged(LifecycleOwner source, Lifecycle.Event event) {
            if (mOwner.getLifecycle().getCurrentState() == DESTROYED) {
                removeObserver(mObserver);
                return;
            }
            activeStateChanged(shouldBeActive());
        }
複製程式碼

3.LiveData為什麼可以解決View空異常問題?

這個問題很簡單,看上圖,因為LiveData響應(比如更新介面操作View)只會在介面可見的時候,如果當前見面不可見,則會延遲到介面可見的時候再響應,所以自然就不會有View空異常的問題了。

那麼LiveData是如何實現:

  1. 只在介面可見的時候才響應的
  2. 如果當前介面不可見,則會延遲到介面可見的時候再響應

關於問題1,因為LiveData是能感知到生命週期的,所以在它回撥響應的時候會加一個額外的條件,就是當前的生命週期必須是可見狀態的,才會繼續執行響應,原始碼如下:

private void considerNotify(ObserverWrapper observer) {
        //如果介面不可見,則不進行響應
        if (!observer.mActive) {
            return;
        }
        if (!observer.shouldBeActive()) {
            observer.activeStateChanged(false);
            return;
        }
        //如果mVersion不大於mLastVersion,說明資料沒有發生變化,則不進行響應
        if (observer.mLastVersion >= mVersion) {
            return;
        }
        observer.mLastVersion = mVersion;
        //noinspection unchecked
        observer.mObserver.onChanged((T) mData);
    }
複製程式碼
@Override
  boolean shouldBeActive() {
       return mOwner.getLifecycle().getCurrentState().isAtLeast(STARTED);
   }
複製程式碼

關於問題2,在LiveData中有一個全域性變數mVersion,而每個observer中有一個變數mLastVersion。當我們每次setValue()修改一次LiveData的值的時候,全域性的mVersion就會+1,這樣mVersion就大於mLastVersion:

@MainThread
    protected void setValue(T value) {
        assertMainThread("setValue");
        mVersion++;
        mData = value;
        dispatchingValue(null);
    }
複製程式碼

而當介面重新可見的時候,只要判斷到mVersion大於mLastVersion,則就會進行響應重新整理View,響應後才會更新mLastVersion=mVersion。

4.LiveData為什麼是粘性的?

所謂粘性,也就是說訊息在訂閱之前釋出了,訂閱之後依然可以接受到這個訊息,像EventBus實現粘性的原理是,把釋出的粘性事件暫時存在全域性的集合裡,之後當發生訂閱的那一刻,遍歷集合,將事件拿出來執行。

而LiveData之所以本身就是粘性的,結合上面的原理圖我們來分析一下,比如有一個資料(LiveData)在A頁面setValue()之後,則該資料(LiveData)中的全域性mVersion+1,也就標誌著資料版本改變,然後再從A頁面開啟B頁面,在B頁面中開始訂閱該LiveData,由於剛訂閱的時候內部的資料版本都是從-1開始,此時內部的資料版本就和該LiveData全域性的資料版本mVersion不一致,根據上面的原理圖,B頁面開啟的時候生命週期方法一執行,則會進行notify,此時又同時滿足頁面是從不可見變為可見、資料版本不一致等條件,所以一進B頁面,B頁面的訂閱就會被響應一次。這就是所謂的粘性,A頁面在發訊息的時候B頁面是還沒建立還沒訂閱該資料的,但是一進入B頁面一訂閱,之前在A中發的訊息就會被響應。

那麼有些業務場景我們是不想要這種粘性的,我們希望只有當我們訂閱了該資料之後,該資料的改變才通知我們,通過上面的分析,這一點應該還是比較好辦到的,只要我們訂閱的時候將全域性的mVersion同步到內部的資料版本,這樣訂閱時候就不會出現內部資料版本與全域性的mVersion不一致,也就去除了粘性。我這裡自定義了一個可以控制是否需要粘性的LiveData。

具體程式碼見: CustomStickyLiveData

三. RxJava

RxJava是可以實現響應式程式設計的另外一個手段,Rxjava也是熱度非常高的一個開源庫,當然我們都知道RxJava一個是有訂閱釋出模式解耦的優點,還有其執行緒模型、鏈式寫法都是其優點。

當然我個人認為不管是鏈式寫法,還是執行緒模型,異或是解決回撥問題都談不上是RxJava的核心優點,有很多人引入RxJava後專案裡只是利用RxJava方便的執行緒模型來做簡單的非同步任務,其實如果只是做非同步任務,有非常多種的方式可以替代RxJava。鏈式寫法的話就更只是編碼上的糖果了。如果在沒有正確的理解RxJava的核心優勢基礎上在程式碼裡對RxJava進行跟風式的濫用,很多時候你會發現,程式碼並沒有變簡潔,甚至有時候很簡單的事情被搞的變複雜了。

我所理解的RxJava的核心優勢應該是它可以對複雜邏輯進行拆分成為一個一個的Observable後,RxJava的各種操作符予這些解耦的Observable能夠合理的進行再組織的能力,並且它給予了你足夠豐富的再組織能力。這種分拆再組織的能力是十分強大的,只有運用好RxJava這種強大的能力,才能真正意義上使你原來非常複雜的揉在一團的邏輯程式碼變得清晰、簡潔,本質上是因為RxJava給你提供了這種強大方便的組織能力,我覺得有點像一種程式設計模式,你可以放心的將複雜的邏輯拆塊,最後RxJava給你提供了豐富的組織、變換、串聯、控制這些塊的能力,只有這個時候你才會真正覺得這是個好東西,而不應該是跟風使用,但是心裡也說不清楚為什麼要使用。

回到文章的主題響應式,Rxjava就不繼續展開了,這篇只說關於文章主題響應式的:

看一下RxJava基本使用的時候一般如下:

Observable.create(new ObservableOnSubscribe<String>() {
            @Override
            public void subscribe(ObservableEmitter<String> e) throws Exception {

                e.onNext("通知觀察者");

            }
        }).subscribe(new io.reactivex.Observer<String>() {
            @Override
            public void onSubscribe(Disposable d) {

            }

            @Override
            public void onNext(String s) {
                Log.i("tag", "接收到訊息" + s);
            }

            @Override
            public void onError(Throwable e) {

            }

            @Override
            public void onComplete() {

            }
        });
複製程式碼

可以看到被觀察者、觀察者,然後通過subscribe()把他們進行繫結。當然可能不看原始碼的話唯一有一點疑惑的地方是: 這裡notify觀察者的方式是通過e.onNext(),然後就會觸發Observer中的onNext。其實如果notify觀察者的方式寫成observer.onNext(),就非常明瞭了。從原始碼上看e.onNext()裡最後呼叫到的就是observer.onNext(),所以就是普通的訂閱釋出模式。

到這裡基本上可以知道,訂閱釋出模式是基礎,LiveData和RxJava是基於訂閱釋出模式去實現自己不同的特點,比如LiveData的生命週期感知能力,RxJava的話自身具備的能力就更雜更強大一點。下面來看看響應式的應用,利用這些響應式手段,我們可以來做些什麼,主要舉兩個例子。

3.響應式的應用

一.MVVM

MVC、MVP、MVVM三者並不是說哪種模式一定優於其他模式,三者有自己各自的優缺點,主要看具體的專案和具體場景下哪種更適合你的需求,可以更加高效的提升你的專案程式碼質量和開發效率。

我下面所闡述的MVVM的優點和缺點,都是基於利用Google lifecycle-aware Components的LiveData+ViewModel來實現MVVM的基礎上來說的。當然這些優缺點都是我基於我們專案中應用實踐以及個人的一些看法,鞋適不適合只有腳知道,所以還是要結合自己的實際場景。

1.MVVM優點

目前我們產線的專案中佔比最大的還是MVP,最開始說了,其實使用MVP在解決程式碼解耦的基礎上,我們寫起程式碼通常是順序性思維,比較流暢,後期去維護以及程式碼閱讀上也相對流暢,同時在實際開發中它也引起了幾個主要的問題:

  1. 記憶體洩漏。由於Presenter裡持有了Activity物件,所以當Presenter中執行了非同步耗時操作時,有時候會引起Activity的記憶體洩漏。

    解決的方案: 一個是可以將Presenter對Activity的引用設定為軟引用,還有一個就是去管理你的非同步耗時任務,當Activity退出時保證他們被取消掉。

  2. View空指標異常。有的時候由於各種原因,Activity已經被回收了,而此時Presenter中要更新View的話經常就會引起view空異常問題。

    解決方案: 當然最簡單的解決方案就是在Presenter中每次要回撥更新介面的時候都判斷下View(Activity)是否為空,但是這種方式顯然太過煩瑣可無法避免疏漏,所以我們可以利用動態代理來實現代理每個更新介面的方法,自動實現在每個更新介面方法之前都判斷一下view是否為空。這樣之後我們就可以大膽的寫程式碼而不會出現view空異常。

  3. 大量繁瑣的回撥。不知道當頁面足夠複雜的時候你是否也體會過Presenter中大量的回撥介面,有時候這種回撥多了以後,總感覺這種方式來更新介面不是非常優雅。

上面說了幾個MVP的缺點,以及為了解決這些缺點,你可以做的一些事。當然大量繁瑣的回撥這個缺點暫時沒有很好的解決方案。

利用LiveData來實現MVVM,剛好能解決以上說的這幾個問題,上面說了LiveData的優點就是能解決記憶體洩漏和View空異常,所以不用做任何額外的事,MVP的前兩個問題就解決了。而第三個問題,由於在MVVM中ViewModel(相當於MVP中Presenter)並不持有view的引用,而是隻處理資料邏輯,所以不存在大量繁瑣回撥的問題,只要在Activity中構建好資料與介面的關係,利用LiveData來繫結資料與介面的響應就可以了,之後只要ViewMoedl中資料發生變化,則響應的介面就會跟著響應改變。

所以相對於MVP來說,利用LiveData來實現的這套MVVM,不僅能解決上面說的這些問題,而且使得資料與介面的解耦更加徹底,ViewModel中只負責資料的邏輯處理,所以做單元測試也十分方便。只要在Activity中構建好資料與介面響應的關係即可。

2.MVVM缺點

  當然在我看來MVVM也有自己的缺點,通過全篇對響應式的探討,應該可以知道對於響應式來說,最重要的就是關係的構建,其實對於MVVM來說一切看起來都很美好,但是如果涉及到的頁面邏輯足夠複雜的時候,你是否依然能夠建立清晰的關係,是否能夠保證構建的關係一定可靠,就顯得非常重要,一但你構建的關係有問題,則就會引起bug, 而且這種問題bug的排查似乎沒有順序性思維程式碼的那麼直接。對於程式碼的閱讀和維護上,其他人是否能正確的理解和用好你所構建的關係,可能也是一個問題。

最後,貼上一張利用LiveData+Viewmodel實現MVVM的架構圖,也就是Google Architecture Components:

MVVM

二.事件匯流排

說到事件匯流排,我們可能第一個想到的就是EventBus,他是簡單的利用了訂閱釋出模式來實現的,上面已經說了,幾種實現響應式的手段的核心都是很相似的,所以自然用RxJava、LiveData也能非常簡單的實現一個事件匯流排。

  EventBus是基於基礎的訂閱釋出模式去實現的,基本原理的話,就是在register的時候,解析所有的註解(早期是利用反射解析,後來為了提高效能,利用編譯時註解生成程式碼),然後將觀察者註冊進全域性map,之後在其他地方post一個訊息就可以利用tag在全域性集合裡找到所有對應的觀察者,然後notify就可以了。

  而RxJava和LiveData的核心基礎就是訂閱釋出模式,加上他們自己的優勢特點,如LiveData的生命週期感知功能,所以利用他們產生的RxBus、LiveDataBus都只要很少的程式碼就能實現事件匯流排的功能,因為LiveData具有避免記憶體洩漏的優點,所以比EventBus和RxBus還多一個優點就是不用解綁。

4.總結

    這是我對響應式程式設計在Android上一些個人見解,包括對響應式程式設計思想的理解,以及對在Android上響應式程式設計實現手段原理的一些解讀,最後是對這些工具手段的具體應用。目前市面上各種響應式層出不窮,所以我們有必要去理解他,吸收他的優點。本文的重點偏向文章的主題響應式,文中的很多點比如lifecycle-aware、RxJava等展開講的話都能有很大的篇幅,之後有時間可以歸納成文。

相關文章