Android:聊聊 MVP 中 Presenter 的生命週期

iamxiarui_發表於2018-07-02

##問題背景

前幾天寫了篇 MVP 入門級的文章,文章結尾有個疑問沒有解決,如何避免 Presenter 記憶體洩漏情況的發生?我檢視了一些資料後,發現這個問題可以轉成另外一個問題,如何保證 PresenterActivity/Fragment 的生命週期一致?

注:為了方便起見,文中 View 層直接用 Activity/Fragment 代替,有時只說 Activity

我們先來看一個小案例,看看具體問題是什麼樣。

案例本身很簡單,就是模擬耗時載入資料,一點技術含量都沒有,只不過是標準的 MVP 模式。

[圖片上傳失敗...(image-4c30c2-1530535722013)]

原始碼暫時就不貼了,現象為主,什麼現象呢?首先我在載入資料時,也就是在 Presenter 那裡故意延時了3秒鐘,在這3秒鐘內,我們對 Activity 做一些邪惡的事情。

1、跳轉到另一個 Activity,但並不 Finish 自身:

[圖片上傳失敗...(image-16c13c-1530535722013)]

2、跳轉到另一個 Activity,同時 Finish 自身:

[圖片上傳失敗...(image-f42772-1530535722013)]

仔細對比可以看出幾個問題:

  • Presenter 所在 Activity 已經不可見了(onStop),Presenter 仍然在作用;
  • Presenter 所在 Activity 已經被殺死了(onDestroy),Presenter 仍然在作用;
  • 頁面不可見的時候,竟然沒有報空指標,且返回時仍然能看見,顯示正常。

這些現象都是因為 Activity 的生命週期變化後 Presenter 沒有做對應的處理所造成的,那該怎麼辦呢?

##一些思考

上文說了,問題本質就是如何保證 PresenterActivity/Fragment 的生命週期一致?其實方法有很多,比較成熟的有 MVPro 框架、Beam 框架,還有今天要說的比較有趣且巧妙的方法 Loader 方案。

對於 MVPro 框架,它的思路是:

既然要保證 PresenterActivity 生命週期一致,那不如就把 Activity 作為 Presenter 層,而不是作為 View 層。

這就上升到更深層次的問題:Activity/Fragment 到底是 V 層還是 P 層?

網上各種說法都有,我個人覺得 P 層是 M 層和 V 層的溝通橋樑,而按照我們慣性的想法就是 Activity 是直觀呈現給使用者的,是與 View 有直接關聯的,包括一些展示、輸入、更新等等操作都是在 Activity 上完成(至少給我們的直觀感覺是這樣),那麼顯然 Activity 更合適在 V 層上。

雖然 MVPro 框架將 Activity 上所有的 View 操作都用一個抽象類來實現,但我仍然不覺得這是一個最優的解決方案(又批判了大神,再逃……)。

Android MVP框架MVPro的使用和原始碼分析 - 亓斌

而對於 Beam 框架,它的思路是這樣的:

PresenterActivity 的繫結關係應由靜態類管理,而不是由 Activity 管理。當 Activity 意外重啟時 Presenter 不應重啟,只需在 Activity 重啟時,讓 PresenterActivity 重新繫結,並根據資料恢復 Activity 狀態即可。而當 Activity 真正銷燬時,對應 Presenter 才應該跟隨銷燬。

這跟設定一個單例的 Application 有點類似,不管 Activity 怎麼變化,Application 都只有一個,所以可以通過這個 Application 來管理。不得不說,這個思路我還是比較能接受的,也是我能想出來的最簡單的方法。

Android應用中MVP最佳實踐 - Jude95

但是回頭看一看 MVP 架構的核心思想(原文及圖來源):

Activity/Fragment 變成一個單純的 View ,負責展示資料並將各種事件分發給中間人,也就是 PresenterPresenter 會處理每一個事件,從 Model 層獲取或上傳資料,並將獲得的資料進行處理並讓 View 層展示。PresenterActivity/Fragment 的通訊,是通過 Activity/Fragment 所繼承的 View 介面來間接完成的。

MVP

這就很顯而易見了,Activity/Fragment 就是 View 層,所以我們仍然需要一個更好的方法來解決這個問題,讓 Presenter 不由 Activity/Fragment 的生命週期來管理。

由於 Presenter 是一箇中間的主持者,所以生命週期一定長於或者說至少不短於 Activity/Fragment ,所以這就有兩點要求:

  • Presenter 生命週期獨立;
  • Presenter 生命週期長於 Activity/Fragment

但我們知道,Presenter 只是我們自己定義的一層中間主持者物件,如果要實現需求還是要繫結一個已有的東西,那 Android 裡有什麼東西的生命週期是獨立且長於 Activity/Fragment 呢?

我首先想到的是 Application ,它的生命週期是獨立且大於等於 Activity 的,完全滿足需求,而且一般我們做專案的時候都會有一個 Application 類,充當一個全域性管理者的角色。但這跟上面的 Beam 框架有點類似,所以暫時不說這個。

那有沒有更好的呢?我沒想到,但是別人想到了,也就是今天說的 Loader

##初識 Loader

關於 Loader 類,其實之前我不太瞭解,只是在《 Andrid 開發藝術探索》裡面見過這個類,當時說的是非同步。因為非同步這東西現在很多框架都能很好的實現,所以並沒在意,但通過幾天的學習,覺得這個類還是很 NB 的。

因為文章重點不是這個類,我就簡單說一下它的作用及特點,不深入討論具體的使用方式,有興趣可以點選文末相關連結學習。

###Loader 是啥

一句話概括:

Android 提供的一種支援 Activity/Fragment 的、非同步的、帶有監聽功能的、能自動重連線的載入器。

哦喲,真的很 NB 的樣子,來一個一個看。

1、支援 Activity/Fragment

這個意思是不管在 Activity 中還是 Fragment 中,它都能通過 getLoaderManager().initLoader(0, null, this) 來建立自身的管理類。而我們的 View 層的實現剛好是 Activity/Fragment

2、非同步

這個詞在 Android 中不要太熟悉,在 Loader 的實現中還有一個抽象子類 AsyncTaskLoader 。它的內部提供一個 AsyncTask 進行非同步的一些耗時操作。這就很厲害了,因為這個問題的源頭就是 Presenter 進行了耗時操作。

3、帶有監聽功能

這個意思其實就是能夠及時響應一個資料的變化並實時更新 UI ,有點類似 ContentObserver ,充當一個觀察者的角色。

4、能自動重新連線

我覺得這才是重磅功能,它能夠在 Activity/Fragment 發生 Configuration Change 的時候,自動重新連線。比如 Activity 突然橫屏了,生命週期發生了巨大變化,這個時候它能夠自己處理這些變化,並自動重新連線自身。

說到這裡,Loader 的強大之處我們已經能夠窺見一丟丟了。

###Loader 的生命週期

前面就一直強調生命週期的問題,既然 Loader 滿足需求,那就來看看它的生命週期。一般來說,一個完成的 Loader 機制需要三個東西,三者關係如下圖所示:

[圖片上傳失敗...(image-1c4b5a-1530535722013)]

下面依次來看:

####1、LoaderManager

顧名思義,它是 Loader 的管理者:

  • initLoader():第一個引數是 LoaderId,第二個引數可選,第三個引數為回撥的實現類,一般都為當前的 Activity/Fragment
  • restartLoader():其實一般通過 initLoader 都會監測是否存在指定 IdLoader ,如果有就重啟一下,但是如果你不想要之前的資料了,就徹底重新一個新的 Loader

####2、LoaderManagerCallbacks

從名稱就可以看出,這是 LoaderManager 的回撥類,裡面有三個方法:

  • onCreateLoader():例項化和返回新建給定 IdLoader ;
  • onLoadFinished():當一個建立好的 Loader 完成了 Load 過程,呼叫此函式;
  • onLoaderReset():當一個建立好的 Loader 要被 Reset ,呼叫此函式,此時資料無效。

####3、Loader

a、生命週期

  • active:活動狀態:
    • started:啟動狀態;
    • stopped:停止狀態,有可能再次啟動。
  • inactive:非活動狀態:
    • abandoned:廢棄狀態,廢棄後過段時間也會重置;
    • reseted:重置狀態,表示該 Loader 已經完全被銷燬重用了。

Loader 生命週期圖

b、onStartLoading

如果 Activity 執行 onStart 方法,Loader 會執行此方法,此時 Loader 處於 started 狀態下,Loader 應該監聽資料來源的變化,並將變化的新資料傳送給客戶端。 這個時候有兩種情況:

  • 已經存在 Loader 所要載入物件例項,應該呼叫 deliverResult() 方法,觸發 onLoadFinished() 回撥的執行,從而客戶端可以從該回撥中輕鬆獲取資料。
  • 如果沒有的話,則先觸發 onCreateLoader() 回撥建立 Loader ,再呼叫 forceLoad() 方法,去促使它去載入,載入後再呼叫 deliverResult() 方法,回撥 onLoadFinished()

c、onStopLoading

Activity/Fragment 執行 onStop() 方法時,Loader 會呼叫此方法,此時 Loader 處於 stopped 狀態下。而且當 Activity/Fragment 處於 stopped 狀態時,所有的 Loader 也會被置於 stopped 狀態。

此時應該繼續監聽資料的變化,但是如果資料有變化應該先存起來,等重新 start 的時候再傳送給客戶端更新 UI 。

d、onReset

abandoned 狀態暫時略過,來看 onReset 這個方法。它會在 Activity/Fragment 銷燬時呼叫(主動呼叫 destroyLoader() 也可)。觸發 onLoaderReset() 回撥,並重新建立 Loader ,後續步驟類似 onStartLoading()

回頭看看上文會發現它的第四個特點跟它的生命週期密切相關。也就是說,它不管 Activity/Fragment 怎麼變化,它自己過它自己的。

###為什麼選 Loader

通過上文對 Loader 的相關了解,現在來總結一下,為什麼 Loader 能夠滿足這樣的需求呢?

  • LoaderAndroid 框架中內部提供的;
  • 每一個 Activity/Fragment 都可以持有自己的 Loader 物件的引用。
  • LoaderActivity/Fragment 狀態改變時是不會被銷燬的,因為它可以自動重建;
  • Loader 的生命週期是是由系統控制的;
  • Loader 會在 Activity/Fragment 不再被使用後由系統自動回收;
  • LoaderActivity/Fragment 的生命週期繫結,事件自身就能分發;

##專案改造

###先分析

好了,花了很長的篇幅去簡單介紹一下 Loader 。現在回到本質問題上,如何利用 Loader 的相關特性去解決 Presenter 的生命週期問題呢?一句話概括:

Loader 的生命週期內,繫結其所在的 Activity/Fragment 所對應的 Presenter

這就能讓 Presenter 獨立於 Activity/Fragment 的生命週期之外,這樣就不用擔心它們生命週期變化所帶來的一系列問題。

###再動手

現在我們就要對本文開頭的那個小案例進行修改了,不過有一點說明:

之前的例子不太好,因為現在 Android 中子執行緒不能手動 stop ,所以沒法演示 Activity 銷燬,Presenter 就同步銷燬的 案例,所以我將耗時部分加了迴圈,根據標誌位,判斷是否迴圈載入。

好,現在一項一項來:

####Bean

資料 Bean 類,太簡單,不說:

public class PersonBean {
    private String name;
    private String age;
	// ...省略
}
複製程式碼

####Model Interface

仍然是為了演示架構強行抽取的,沒有實質性意義的方法:

public interface IPersonModel {
    //載入Person資訊
    ArrayList<PersonBean> loadPersonInfo();
}
複製程式碼

####Model

實現 Model Interface ,這裡是模擬資料:

public class PersonModel implements IPersonModel {

    //存一下Person的資訊
    private ArrayList<PersonBean> personList = new ArrayList<>();
    
    /**
     * 載入Person資訊
     *
     * @return 返回資訊集合
     */
    @Override
    public ArrayList<PersonBean> loadPersonInfo() {
        personList.add(initPerson());
        return personList;
    }

    private PersonBean initPerson() {
        PersonBean personBean = new PersonBean();
        personBean.setName("張三");
        //...省略
        return personBean;
    }
}
複製程式碼

####View Interface

View 層必須的 Interface ,這裡也就一個方法:

public interface IPersonView {
    //更新UI
    void updateUI(ArrayList<PersonBean> personList);
}
複製程式碼

####Base Presenter

這裡因為我們需要跟 Activity 生命週期掛鉤,所以抽取一個 Presenter 的基類:

public interface BasePresenter<V> {
    void onViewAttached(V view);
    void onViewDetached();
    void onDestroyed();
}
複製程式碼

####Presenter

這裡就是重要的 Presenter 的實現了,有幾點要注意的:

  • 仍然需要 ModelView 層的介面;
  • 執行緒是迴圈的,是否迴圈通過標記判斷;
  • onViewAttached、onViewDetached、onDestroyed 中改變迴圈標記。
	public class PersonPresenter implements BasePresenter {
	
	    private IPersonModel mPersonModel;  //Model介面
	    private IPersonView mPersonView;    //View介面
	
	    private Handler mHandler = new Handler();   //模擬耗時用的 沒實質性作用
	    private boolean isLoad = true;              //迴圈載入標誌
	
	    public PersonPresenter(IPersonView mPersonView) {
	        mPersonModel = new PersonModel();
	        this.mPersonView = mPersonView;
	    }
	
	    public void updateUIByLocal() {
	        //Model層處理
	        final ArrayList<PersonBean> personList = mPersonModel.loadPersonInfo();
	        new Thread(new Runnable() {
	            @Override
	            public void run() {
	                while (isLoad) {
	                    //模擬1s耗時
	                    try {
	                        Thread.sleep(1000);
	                    } catch (InterruptedException e) {
	                        e.printStackTrace();
	                    }
	                    //執行在 Main 執行緒
	                    mHandler.post(new Runnable() {
	                        @Override
	                        public void run() {
	                            //View層更新
	                            mPersonView.updateUI(personList);
	                        }
	                    });
	                }
	            }
	        }).start();
	    }
	
	    public void onViewAttached(Object view) {
	        this.isLoad = true;
	        updateUIByLocal();
	    }
	
	    public void onViewDetached() {
	        this.isLoad = false;
	    }
	
	    public void onDestroyed() {
	        this.isLoad = false;
	    }
	}
複製程式碼

####Loader

現在來看 Loader 怎麼寫,先看原始碼:

public class PresenterLoader<T extends BasePresenter> extends Loader<T> {

    private PersonPresenter presenter;
    private PresenterFactory factory;

    public PresenterLoader(Context context,PresenterFactory factory) {
        super(context);
        this.factory = factory;
    }

    @Override
    protected void onStartLoading() {

        // 如果已經有Presenter例項那就直接返回
        if (presenter != null) {
            deliverResult((T) presenter);
            return;
        }

        // 如果沒有 就促使載入
        forceLoad();
    }

    @Override
    protected void onForceLoad() {
        // 例項化 Presenter
        presenter = factory.create();
        // 返回 Presenter
        deliverResult((T) presenter);
    }

    @Override
    protected void onReset() {
        presenter.onDestroyed();
        presenter = null;
    }
}
複製程式碼
  • 這裡只繼承了最簡單的 Loader 類,T 就是各種繼承 BasePresenterPresenter
  • 記著 Loader 的生命週期與 Presenter 生命週期一致,所以 Loader 開啟,那就要載入 Presenter。如果 Loader 重啟,那麼 Presenter 就要銷燬。
  • 這裡有個 PresenterFactory 類,就是為了建立各種 Presenter,具體看下面。

####PresenterFactory

這是 Presenter 的工廠類,為了建立各種 Presenter,先看程式碼怎麼寫的:

public class PresenterFactory{

    private IPersonView mPersonView;

    public PresenterFactory(IPersonView mPersonView) {
        this.mPersonView = mPersonView;
    }

    public PersonPresenter create() {
        return new PersonPresenter(mPersonView);
    }
}
複製程式碼

可以看到很簡單,就是 new 一個對應的 Presenter 出來,這裡我偷懶了,最好抽取一個介面出來:

public interface PresenterFactory<T extends Presenter> {
	T create();
}   
複製程式碼

####View

好了,那麼最後就是 View 層的實現類 Activity 了,仍然先看程式碼:

public class MainActivity extends AppCompatActivity implements IPersonView, LoaderManager.LoaderCallbacks<PersonPresenter>, View.OnClickListener {

    /*===== 控制相關 =====*/
    private int i = 0;
    private Toast mToast;
    private PersonPresenter mPersonPresenter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        initView();
        initListener();
        initData();
    }

    //...省略

    private void initData() {
        //得到一個Loader管理者,並建立一個Loader
        getLoaderManager().initLoader(0, null, this);
    }

    public void onClick(View view) {
        switch (view.getId()) {
            case R.id.bt_main_load:
                mPersonPresenter.updateUIByLocal();
                break;
            case R.id.bt_main_goto:
                gotoOther(fsvMainFinish.ismIsOpen());
                break;
        }
    }

    private void gotoOther(boolean isFinish) {
        startActivity(new Intent(this, OtherActivity.class));
        if (isFinish) {
            finish();
        }
    }

    @Override
    protected void onStart() {
        super.onStart();
        mPersonPresenter.onViewAttached(this);
    }

    @Override
    protected void onStop() {
        super.onStop();
        mPersonPresenter.onViewDetached();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        mPersonPresenter.onDestroyed();
    }

    /**
     * View 介面方法 更新UI
     *
     * @param personList 使用者集合
     */
    @Override
    public void updateUI(ArrayList<PersonBean> personList) {
        PersonBean personBean = personList.get(0);
        tvMainName.setText("姓名:" + personBean.getName());
        //...省略
        showToast("第 " + i + " 次載入");
        i++;
    }

    /*========== Loader 的回撥方法 ==========*/
    @Override
    public Loader<PersonPresenter> onCreateLoader(int id, Bundle args) {
        //建立
        return new PresenterLoader<>(this, new PresenterFactory(this));
    }

    @Override
    public void onLoadFinished(Loader<PersonPresenter> loader, PersonPresenter presenter) {
        //完成載入
        this.mPersonPresenter = presenter;
    }

    @Override
    public void onLoaderReset(Loader<PersonPresenter> loader) {
        //銷燬
        this.mPersonPresenter = null;
    }
}
複製程式碼

程式碼中的核心有這幾個部分:

  • 分別實現了 View 層介面和 LoaderManager.LoaderCallbacks 介面,因為前文已經說過 Loader 需要回撥;
  • 通過 getLoaderManager().initLoader(0, null, this) 得到一個管理者並新建一個 Id0Loader ,這裡我沒有用 V4 包,所以沒有 support ;
  • Activity 的每個生命週期方法中,呼叫 Presenter 的相關方法;
  • 實現 LoaderCallbacks 介面的相關回撥方法。

看到這裡,大家可能有疑問,從程式碼中看 Presenter 的相關方法確實已經跟 Activity 的生命週期連在一起了,但為什麼說是繫結了 Loader 呢?原因如下:

  • Presenter 確實和 Loader 繫結了,因為 Presenter 隨著 Loader 的建立/銷燬而建立/銷燬,並非是 Activity
  • Activity 生命週期中 Presenter 對應的方法並不意味著它和 Presenter 繫結,只是生命週期發生改變需要 Presenter 做出一些變化而已;
  • 這種做法其實也可以說是:利用 Loader 延長了 Presenter 的生命週期。

###看結果

現在我們看一下結果:

1、跳轉到另一個 Activity,但並不 Finish 自身:

[圖片上傳失敗...(image-9274bc-1530535722013)]

2、跳轉到另一個 Activity,同時 finish() 自身:

[圖片上傳失敗...(image-92f4bf-1530535722013)]

從圖中我們可以看到兩個現象:

  • Presenter 所在 Activity 生命週期發生變化時,Presenter 也會發生對應的變化,圖中的變化就是停止迴圈載入;
  • 當不 finishActivity 並再次返回時,Presenter 仍然可以繼續之前停止的位置開始載入,這就是 Loader 的作用。

上面兩點說明的問題一句話概括就是:

Presenter 的生命週期已經和 Activity 保持一致了,而且比 Activity 的生命週期還要長,因為它被 Loader 繫結了。

到此,文章開頭的疑問也就基本解決了。

##總結

回頭看了一下,實在沒想到寫了這麼多,有的地方還是囉嗦了,而且案例選擇的不太好,不過在原理上已經沒啥大問題了。

不過值得注意的是,用 Loader 方案去解決這種問題,並不是完美的,真正實踐起來還是有坑的地方,比如 Fragment 裡面比較難控制 Loader ,只是思路比較有趣,解決起來也相對比較容易,至少不影響 MVP 的架構,個人來說比較喜歡。

之前也跟 GithubApp 開發者 交流過一次,他的思路是使用一個管理者去管理 Rx 中所有的 Subscription 。要是生命週期變化,對應的 Presenter 就會變化,根據這些變化一些 Subscription 就會被退訂,也就防止了記憶體洩漏的情況。

不管怎麼說,還是實踐出真知。

###參考資料

通過Loader延長Presenter生命週期

深入原始碼解析Android中Loader、AsyncTaskLoader、CursorLoader、LoaderManager

Android應用Loaders全面詳解及原始碼淺析 - 工匠若水

###專案原始碼

LoaderMVPDemo - IamXiaRui


個人部落格:www.iamxiarui.com 原文連結:http://www.iamxiarui.com/?p=903

相關文章