前言
在 Android 中,任何耗時的操作都不能放在 UI 執行緒中,所以耗時的操作都需要使用非同步載入來實現。其實,載入耗時資料的常用方式其實也挺多的,就讓我們來看一下
1、Thread + Handler
2、AsyncTask
3、Loader
前面兩種非同步載入方式,相信大家是比較熟悉的,但是第三種方式,可能有些人是沒怎麼接觸過的,其實在 ContentProvider 中也可能存在耗時的操作,這時候也應該使用非同步操作,而 Android 3.0 之後最推薦的非同步操作就是 Loader,使用 Loader 機制能讓我們高效地載入資料
一、Loader 簡介
Android 3.0 中引入了 Loader 機制,讓開發者能輕鬆在 Activity 和 Fragment 中非同步載入資料,Loader 機制具有以下特徵:
-
可用於每個 Activity 或 Fragment
-
支援非同步載入資料
-
監控資料來源並在內容變化時傳遞新結果
-
在某一配置更改後重建載入器時,會自動重新連線上一個載入器的遊標。因此,它們無需重新查詢其資料。
我們用一張圖來直觀地認識下 Loader 機制和另外兩種做法之間的區別
從圖片中可以看出 Loader 機制的寫法是相當簡潔的,可以讓我們進行快速的開發,而且效率方面也是非常高的。
二、相關類和 API 介紹
本節內容大部分來自官方文件,詳細內容可以 點選這裡
在介紹 Loader 的使用之前,我們先來看一下與 Loader 機制相關的一些類和介面
類 / 介面 | 說明 |
---|---|
LoaderManager | 一種與 Activity 或 Fragment 相關聯的抽象類,用於管理一個或多個 Loader 例項。這有助於應用管理與 Activity 或 Fragment 生命週期相關的、執行時間較長的操作。它常見的用法是 與 CursorLoader 一起使用,不過應用也可以自由寫入自己的載入器,用於載入其他型別的資料 |
LoaderManager.LoaderCallbacks | 回撥介面,用於客戶端與 LoaderManager 進行互動,例如,可以使用 onCreateLoader() 回撥方法建立新的載入器 |
Loader | 一種執行非同步載入資料的抽象類。這是載入器的基類。我們通常會使用 CursorLoader,但也可以實現自己的子類。當載入器處於活動狀態時,應監控其資料來源並在內容變化時傳遞新結果 |
AsyncTaskLoader | 提供 AsyncTask 來執行工作的抽象載入器 |
CursorLoader | AsyncTaskLoader 的子類,它將查詢 ContentResolver 並返回一個 Cursor。使用此載入器是從 ContentProvider 非同步載入資料的最佳方式,而不用通過 Activity 或 Fragment 的 API 來執行託管查詢 |
以上便是 Loader 機制相關的類,但並不是我們建立的每個載入器都要用到上述所有的類和介面。但是,為了初始化載入器以及實現一個 Loader 類(如 CursorLoader),我們需要引用 LoaderManager。
2.1 載入器的使用
使用載入器的應用通常包括:
-
Activity 或 Fragment
-
LoaderManager 的例項
-
一個 CursorLoader,用於載入由 ContentProvider 支援的資料。當然我們也可以實現自己的 Loader 或 AsyncTaskLoader 子類,從其他的資料來源中載入資料
-
一個 LoaderManager.LoaderCallbacks 實現,可以使用它來建立新的載入器,並管理對現有載入器的引用
-
顯示載入器資料的方法,如 SimpleCursorAdapter
-
使用 CursorLoader 時的資料來源,如 ContentProvider
啟動載入器
LoaderManager 可在 Activity 或 Fragment 內管理一個或多個 Loader 例項,每個 Activity 或 Fragment 中只有一個 LoaderManager。通過我們會在 Activity 的 onCreate() 方法或 Fragment 中的 onActivityCreate() 方法內初始化 Loader
getSupportLoaderManager().initLoader(0,null,this);
複製程式碼
initLoader() 方法採用以下引數:
-
用於標識載入器的唯一 ID,在程式碼示例中,ID 為 0
-
在構建時提供給載入器的可選引數(在程式碼示例中,為 null)
-
LoaderManager.LoaderCallbacks 實現,LoaderManager 將呼叫該實現來報告載入器事件。在此示例中,本地類實現了 LoaderManager.LoaderCallbacks 介面,因此直接傳遞它對自身的引用 this
initLoader() 呼叫確保載入器已經初始化且處於活動狀態,這可能會出現兩種結果:
-
如果指定 ID 的載入器已經存在,那麼將重複使用上次建立的載入器
-
如果指定 ID 的載入器不存在,則 initLoader() 將觸發 LoaderManager.LoaderCallbacks 中的 onCreateLoader() 方法,在這個方法中,我們可以實現程式碼以例項化並返回新的載入器
無論何種情況,給定的 LoaderManager.LoaderCallbacks 實現均與載入器相關聯,且在載入器狀態變化時呼叫。如果在呼叫時,呼叫程式處於啟動狀態,且請求的載入器已存在並生成了資料,則系統將立即呼叫 onLoadFinish()
有一點要注意的是,initLoader() 方法將返回已建立的 Loader,但我們不用捕獲它的引用。**LoaderManager 將自動管理載入器的生命週期。**LoaderManager 將根據需要啟動和停止載入,並維護載入器的狀態及其相關內容。這意味著我們將很少與載入器直接進行互動。當特定事件發生時,我們通常會使用 LoaderManager.LoaderCallbacks 方法干預載入程式。
重啟載入器
當我們使用 initLoader(),它將使用含有指定 ID 的現有載入器(如有)。如果沒有它會建立一個。但有時,我們想捨棄這些舊資料並重新開始。
要捨棄舊資料,我們需要使用 restartLoader(),例如,當使用者的查詢更改時,SearchView.OnQueryTextListener 實現將重啟載入器。載入器需要重啟,以便它能夠使用修正後的搜尋過濾器執行新查詢:
public boolean onQueryTextChanged(String newText){
mCurFilter = !TextUtils.isEmpty(newText) ? newText : null;
getSupportLoaderManager().restartLoader(0, null, this);
return true;
}
複製程式碼
使用 LoaderManager 回撥
LoaderManager.LoaderCallbacks 是一個支援客戶端與 LoaderManager 互動的回撥介面
載入器(特別是 CursorLoader)在停止執行後,仍需保留其資料,這樣既可保留 Activity 或 Fragment 的 onStop() 和 onStart() 方法中的資料。當使用者返回應用時,無需等待它重新載入這些資料。
LoaderManager.LoaderCallbacks 介面包括以下方法
-
onCreateLoader():針對指定的 ID 進行例項化並返回新的 Loader
-
onLoadFinished():將在先前建立的載入器完成載入時呼叫
-
onLoaderReset():將在先前建立的載入器重置且其資料因此不可用時呼叫
onCreateLoader()
當我們嘗試訪問載入器時(例如,通過 initLoader()),該方法將檢查是否已存在由該 ID 指定的載入器。如果沒有,它將觸發 LoaderManager.LoaderCallbacks 中的 onCreateLoader() 方法。在此方法中,我們可以建立載入器,通過這個方法將返回 CursorLoader,但我們也可以實現自己的 Loader 子類。
在下面的示例中,onCreateLoader() 方法建立了 CursorLoader。我們必須使用它的構造方法來構建 CursorLoader。構造方法 需要對 ContentProvider 執行查詢時所需的一系列完整資訊
public CursorLoader(Context context, Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
super(context);
mObserver = new ForceLoadContentObserver();
mUri = uri;
mProjection = projection;
mSelection = selection;
mSelectionArgs = selectionArgs;
mSortOrder = sortOrder;
}
複製程式碼
引數名 | 作用 |
---|---|
uri | 用於檢索內容的 URI |
projection | 返回的列的列表。傳遞 null 時,將返回所有列,這樣的話效率會很低 |
selection | 一種用於宣告返回那些行的過濾器,採用 SQL WHERE 子句格式。傳遞 null 時,將為指定的 URI 返回所有行 |
selectionArgs | 我們可以在 selection 中包含 ?,它將按照在 selection 中顯示的順序替換為 selectionArgs 中的值 |
sortOrder | 行的排序依據,採用 SQL ORDER BY 子句格式。傳遞 null 時,將使用預設排序順序(可能並未排序) |
示例程式碼:
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
Uri baseUri;
if (mCurFilter != null) {
baseUri = Uri.withAppendedPath(Contacts.CONTENT_FILTER_URI,
Uri.encode(mCurFilter));
} else {
baseUri = Contacts.CONTENT_URI;
}
String select = "((" + Contacts.DISPLAY_NAME + " NOTNULL) AND ("
+ Contacts.HAS_PHONE_NUMBER + "=1) AND ("
+ Contacts.DISPLAY_NAME + " != '' ))";
return new CursorLoader(getActivity(), baseUri,
CONTACTS_SUMMARY_PROJECTION, select, null,
Contacts.DISPLAY_NAME + " COLLATE LOCALIZED ASC");
}
複製程式碼
onloadFinished
當先前建立的載入器完成載入時,將會呼叫此方法。該方法必須在為此載入器提供的最後一個資料釋放之前呼叫。此時,我們應該移除所有使用的舊資料(因為它們很快就會被釋放),但不要自行釋放這些資料,因為這些資料歸載入器所有,載入器會處理它們。
當載入器發現應用不再使用這些資料時,將會釋放它們。例如,如果資料是來自 CursorLoader 的一個遊標,則我們不應手動對其呼叫 close()。如果遊標放置在 CursorAdapter 中,則應使用 swapCursor() 方法,使舊 Cursor 不會關閉
SimpleCursorAdapter mAdapter;
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
mAdapter.swapCursor(data);
}
複製程式碼
onLoadReset
該方法將在 先前建立的載入器重置 且 資料因此不可用 時呼叫,通過此回撥,我們可以瞭解何時將釋放資料,因此能夠及時移除其引用。
此實現呼叫值為 null 的 swapCursor()
SimpleCursorAdapter mAdapter;
public void onLoaderReset(Loader<Cursor> loader) {
mAdapter.swapCursor(null);
}
複製程式碼
三、Loader 機制的使用場景和使用方式
Loader 機制一般用於資料載入,特別是用於載入 ContentProvider 中的內容,比起 Handler + Thread 或者 AsyncTask 的實現方式,Loader 機制能讓程式碼更加的簡潔易懂,而且是 Android 3.0 之後最推薦的載入方式。
Loader 機制的 使用場景 有:
-
展現某個 Android 手機有多少應用程式
-
載入手機中的圖片和視訊資源
-
訪問使用者聯絡人
下面用一個載入手機中的圖片資料夾的例子,看看在實際開發中如何運用 Loader 機制進行高效載入。
3.1 實現自己的載入器
載入器是我們載入資料的工具,通過將對應的 URI 以及其他的查詢條件傳遞給載入器,便可讓載入器在後臺高效地載入資料,等資料載入完成了便會返回一個 Cursor.
public class AlbumLoader extends CursorLoader {
private static final Uri QUERY_URI = "content://media/external/file";
private static final String[] PROJECTION = {
"_id",
"bucket_id",
"bucket_display_name",
"_data",
"COUNT(*) AS " + "count"};
private static final String SELECTION =
"(media_type=? OR media_type =?) AND _size>0) GROUP BY (bucket_id"
private static final String[] SELECTION_ARGS = {
String.valueOf(MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE),
String.valueOf(MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO)
};
private static final String BUCKET_ORDER_BY = "datetaken DESC";
private AlbumLoader(Context context, String selection, String[] selectionArgs) {
super(context, QUERY_URI, PROJECTION, SELECTION, SELECTION_ARGS, BUCKET_ORDER_BY);
}
public static CursorLoader newInstance(Context context) {
String selection = SELECTION;
String[] selectionArgs = SELECTION_ARGS;
return new AlbumLoader(context, selection, selectionArgs);
}
@Override
public Cursor loadInBackground() {
return super.loadInBackground();
}
}
複製程式碼
3.2 實現 LoaderCallbacks 進行客戶端的互動
為了降低程式碼的耦合度,繼承 LoaderManager.Loadercallbacks 實現 AlbumLoader 的管理類,將 Loader 的各種狀態進行管理。
通過外部傳入 Context,採用弱引用的方式防止記憶體洩露,獲取 LoaderManager,並在 AlbumCollection 內部定義了相應的介面,將載入完成後返回的 Cursor 回撥出去,讓外部的 Activity 或 Fragment 進行相應的處理。
public class AlbumCollection implements LoaderManager.LoaderCallbacks<Cursor> {
private static final int LOADER_ID = 1;
private WeakReference<Context> mContext;
private LoaderManager mLoaderManager;
private AlbumCallbacks mCallbacks;
@Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
Context context = mContext.get();
return AlbumLoader.newInstance(context);
}
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
Context context = mContext.get();
mCallbacks.onAlbumLoad(data);
}
@Override
public void onLoaderReset(Loader<Cursor> loader) {
Context context = mContext.get();
mCallbacks.onAlbumReset();
}
public void onCreate(FragmentActivity activity, AlbumCallbacks callbacks){
mContext = new WeakReference<Context>(activity);
mLoaderManager = activity.getSupportLoaderManager();
mCallbacks = callbacks;
}
public void loadAlbums(){
mLoaderManager.initLoader(LOADER_ID, null, this);
}
public interface AlbumCallbacks{
void onAlbumLoad(Cursor cursor);
void onAlbumReset();
}
}
複製程式碼
3.3 主介面中的邏輯
看到這程式碼是不是覺得特別簡潔,讓 MainActivity 中繼承了 AlbumCollection 中的 AlbumCallback 介面,接著 onCreate() 中例項化了 AlbumCollection,然後讓 AlbumCollection 開始載入資料。
等資料載入完成後,便將包含資料的 Cursor 回撥在 onAlbumLoad() 方法中,我們便可以進行 UI 的更新。
可以看到採用 Loader 機制,可以讓我們的 Activity 或 Fragment 中的程式碼變得相當的簡潔、清晰,而且程式碼耦合程度也相當低。
public class MainActivity extends AppCompatActivity implements AlbumCollection.AlbumCallbacks{
private AlbumCollection mCollection;
private AlbumAdapter mAdapter;
private RecyclerView mRvAlbum;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mCollection = new AlbumCollection();
mCollection.onCreate(this, this);
mCollection.loadAlbums();
}
@Override
public void onAlbumLoad(Cursor cursor) {
mRvAlbum = (RecyclerView) findViewById(R.id.main_rv_album);
mRvAlbum.setLayoutManager(new LinearLayoutManager(this));
mRvAlbum.setAdapter(new AlbumAdapter(cursor));
}
@Override
public void onAlbumReset() {
}
}
複製程式碼
以上便是本文的全部內容,程式碼我已經放上 Github 了,需要完整程式碼的 點選這裡。覺得有幫助的話,希望能幫忙點個贊,歡迎關注。