Android Loader用法總結
Android提供了幾種非同步載入資料的方式,AsyncTaskLoader就是其中一種,這裡對它的用法做一個總結。
Android在3.0引入了Loader(載入器),支援在Activity或Fragment中非同步載入資料。想要在低版本上使用Loader可以用v4相容包。
3.0之後官方文件強烈推薦使用Loader載入資料。
Loader的特性
- 可用於每個 Activity 和 Fragment。
- 支援非同步載入資料。
- 監控其資料來源並在內容變化時傳遞新結果。
- 在某一配置改變(如橫豎屏切換)後重建Loader時,會自動重新連線上一個Loader的遊標(cursor)。 因此,它們無需重新查詢資料。
LoaderManager
一個與 Activity 或 Fragment 相關聯的的抽象類,用於管理一個或多個 Loader 例項。 這有助於應用管理與 Activity 或 Fragment 生命週期相關聯的、執行時間較長的操作。
它最常見的用法是與 CursorLoader 一起使用,你也可以實現自己的Loader來載入其他型別的資料。
每個 Activity 或Fragment中只有一個 LoaderManager。但一個 LoaderManager 可以有多個Loader。
LoaderManager.LoaderCallbacks
一個回撥介面,用於客戶端與 LoaderManager 進行互動。例如,你可以使用 onCreateLoader() 回撥方法建立新的Loader。
Loader
一個執行非同步資料載入的抽象類,是Loader的基類。
通常使用 CursorLoader,你也可以實現自己的子類。
Loader處於活動狀態時,它們應該監視它們的資料來源並且在資料改變時傳送新的結果。
Loader本身並不支援非同步載入機制,所以當我們編寫自己的Loader的時候,不應該直接繼承Loader類,應該繼承AsyncTaskLoader,AsyncTaskLoader支援非同步載入機制。
AsyncTaskLoader
一個使用AsyncTask來執行非同步載入工作的抽象類。
CursorLoader
AsyncTaskLoader的子類,查詢ContentResolver然後返回一個Cursor。
該類以標準遊標查詢實現了Loader協議,它的查詢是通過AsyncTaskLoader在後臺執行緒中執行,所以不會阻塞UI執行緒。使用這個Loader是從ContentProvider非同步載入資料的最好方式。
框架結構
啟動Loader
通常要在Activity的onCreate()方法中或Fragment的onActivityCreated()方法中初始化Loader,可以按照下面的方式建立:
//準備Loader,重連一個已存在的Loader或者啟動新的Loader
//id:一個唯一的ID用於標識這個Loader,在這個例子中是0;
//args:可選的引數,在Loader初始化時作為引數傳入,本例中是null;
//callbacks:一個LoaderManager.LoaderCallbacks的實現,LoaderManager 將呼叫此實現來報告Loader事件;在這個例子中,當前類實現了這個介面,所以傳的是自身的引用:this。
getLoaderManager().initLoader(0, null, this);
initLoader() 確保了Loader已初始化且處於活動狀態。這可能會出現兩種結果:
- 如果 ID 所指的Loader已存在,則將重複使用上次建立的Loader,這時候args會被忽略,因為重用了之前的Loader。
- 如果 ID 所指的Loader不存在,則 initLoader() 將觸發 LoaderManager.LoaderCallbacks 中的回撥方法 onCreateLoader()。在此方法中,你可以在這裡例項化並返回新的Loader。
在這兩種情況下,傳入的LoaderManager.LoaderCallbacks的實現都與Loader繫結在一起,並且會在Loader狀態變化時被呼叫;
如果在呼叫這個initLoader()方法時,Loader已經處於啟動狀態(也就是說這個Loader已存在),並且所請求的Loader已產生了資料,那麼系統會馬上呼叫onLoadFinished(),所以你必須為這種情況的發生做好準備。
intiLoader()會返回一個建立的Loader,但是你不用獲取它的引用,因為LoadeManager會自動管理該Loader的生命週期,你只用在它回撥提供的生命週期方法中做自己資料邏輯的處理即可。
重啟Loader
當你使用initLoader()時,如果指定ID的Loader已經存在,則它使用這個Loader;如果不存在,它將建立一個新的。但是有的時候你卻想丟棄舊的Loader然後開始一個新的Loader。
要想丟棄舊的Loader,應該使用restartLoader()。例如,下面這個SearchView.OnQueryTextListener的實現在使用者查詢發生改變時重啟了Loader,Loader需要重啟從而才能使用新的搜尋過濾詞來進行一次新的查詢。
public boolean onQueryTextChanged(String newText) {
// 當搜尋內容變化時,更新搜尋過濾詞,重啟Loader並用這個搜尋過濾詞重新查詢
mCurFilter = !TextUtils.isEmpty(newText) ? newText : null;
getLoaderManager().restartLoader(0, null, this);
return true;
}
使用LoaderManager中的回撥
LoaderManager.LoaderCallbacks是一個回撥介面,它使得客戶端可以與LoaderManager進行互動。
Loader,尤其是CursorLoader,我們希望在它停止後依然保持資料,這讓應用可以在Activity或Fragment的onStop()和onStart()方法保持資料;當用戶返回應用時,他們不需要再等待資料載入,你可以使用LoaderManager.LoaderCallbacks中的方法,在需要時建立新的Loader,並且告訴應用什麼時候要停止使用Loader中的資料。
LoaderManager.LoaderCallbacks包含以下方法:
onCreateLoader()
根據傳入的ID,初始化並返回一個新的Loader。當你試圖去操作一個Loader時(比如,呼叫initLoader()方法),它會檢查是否指定ID的Loader已經存在,如果不存在,將會觸發LoaderManager.LoaderCallbacks中的onCreateLoader(),你需要在這裡建立一個新Loader並返回。
onLoadFinished():當一個Loader完成了資料載入之後呼叫。
這個方法確保會在Loader上的資料被釋放之前被呼叫。在此方法中,你必須移除所有對舊資料的使用(因為它們將很快會被刪除),但是不要自己去釋放它們,因為Loader會去做這些事情。
Loader一旦瞭解到應用不再使用這些資料時,將馬上釋放這些資料。
例如,如果資料是一個從CursorLoader來的Cursor,你不應該自己呼叫cursor的close()方法;如果cursor被放置在一個CursorAdapter中,你可以使用swapCursor()方法進行新資料交換,這樣,舊的cursor就不會被關閉(CursorLoader的實現中會自動幫你關閉舊的cursor),就不會導致Adapter載入異常。
onLoaderReset():當一個Loader被重置,從而使得其資料無效時被呼叫。
這個回撥告訴你什麼時候資料將被釋放,所以你可以釋放對它的引用。
下面實現了呼叫引數為null的swapCursor()
public void onLoaderReset(Loader<Cursor> loader) {
mAdapter.swapCursor(null);
}
例子
下面的例子實現了從通訊錄載入使用者暱稱,通過SearchView過濾搜尋結果。
因為要讀取通訊錄,所以要現在Manifest中宣告READ_CONTACTS 許可權
public class CursorLoaderActivity
extends Activity
implements SearchView.OnQueryTextListener,
LoaderManager.LoaderCallbacks<Cursor> {
private SimpleCursorAdapter mAdapter;
private String mCurFilter;// 搜尋過濾器
private ListView mListView;
private TextView mTxtEmpty;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_loader);
mListView = (ListView) findViewById(R.id.listview);
mTxtEmpty = (TextView) findViewById(R.id.txt_empty);
mListView.setEmptyView(mTxtEmpty);//沒有資料時顯示的view
mAdapter = new SimpleCursorAdapter(this,
android.R.layout.simple_list_item_2, null,
new String[]
ContactsContract.Contacts.DISPLAY_NAME,
ContactsContract.Contacts.CONTACT_STATUS},
new int[]{android.R.id.text1,
android.R.id.text2}, 0);
mListView.setAdapter(mAdapter);
// 啟動loader,可以重連一個已經存在的也可以啟動一個新的
getLoaderManager().initLoader(0, null, this);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
//在ActionBar上顯示SearchView
MenuItem item = menu.add("Search");
item.setIcon(android.R.drawable.ic_menu_search);
item.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
SearchView sv = new SearchView(this);
sv.setOnQueryTextListener(this);
item.setActionView(sv);
return super.onCreateOptionsMenu(menu);
}
public boolean onQueryTextChange(String newText) {
//當搜尋字串變化時呼叫
//使用新的搜尋過濾器重啟loader
mCurFilter = !TextUtils.isEmpty(newText) ? newText : null;
getLoaderManager().restartLoader(0, null, this);
return true;
}
@Override
public boolean onQueryTextSubmit(String query) {
return true;
}
static final String[] CONTACTS_SUMMARY_PROJECTION = new String[]{
ContactsContract.Contacts._ID,
ContactsContract.Contacts.DISPLAY_NAME,
ContactsContract.Contacts.CONTACT_STATUS,
ContactsContract.Contacts.CONTACT_PRESENCE,
ContactsContract.Contacts.PHOTO_ID,
ContactsContract.Contacts.LOOKUP_KEY,
};
/**
* 建立一個CursorLoader並返回,如果initLoader()指定id的loader不存在,會觸發這個回撥方法
*
* @param id 給loader指定的id,如果只有一個loader,就不用管id
* @param args 傳遞的引數
* @return
*/
@Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
//根據是否對結果過濾來確定uri
Uri baseUri;
if (mCurFilter != null) {
baseUri = Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_FILTER_URI,
Uri.encode(mCurFilter));
} else {
baseUri = ContactsContract.Contacts.CONTENT_URI;
}
String select = "((" + ContactsContract.Contacts.DISPLAY_NAME + " NOTNULL) AND ("
+ ContactsContract.Contacts.HAS_PHONE_NUMBER + "=1) AND ("
+ ContactsContract.Contacts.DISPLAY_NAME + " != '' ))";
CursorLoader cursorLoader = new CursorLoader(
this,
baseUri,//uri:要獲取內容的uri
CONTACTS_SUMMARY_PROJECTION, //要返回的列的集合,傳入null將會返回所有列(比較低效)
select,//一個過濾器,表示哪些行將會被返回,格式化成類似SQL where子句的樣子,傳入null將返回所有行
null,//selectionArgs:可以在selection中包含一些'?',它將被本引數的值替換掉,這些值出現的順序與'?'在selection中出現的順序一致,值將作為字串
ContactsContract.Contacts.DISPLAY_NAME + " COLLATE LOCALIZED ASC"//sortOrder:排序字串,格式化成類似於SQL ORDER BY子句樣子(除去ORDER BY),傳入null將使用預設順序,預設順序可能是無序的
);
return cursorLoader;
}
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
//釋放cursor(系統會幫我們關閉舊的cursor)
mAdapter.swapCursor(data);
}
public void onLoaderReset(Loader<Cursor> loader) {
//當onLoadFinished()中的cursor將要被關閉的時候呼叫,我們需要確保它不再被使用
mAdapter.swapCursor(null);
}
}
什麼時候不該用Loader
如果你需要確保後臺任務執行完成,就不要用Loader,因為當Activity或Fragment被銷燬的時候,這個Activity或Fragment裡面的Loader也會被銷燬。如果要執行後臺任務,可以使用Service。
要記得Loader是用來幫助你建立相應的UI,非同步載入資料到這些UI上。這就是Loader和建立它的元件的生命週期繫結到一起的原因,不要濫用Loader!
在Service中使用
LoaderManager明顯的是為Activity或Fragment生命週期而設計的來管理Loader的類,因為Service沒有這些狀態變化,所以沒必要在Service中使用LoaderManager。
但是如果你已經為Activity或Fragment寫好了Loader,現在要在Service中重用這些程式碼,可以不使用LoaderManager,直接操作Loader。
在Service被建立的時候,create, register, start你的Loader
@Override
public void onCreate() {
mCursorLoader = new CursorLoader(context, contentUri, projection, selection, selectionArgs, orderBy);
mCursorLoader.registerListener(LOADER_ID_NETWORK, this);
mCursorLoader.startLoading();
}
使Service implements OnLoadCompleteListener介面,在回撥函式onLoadComplete中繫結資料和UI
@Override
public void onLoadComplete(Loader<Cursor> loader, Cursor data) {
}
在onDestroy中銷燬Loader
@Override
public void onDestroy() {
// Stop the cursor loader
if (mCursorLoader != null) {
mCursorLoader.unregisterListener(this);
mCursorLoader.cancelLoad();
mCursorLoader.stopLoading();
}
}
當然你也可以這樣寫
private MyCursorLoader mMyCursorLoader;
Loader.OnLoadCompleteListener<Cursor> mCursorListener = new Loader.OnLoadCompleteListener<ConnectionStatus>() {
@Override
public void onLoadComplete(Loader<Cursor> loader, Cursor data) {
}
};
Bundle bundle = new Bundle();
bundle.putString(BUNDLE_KEY_UID, uid);
mMyCursorLoader = new MyCursorLoader(mContext, bundle, null, this);
mMyCursorLoader.registerListener(LOADER_1, mCursorListener);
mMyCursorLoader.startLoading();
AsyncTaskLoader & AsyncTAsk
AsyncTaskLoader
優勢:會自動重新整理資料變化,會自動處理Activity配置變化造成的影響,適合處理純資料載入。
劣勢:不能實時通知UI重新整理,不能在onLoadFinished時主動切換生命週期(比如replace fragment)。
AsyncTask
優勢:可以與UI實時互動及replace操作。
劣勢:不會自動處理Activity配置變化造成的影響。
Loader生命週期
…
幾個坑
Loader id重複導致資料邏輯異常
多執行緒中restartLoader導致Loader丟擲異常(都在UI執行緒中執行就行了)