Android Loader 機制,讓你的資料載入更加輕鬆

developerHaoz發表於2017-12-06

前言

在 Android 中,任何耗時的操作都不能放在 UI 執行緒中,所以耗時的操作都需要使用非同步載入來實現。其實,載入耗時資料的常用方式其實也挺多的,就讓我們來看一下

1、Thread + Handler

Thread + Handler

2、AsyncTask

AsyncTask

3、Loader

Loader

前面兩種非同步載入方式,相信大家是比較熟悉的,但是第三種方式,可能有些人是沒怎麼接觸過的,其實在 ContentProvider 中也可能存在耗時的操作,這時候也應該使用非同步操作,而 Android 3.0 之後最推薦的非同步操作就是 Loader,使用 Loader 機制能讓我們高效地載入資料

一、Loader 簡介


Android 3.0 中引入了 Loader 機制,讓開發者能輕鬆在 Activity 和 Fragment 中非同步載入資料,Loader 機制具有以下特徵:

  • 可用於每個 Activity 或 Fragment

  • 支援非同步載入資料

  • 監控資料來源並在內容變化時傳遞新結果

  • 在某一配置更改後重建載入器時,會自動重新連線上一個載入器的遊標。因此,它們無需重新查詢其資料。

我們用一張圖來直觀地認識下 Loader 機制和另外兩種做法之間的區別

Android 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 了,需要完整程式碼的 點選這裡。覺得有幫助的話,希望能幫忙點個贊,歡迎關注。


猜你喜歡

相關文章