一、概述
在前面的一篇文章中,我們分析了LoaderManager
的實現,裡面涉及到了很多的細節問題,我們並不需要刻意地記住每個流程,之所以需要分析,以後在使用的過程中,如果遇到問題了,再去檢視相關的原始碼就好了。
對於Loader
框架的理解,我認為掌握以下四個方面的東西就可以了:
- 對
LoaderManager
的實現原理有一個大概的印象。 LoaderManager
的三個關鍵方法initLoader/restartLoader/destroyLoader
的使用場景。- 學會自定義
Loader
來實現資料的非同步載入。 - 總結一些
App
開發中常用的場景。
第一點可以參考前面的這篇文章:
今天這篇,我們將專注於分析第二點:initLoader/restartLoader
的區別。
二、基本概念的區別
首先,我們回顧一下,對於init/restart
的定義:
-
initLoader
-
通過呼叫這個方法來初始化一個特定
ID
的Loader
,如果當前已經有一個和這個ID
關聯的Loader
,那麼並不會去回撥onCreateLoader
來通知使用者傳入一個新的Loader
例項替代那個舊的例項,僅僅是替代Callback
,也就是說,上面的Bundle
引數被丟棄了;而假如不存在一個關聯的例項,那麼會進行初始化,並啟動它。 -
這個方法通常是在
Activity/Fragment
的初始化階段呼叫,因為它會判斷之前是否存在相同的Loader
,這樣我們就可以複用之前已經載入過的資料,當螢幕裝轉導致Activity
重建的時候,我們就不需要再去重新建立一個新的Loader
,也免去了重新讀取資料的過程。 -
restartLoader
-
呼叫這個方法,將會重新建立一個指定
ID
的Loader
,如果當前已經有一個和這個ID
關聯的Loader
,那麼會對它進行canceled/stopped/destroyed
等操作,之後,使用新傳入的Bundle
引數來建立一個新的Loader
,並在資料載入完畢後遞交給呼叫者。 -
並且,在呼叫完這個函式之後,所有之前和這個
ID
關聯的Loader
都會失效,我們將不會收到它們傳遞過來的任何資料。
總結下來就是:
- 當呼叫上面這兩個方法時,如果不存在一個和
ID
關聯的Loader
,那麼這兩個方法是完全相同的。 - 如果已經存在相關聯的
Loader
,那麼init
方法除了替代Callback
,不會做任何其它的事情,包括取消/停止等。而restart
方法將會建立一個新的Loader
,並且重新開始查詢。
三、程式碼的區別
為了方便大家更直觀地理解,我們擷取一部分的原始碼來看一下:
initLoader
LoaderInfo info = mLoaders.get(id);
if (info == null) {
//如果不存在關聯的Loader,那麼建立一個新的Loader
info = createAndInstallLoader(id, args, LoaderManager.LoaderCallbacks<Object>)callback);
} else {
//如果已經存在,那麼僅僅替代Callback,傳入的Bundle引數會被丟棄。
info.mCallbacks = (LoaderManager.LoaderCallbacks<Object>)callback;
}
複製程式碼
restartLoader
LoaderInfo info = mLoaders.get(id);
//如果已經存在一個相關聯的Loader,那麼執行操作。
if (info != null) {
//mInactiveLoaders列表就是用來跟蹤那些已經被拋棄的Loader
LoaderInfo inactive = mInactiveLoaders.get(id);
if (inactive != null) {
//對跟蹤列表進行一系列的操作。
} else {
//取消被拋棄的Loader,並加入到跟蹤列表當中,以便在新的Loader完成任務之後銷燬它。
info.mLoader.abandon();
mInactiveLoaders.put(id, info);
}
}
//通知呼叫者,建立一個新的Loader,這個Loader將會和新的Bundle和Callback相關聯。
info = createAndInstallLoader(id, args, (LoaderManager.LoaderCallbacks<Object>)callback);
複製程式碼
通過上面的程式碼,就印證了前面第二節我們的說話,我們根據這些區別可以知道它們各自的職責:
initLoader
是用來確保Loader
能夠被初始化,如果已經存在相同ID
的Loader
,那麼它會複用之前的。restartLoader
的應用場景則是我們的查詢條件發生了改變。因為LoaderManager
是用ID
關聯的,當這個Loader
已經獲取到了資料,那麼就不需要再啟動它了。因此當我們的需求發生了改變,就需要重新建立一個Loader
。
也就是說:
- 查詢條件一直不變時,使用
initLoader
- 查詢條件有可能發生改變時,採用
restartLoader
。
五、對於螢幕旋轉的情況
5.1 重建
當我們在Manifest.xml
沒有給Activity
配置configChanged
的時候,旋轉螢幕會導致的Activity/Fragment
重建,這時候有兩點需要注意的:
-
由於此時我們的查詢條件並不會發生改變,並且
LoaderManager
會幫我們恢復Loader
的狀態。因此,我們沒有必要再去呼叫restartLoader
來重新建立Loader
來執行一次耗時的查詢操作。 -
LoaderManager
雖然會恢復Loader
,但是它不會儲存Callback
例項,因此,如果我們希望在旋轉完之後獲得資料,那麼至少要呼叫一次initLoader
來傳入一個新的Callback
進行監聽。
在這種情況下,假如我們在旋轉之前Loader
已經載入資料完畢了,那麼onLoadFinished
會立即被會調。
5.2 沒有重建
當沒有重建時,不會走onCreate
方法,因此需要在別的地方來初始化Loader
。
5.3 LoaderId
針對上面的這兩種情況,我們都需要自己去儲存LoaderId
,在元件恢復之後,通過這個儲存的id
去呼叫init/restart
方法,一般情況下,是通過savedInstanceState
來儲存的。
六、示例
現在,我們通過一個很簡單的例子,來看一下,initLoader/restartLoader
的區別,我們的Demo
中有一個EditText
和一個TextView
,當EditText
發生改變時,我們將當前EditText
的內容作為查詢的Key
,查詢任務就是呼叫Loader
,延時2s
,並將這個key
作為查詢的結果展示在TextView
上。
6.1 採用initLoader
來查詢:
public class MainActivity extends Activity {
private static final String LOADER_TAG = "loader_tag";
private static final String QUERY = "query";
private MyLoaderCallback mMyLoaderCallback;
private TextView mResultView;
private EditText mEditText;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
init();
}
private void init() {
mEditText = (EditText) findViewById(R.id.loader_input);
mResultView = (TextView) findViewById(R.id.loader_result);
mEditText.addTextChangedListener(new MyEditTextWatcher());
mMyLoaderCallback = new MyLoaderCallback();
}
private void startQuery(String query) {
if (query != null) {
Bundle bundle = new Bundle();
bundle.putString(QUERY, query);
getLoaderManager().initLoader(0, bundle, mMyLoaderCallback);
}
}
private void showResult(String result) {
if (mResultView != null) {
mResultView.setText(result);
}
}
private static class MyLoader extends BaseDataLoader<String> {
public MyLoader(Context context, Bundle bundle) {
super(context, bundle);
}
@Override
protected String loadData(Bundle bundle) {
Log.d(LOADER_TAG, "loadData");
try {
Thread.sleep(2000);
} catch (Exception e) {
e.printStackTrace();
}
return bundle != null ? bundle.getString(QUERY) : "empty";
}
}
private class MyLoaderCallback implements LoaderManager.LoaderCallbacks {
@Override
public Loader onCreateLoader(int id, Bundle args) {
Log.d(LOADER_TAG, "onCreateLoader");
return new MyLoader(getApplicationContext(), args);
}
@Override
public void onLoadFinished(Loader loader, Object data) {
Log.d(LOADER_TAG, "onLoadFinished");
showResult((String) data);
}
@Override
public void onLoaderReset(Loader loader) {
Log.d(LOADER_TAG, "onLoaderReset");
showResult("");
}
}
private class MyEditTextWatcher implements TextWatcher {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
Log.d(LOADER_TAG, "onTextChanged=" + s);
startQuery(s != null ? s.toString() : "");
}
@Override
public void afterTextChanged(Editable s) {}
}
}
複製程式碼
當我們輸入a
時,成功地獲取到了資料:
b
,發現onLoadFinished
立即被回撥了,但是結果還是之前地a
我們通過AS
的斷電發現,整個呼叫過程如下:
也就是在initLoader
之後,立即執行了:
public <D> Loader<D> initLoader(int id, Bundle args, LoaderManager.LoaderCallbacks<D> callback) {
//按照前面的分析,此時的info不為null。
if (info.mHaveData && mStarted) {
info.callOnLoadFinished(info.mLoader, info.mData);
}
return (Loader<D>)info.mLoader;
}
複製程式碼
而之後,callOnLoadFinished
就會把之前的資料傳回給呼叫者,因此沒有執行onCreateLoader
,也沒有進行查詢操作:
void callOnLoadFinished(Loader<Object> loader, Object data) {
//傳遞資料給呼叫者.
if (mCallbacks != null) {
mCallbacks.onLoadFinished(loader, data);
}
}
複製程式碼
假如,我們在a
觸發的任務還沒有執行時就輸入b
,我們看看會發生什麼,可以看到,它並不會考慮後來的任務:
6.2 採用restartLoader
查詢
還是按照前面的方式,我們先輸入a
:
initLoader
的結果完全相同,接下來再輸入b
:
可以清楚地看到,在執行完restartLoader
之後,LoaderManager
回撥了onCreateLoader
方法讓我們傳入新的Loader
,並且之後重新進行了查詢,併成功地返回了結果。
接下來,我們看一下連續輸入的情況:
可以看到,雖然a
觸發的任務已經開始了,但是當我們輸入b
的時候,最終得到的時ab
這個結果,並且a
所觸發的任務的結果並沒有返回給呼叫者,這也是我們所希望的,因為我們的結果一定是要以使用者最後輸入的為準。
6.3 加上保證正確初始化的程式碼
最後,我們再加上保證Loader
能夠正確初始化的程式碼,一個簡單的聯想輸入 - 載入框架就搭建好了。
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
init();
restore(savedInstanceState);
}
private void save(Bundle outState) {
if (mEditText != null) {
outState.putString(QUERY, mEditText.getText().toString());
}
}
private void restore(Bundle savedInstanceState) {
if (savedInstanceState != null) {
Bundle bundle = new Bundle();
String query = bundle.getString(QUERY);
bundle.putString(QUERY, query);
getLoaderManager().initLoader(0, bundle, mMyLoaderCallback);
}
}
@Override
protected void onSaveInstanceState(Bundle outState) {
save(outState);
super.onSaveInstanceState(outState);
}
複製程式碼