##問題背景
前幾天寫了篇 MVP 入門級的文章,文章結尾有個疑問沒有解決,如何避免 Presenter 記憶體洩漏情況的發生?我檢視了一些資料後,發現這個問題可以轉成另外一個問題,如何保證 Presenter 與 Activity/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 沒有做對應的處理所造成的,那該怎麼辦呢?
##一些思考
上文說了,問題本質就是如何保證 Presenter 與 Activity/Fragment 的生命週期一致?其實方法有很多,比較成熟的有 MVPro 框架、Beam 框架,還有今天要說的比較有趣且巧妙的方法 Loader 方案。
對於 MVPro 框架,它的思路是:
既然要保證 Presenter 與 Activity 生命週期一致,那不如就把 Activity 作為 Presenter 層,而不是作為 View 層。
這就上升到更深層次的問題:Activity/Fragment 到底是 V 層還是 P 層?
網上各種說法都有,我個人覺得 P 層是 M 層和 V 層的溝通橋樑,而按照我們慣性的想法就是 Activity 是直觀呈現給使用者的,是與 View 有直接關聯的,包括一些展示、輸入、更新等等操作都是在 Activity 上完成(至少給我們的直觀感覺是這樣),那麼顯然 Activity 更合適在 V 層上。
雖然 MVPro 框架將 Activity 上所有的 View 操作都用一個抽象類來實現,但我仍然不覺得這是一個最優的解決方案(又批判了大神,再逃……)。
Android MVP框架MVPro的使用和原始碼分析 - 亓斌
而對於 Beam 框架,它的思路是這樣的:
Presenter 與 Activity 的繫結關係應由靜態類管理,而不是由 Activity 管理。當 Activity 意外重啟時 Presenter 不應重啟,只需在 Activity 重啟時,讓 Presenter 與 Activity 重新繫結,並根據資料恢復 Activity 狀態即可。而當 Activity 真正銷燬時,對應 Presenter 才應該跟隨銷燬。
這跟設定一個單例的 Application 有點類似,不管 Activity 怎麼變化,Application 都只有一個,所以可以通過這個 Application 來管理。不得不說,這個思路我還是比較能接受的,也是我能想出來的最簡單的方法。
但是回頭看一看 MVP 架構的核心思想(原文及圖來源):
將 Activity/Fragment 變成一個單純的 View ,負責展示資料並將各種事件分發給中間人,也就是 Presenter 。Presenter 會處理每一個事件,從 Model 層獲取或上傳資料,並將獲得的資料進行處理並讓 View 層展示。Presenter 與 Activity/Fragment 的通訊,是通過 Activity/Fragment 所繼承的 View 介面來間接完成的。
這就很顯而易見了,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():第一個引數是 Loader 的 Id,第二個引數可選,第三個引數為回撥的實現類,一般都為當前的 Activity/Fragment。
- restartLoader():其實一般通過 initLoader 都會監測是否存在指定 Id 的 Loader ,如果有就重啟一下,但是如果你不想要之前的資料了,就徹底重新一個新的 Loader 。
####2、LoaderManagerCallbacks
從名稱就可以看出,這是 LoaderManager 的回撥類,裡面有三個方法:
- onCreateLoader():例項化和返回新建給定 Id 的 Loader ;
- onLoadFinished():當一個建立好的 Loader 完成了 Load 過程,呼叫此函式;
- onLoaderReset():當一個建立好的 Loader 要被 Reset ,呼叫此函式,此時資料無效。
####3、Loader
a、生命週期
- active:活動狀態:
- started:啟動狀態;
- stopped:停止狀態,有可能再次啟動。
- inactive:非活動狀態:
- abandoned:廢棄狀態,廢棄後過段時間也會重置;
- reseted:重置狀態,表示該 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 能夠滿足這樣的需求呢?
- Loader 是 Android 框架中內部提供的;
- 每一個 Activity/Fragment 都可以持有自己的 Loader 物件的引用。
- Loader 在 Activity/Fragment 狀態改變時是不會被銷燬的,因為它可以自動重建;
- Loader 的生命週期是是由系統控制的;
- Loader 會在 Activity/Fragment 不再被使用後由系統自動回收;
- Loader 與 Activity/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 的實現了,有幾點要注意的:
- 仍然需要 Model 和 View 層的介面;
- 執行緒是迴圈的,是否迴圈通過標記判斷;
- 在 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 就是各種繼承 BasePresenter 的 Presenter。
- 記著 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) 得到一個管理者並新建一個 Id 為 0 的 Loader ,這裡我沒有用 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 也會發生對應的變化,圖中的變化就是停止迴圈載入;
- 當不 finish 掉 Activity 並再次返回時,Presenter 仍然可以繼續之前停止的位置開始載入,這就是 Loader 的作用。
上面兩點說明的問題一句話概括就是:
Presenter 的生命週期已經和 Activity 保持一致了,而且比 Activity 的生命週期還要長,因為它被 Loader 繫結了。
到此,文章開頭的疑問也就基本解決了。
##總結
回頭看了一下,實在沒想到寫了這麼多,有的地方還是囉嗦了,而且案例選擇的不太好,不過在原理上已經沒啥大問題了。
不過值得注意的是,用 Loader 方案去解決這種問題,並不是完美的,真正實踐起來還是有坑的地方,比如 Fragment 裡面比較難控制 Loader ,只是思路比較有趣,解決起來也相對比較容易,至少不影響 MVP 的架構,個人來說比較喜歡。
之前也跟 GithubApp 開發者 交流過一次,他的思路是使用一個管理者去管理 Rx 中所有的 Subscription 。要是生命週期變化,對應的 Presenter 就會變化,根據這些變化一些 Subscription 就會被退訂,也就防止了記憶體洩漏的情況。
不管怎麼說,還是實踐出真知。
###參考資料
深入原始碼解析Android中Loader、AsyncTaskLoader、CursorLoader、LoaderManager
Android應用Loaders全面詳解及原始碼淺析 - 工匠若水
###專案原始碼
個人部落格:www.iamxiarui.com 原文連結:http://www.iamxiarui.com/?p=903