Android中Loader及LoaderManager的使用(附原始碼下載)

孫群發表於2015-09-30

managedQuery方法的缺陷

Loader是用來更好地載入資料的,在我們談論Loader之前,我們先研究一下Activity的managedQuery方法,該方法也是用於在Activity中載入資料的。在Android 3.0之前的版本中,我們如果想在Activity中通過ContentResolver對ContentProvider進行查詢,我們可以方便的呼叫Activity的managedQuery方法,該方法的原始碼如下:

@Deprecated
    public final Cursor managedQuery(Uri uri, String[] projection, String selection,
            String[] selectionArgs, String sortOrder) {
        Cursor c = getContentResolver().query(uri, projection, selection, selectionArgs, sortOrder);
        if (c != null) {
            startManagingCursor(c);
        }
        return c;
    }

從上面的程式碼我們可以看出managedQuery中通過ContentResolver執行了query方法,並將得到的Cursor物件交給startManagingCursor()方法來管理,這就導致了managedQuery方法存在如下缺陷:
1. Activity的managedQuery方法內部在主執行緒上執行了ContentResolver的query()方法,但該方法是個耗時方法,很可能導致應用程式無響應,出現ANR現象。
2. ContentResolver的query()方法返回了一個Cursor物件,將該Cursor物件傳遞給了startManagingCursor()方法,該方法是幹嘛的呢?檢視API文件,我們發現可以用startManagingCursor()方法管理Cursor的生命週期,使Cursor的生命週期與Activity的生命週期相對應,具體如下:

  • 當Activity處於stopped狀態的時候,會自動呼叫Cursor的deactive()方法
  • 當Activity從stopped狀態變為started狀態的時候,其又會自動呼叫該Cursor的requery()方法重新查詢資料
  • 當Activity銷燬的時候,該Cursor物件也會自動關閉
  • 當Activity configuration發生變化的時候(比如手機的橫屏豎屏來回切換等),Activity會重啟,在重啟的時候Cursor也會重新執行requery()方法

通過上面的描述,看起來startManagingCursor()很智慧,而且貌似很完美地幫我們處理了Cursor的生命週期,但是我們需要注意的是,當Activity從onStop()轉變到onStart()的時候,其會重新執行Cursor的requery()方法,但是該方法的執行時執行在主執行緒上的,並且Cursor的requery()方法也是耗時方法,該方法很有可能阻塞UI執行緒,導致ANR現象,並且在Activity重啟的時候,也會導致Cursor執行requery()方法,進一步增加應用出現無響應的情況,即ANR。

綜上,如果我們在Activity中呼叫了managedQuery()方法,那麼我們就導致我們在主執行緒上執行ContentResolver的query()方法,並增大了在主執行緒上執行Cursor的requery()方法的機率,這兩個方法都是耗時方法,都會阻塞主執行緒,因此,Activity中呼叫managedQuery()方法會極大增加應用出現ANR的現象。


Loader相關的核心類

現在如果去看Android 最新的API檢視Activity的startManagingCursor()方法,你會發現該方法從Android 3.0之後就被廢棄掉了,Android官方建議用Loader作為替代,其實Loader的作用不僅僅是作為startManagingCursor()方法的替代品,Loader以及LoaderManager是Android Framework中非同步載入各種資料(不限於Cursor)的標準機制。

Android從3.0版本之後引入了Loader以及LoaderManager,使得我們可以在Activity或Fragment中使用它們。Loader是載入器,它完成實際的資料載入工作。LoaderManager是Loader的管理著,其管理著一個或多個Loader的生命週期。

在應用程式中使用Loader常牽涉到以下的及各類:

LoaderManager:我們使用Loader載入資料時,實際上我們並不直接與Loader打交道,即我們無需也不應該呼叫Loader的相應的方法,相反,我們應用使用LoaderManager實現對Loader的管理。LoaderManager,顧名思義,就是Loader的管理器,我們可以在Activity或Fragment中通過呼叫getLoaderManager()方法獲取到LoaderManager物件,一個Activity/Fragment只有一個單例的LoaderManager物件。LoaderManager可以管理一個或多個的Loader,能夠維護Loader的生命週期。LoaderManger有兩個方法我們會經常用到:initLoader()方法和restartLoader()方法。

  • initLoader:通過呼叫LoaderManagr的initLoader()方法,我們可以建立一個Loader。
  • restartLoader:通過呼叫LoaderManager的restartLoader()方法,我們可以重啟一個Loader。
  • destroyLoader:另外通過呼叫LoaderManager的destroyLoader()方法我們可以銷燬一個Loader,不過該方法不常用,因為LoaderManager在合適的時機下會自動銷燬Loader。

我們是在Activity或Fragment使用Loader的,Activity、Fragment與LoaderManagement互動類似於client-server模式,即Activity或Fragment是該client-server模型中的client端,即客戶端,在本文中,我們所提到的客戶端均指的是Loader的使用者,即Activity或Fragment。

LoaderManager.LoaderCallbacks:LoaderManager.LoaderCallbacks是LoaderManager中的內部介面,客戶端與Loader的通訊完全是事件機制,即客戶端需要實現LoaderCallbacks中的各種回撥方法,以響應Loader & LoaderManager觸發的各種事件。客戶端在呼叫LoaderManager的initLoader()或restartLoader()方法時,就需要客戶端向這兩個方法中傳入一個LoaderCallbacks的例項。LoaderCallbacks有三個回撥方法需要實現:onCreateLoader()、onLoadFinished()以及onLoaderReset()。

  • onCreateLoader:我們要在onCreateLoader()方法內返回一個Loader的例項物件。很多情況下,我們需要查詢ContentProvider裡面的內容,那麼我們就需要在onCreateLoader中返回一個CursorLoader的例項,CursorLoader繼承自Loader。當然,如果CursorLoader不能滿足我們的需求,我們可以自己編寫自己的Loader然後在此onCreateLoader方法中返回。

  • onLoadFinished:當onCreateLoader中建立的Loader完成資料載入的時候,我們會在onLoadFinished回撥函式中得到載入的資料。在此方法中,客戶端可以得到資料並加以使用,在這之前,如果客戶端已經儲存了一份老的資料,那麼我們需要釋放對老資料的引用。

  • onLoaderReset:當之前建立的Loader被銷燬(且該Loader向客戶端傳送過資料)的時候,就會觸發onLoaderReset()回撥方法,此時表明我們之前獲取的資料被重置且處於無效狀態了,所以客戶端不應該再使用這份“過期”的無效的老資料,應該釋放對該無效資料的引用。

Loader:Loader是具體的資料載入器,但是需要說明的是Loader類本身並不支援非同步載入機制,所以當我們要編寫自己的資料載入器的時候,我們不應該直接繼承自Loader類,我們應該繼承自AsyncTaskLoader類,AsyncTaskLoader支援非同步載入機制,下面會對AsyncTaskLoader詳細解釋,此處不多說。Loader有許多public的方法,比如startLoading()、stopLoading()等,但是客戶端不應該直接呼叫這些方法,這些方法是由LoaderManager呼叫的,如果客戶端呼叫了這些public的方法,就很有可能導致Loader生命週期出現混亂,進而影響到LoaderManager對Loader的管理。

AsyncTaskLoader: AsyncTaskLoader繼承自Loader,上面我們提到了Loader類本身沒有非同步載入資料的機制,但是AsyncTaskLoader具有非同步載入的機制,這是因為AsyncTaskLoader內部使用了AsyncTask來進行非同步資料載入,所以如果我們想實現自己的Loader,我們應該直接繼承自AsyncTaskLoader類(或其子類),而非Loader類。AsyncTaskLoader中的loadInBackground()方法是抽象方法,所以AsyncTaskLoader是抽象類,其子類應該實現loadInBackground()方法,在該方法中應該實現具體的非同步載入邏輯。總之,AsyncTaskLoader不會阻塞主執行緒。

CursorLoader:CursorLoader繼承自AsyncTaskLoader,其實現了AsyncTaskLoader的loadInBackground()方法,在該方法中會執行ContentResolver的query()方法,從而實現對ContentProvider的資料查詢,其得到的資料是Cursor物件。當我們想從ContentProvider中查詢資料時候,我們不應該使用Activity中的managedQuery()方法,我們應該使用LoaderManager和CursorLoader,因為CursorLoader是非同步載入資料,不會阻塞主執行緒。

下面一張圖能反應出這幾個類之間的關係:
這裡寫圖片描述

Activity與Fragment是客戶端,客戶端通過LoaderManager的initLoader或restartLoader向LoaderManager發起獲取資料的請求,LoaderManager內部會建立相應的Loader去載入資料,資料載入完畢後會觸發LoaderCallbacks中的相應回撥方法,通過這些回撥方法,Loader可以得知相應事件的觸發。


示例程式碼

我根據Android API中的示例程式碼做了一個示例應用,並做了一些相應處理。該應用預設情況下會顯示使用者的所有聯絡人,也可以輸入相應的關鍵字對聯絡人進行過濾,UI介面如下所示:

這裡寫圖片描述

程式碼如下所示:

package com.ispring.loaderdemo;

import android.app.Activity;
import android.app.LoaderManager;
import android.content.CursorLoader;
import android.content.Loader;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.provider.ContactsContract;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.KeyEvent;
import android.widget.EditText;
import android.widget.ListView;
import android.widget.SimpleCursorAdapter;
import android.widget.TextView;


public class MainActivity extends Activity implements LoaderManager.LoaderCallbacks<Cursor>, TextWatcher {

    private EditText editText = null;

    private ListView listView = null;

    private SimpleCursorAdapter adapter = null;


    private final int CURSOR_LOADER_ID = 1;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        //繫結編輯框的文字變化事件
        editText = (EditText)findViewById(R.id.editText);
        editText.setOnEditorActionListener(new TextView.OnEditorActionListener() {
            @Override
            public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
                return false;
            }
        });
        editText.addTextChangedListener(this);

        //獲取ListView
        listView = (ListView)findViewById(R.id.listView);

        //建立Adapter
        adapter = 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);
        listView.setAdapter(adapter);

        //查詢全部聯絡人
        Bundle args = new Bundle();
        args.putString("filter", null);
        LoaderManager lm = getLoaderManager();
        lm.initLoader(CURSOR_LOADER_ID, args, this);
    }

    @Override
    public void beforeTextChanged(CharSequence s, int start, int count, int after) {
    }

    @Override
    public void onTextChanged(CharSequence s, int start, int before, int count) {
    }

    @Override
    public void afterTextChanged(Editable s) {
        String filter = editText.getText().toString();
        Bundle args = new Bundle();
        args.putString("filter", filter);
        LoaderManager lm = getLoaderManager();
        lm.restartLoader(CURSOR_LOADER_ID, args, this);
    }

    @Override
    public Loader<Cursor> onCreateLoader(int id, Bundle args) {

        Uri uri;

        String filter = args != null ? args.getString("filter") : null;

        if(filter != null){
            //根據使用者指定的filter過濾顯示
            uri = Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_FILTER_URI, Uri.encode(filter));
        }else{
            //顯示全部
            uri = ContactsContract.Contacts.CONTENT_URI;
        }

        String[] projection = new String[]{
                ContactsContract.Contacts._ID,
                ContactsContract.Contacts.DISPLAY_NAME,
                ContactsContract.Contacts.CONTACT_STATUS
        };

        String selection = "((" + ContactsContract.Contacts.DISPLAY_NAME + " NOTNULL) AND "+
                "(" + ContactsContract.Contacts.HAS_PHONE_NUMBER + " =1) AND "+
                "(" + ContactsContract.Contacts.DISPLAY_NAME + " != ''))";

        String sortOrder = ContactsContract.Contacts.DISPLAY_NAME + " COLLATE LOCALIZED ASC";

        return new CursorLoader(this, uri, projection, selection, null, sortOrder);
    }

    @Override
    public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
        adapter.swapCursor(data);
    }

    @Override
    public void onLoaderReset(Loader<Cursor> loader) {
        adapter.swapCursor(null);
    }
}

我們在AndroidManifest.xml中需要加入android.permission.READ_CONTACTS許可權以讀取聯絡人資訊。

我們分析一下以上程式碼:

1.首先MainActivity實現了LoaderManager.LoaderCallbacks介面,實現了其中的onCreateLoader()、onLoadFinished()和onLoaderReset()方法。

2.在MainActivity的onCreate()方法中,我們為ListView建立了一個SimpleCursorAdapter物件,不過我們在SimpleCursorAdapter的建構函式中傳入的cursor物件為null,因為我們後面會通過Loader得到該Cursor物件。

3.我們建立了一個Bundle物件,並向其中新增了一個key為filter的鍵值對,值為null,表示我們不過濾,這在後面會用到。然後我們呼叫LoaderManager的initLoader(loaderId, bundle, LoaderCallbacks)方法,該方法會啟動一個指定ID的Loader。

  • 該方法的第一個引數loaderId指的是我們要啟動的Loader的ID,如果我們想要啟動多個不同用途的Loader,那麼我們就要給不同用途的Loader設定不同的loaderId,如果我們的Activity中只用到了一個Loader,那麼此處的loaderId就不那麼重要了,隨便一個數字即可,比如0。
  • 該方法的第二個引數是Bundle物件,該物件攜帶了我們要建立Loader時所需要的必要資訊,當然如果我們沒有必要資訊需要傳遞,那麼第二個引數可以傳遞null。
  • 該方法的第三個引數是一個LoaderCallbacks物件,由於我們的MainActivity實現了LoaderCallbacks介面,所以我們此處就傳遞了this。

4.在執行了LoaderManager的initLoader()方法之後,Android會首先查詢LoaderManager中有沒有指定loaderId的Loader,如果沒有,就會執行LoaderCallbacks的onCreateLoader()方法去建立一個Loader,onCreateLoader()方法是個回撥方法,該方法中的loaderId引數和Bundle引數都是在上面提到的LoaderManager的initLoader(loaderId, bundle, LoaderCallbacks)中傳遞過來的。由於我們的Activity中在業務邏輯上只用到一種型別的Loader,所以我們無需在onCreateLoader()方法中判斷loaderId的型別做判斷處理。然後我們根據傳入的bundle物件判斷是要查詢全部聯絡人還是查詢包含指定關鍵字的聯絡人,並基於建立了用於查詢ContentProvider的Uri和sql語句,並將其作為CursorLoader()的建構函式,從而得到CursorLoader的例項物件。此處需要注意的是,雖然LoaderManager的initLoader()方法會返回一個Loader物件,但是我們在Activity中不應該儲存該Loader物件,因為客戶端不應該直接呼叫Loader的方法,所以儲存一個Loader物件也是無意義的。在執行了LoaderManager的initLoader()方法之後,Android會將該方法得到的Loader物件儲存在LoaderManager中。

5.當我們建立的CursorLoader物件獲取到Cursor資料的時候,Android會執行onLoadFinished()回撥方法,在該方法的的形參中我們可以得到Cursor資料物件,然後呼叫adapter.swapCursor(data)方法,這樣我們的ListView就會用我們得到的Cursor物件顯示UI了。

6.上面的過程演示了我們的應用在一開始的時候會顯示所有的聯絡人資訊,當我們文字編輯框中輸入關鍵字的時候,會觸發afterTextChanged()方法的執行,在該方法中我們又構建了一個Bundle物件,並將文字編輯框中的關鍵字放入到Bundle物件中,然後我們呼叫了LoaderManager的restartLoader(loaderId, Bundle, LoaderCallbacks)方法,該方法與initLoader(loaderId, Bundle, LoaderCallbacks)方法中的形參列表完全相同。當我們呼叫restartLoader()方法時,LoaderManager會首先查詢看一下LoaderManager中是否已經存在該指定loaderId的Loader,如果有的話先標記一下。然後重新執行LoaderCallbacks的onCreateLoader()方法,不過這次由於Bundle中含有關鍵字資訊,所以我們會利用該關鍵字構建Uri及sql語句,從而構建新的Loader,從而根據關鍵字從ContentProvider中過濾並載入新的資料。當該新的Loader完成載入資料之後,LoaderManager會首先看一下有沒有標記過該loaderId對應的老的Loader,如果有的話會執行LoaderCallbacks的onLoaderReset()方法,從而告知客戶端老的Cursor物件廢棄無用了,所以我們在onLoaderReset()中執行adapter.swapCursor(null),從而是客戶端取消對原有Cursor物件的引用,在這之後LoaderManager會銷燬該loaderId的老的Loader。然後LoaderManager又會馬上將新的Loader得到的資料傳遞給LoaderCallbacks方法的onLoadFinished()方法中,這樣我們就在onLoadFinished()中獲取到了新的資料,並通過呼叫adapter.swapCursor(data)而使客戶端能夠應用到新的Cursor物件。

以上就是我們全部的邏輯程式碼,需要注意的是,我們在以上程式碼中並沒有呼叫LoaderManager的destroyLoader()方法,因為在Activity和Fragment銷燬執行onDestroy()的時候,Android會執行LoaderManager的doDestroy()方法,該方法會銷燬LoaderManager中所儲存的所有的Loader物件。需要注意的是,LoaderManager的doDestroy()方法不是一個public方法,所以在API文件中看不到,在原始碼中可以看到。


使用support v4 支援庫

我們在上面提到,Loader機制是從Android 3.0才開始引入的,那麼是不是Android 3.0之前我們就不能使用Loader了呢?

其實在Android 3.0之前,我們也可以使用Loader,方案就是使用Android提供的support v4支援庫。

這裡簡單介紹一下Android的support支援庫。Android系統在不斷更新,每個版本都會推出許多重要的新的類和特性,那麼為了讓低版本的Android系統也能使用這些新的特性,Google的工程師們開發了許多向後支援庫,目前有v4、v7、v8、v13、v14、v17等版本。支援庫的命名原則很簡單,就是”v”加上從開始支援的API Level,比如support v4支援從API Level 4開始的Android系統,即從Android 1.6開始可以使用support v4;support v7支援從API Level 7開始的Android系統,即從Android 2.1開始可以使用support v7。每個版本的support支援庫都對應一個jar包,在使用時我們需要引入對應的jar包。需要注意的是,高版本的support支援庫需要依賴低版本的support支援庫,比如要使用support v7支援庫,那麼我們也必須同時引入support v4支援庫,因為v7依賴v4。

每個support版本支援的特性不一樣,拿support v4支援庫來說,v4最大的作用就是可以使低版本的Android(1.6+)的系統可以使用Fragment和Loader,所以為了讓低版本的Android能夠使用Loader,我們需要使用support v4支援庫,我們要對以上的原始碼進行一些類的替換,具體方法如下:

1.首先在MainActivity開頭要引入support v4相關的類,即首先註釋掉以下程式碼:

import android.app.Activity;
import android.app.LoaderManager;
import android.content.Loader;
import android.content.CursorLoader;
import android.widget.SimpleCursorAdapter;

然後將其替換成如下的support v4的相關類:

import android.support.v4.app.FragmentActivity;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.Loader;
import android.support.v4.content.CursorLoader;
import android.support.v4.widget.SimpleCursorAdapter;

2.讓我們的MainActivity繼承自android.support.v4.app.FragmentActivity而非直接繼承自android.app.Activity

3.將系統中兩處呼叫getLoaderManager()的地方替換成getSupportLoaderManager()getSupportLoaderManager()android.support.v4.app.FragmentActivity中的方法,該方法返回android.support.v4.app.LoaderManager的例項,而非android.app.LoaderManager

原始碼下載連結:
http://download.csdn.net/detail/sunqunsunqun/9150955

如果想深入瞭解Loader的生命週期以及Loader、AsyncTaskLoader、CursorLoader、LoaderManager的原始碼執行過程,可參見博文《深入原始碼解析Android中Loader、AsyncTaskLoader、CursorLoader、LoaderManager》

後面會寫一篇如何編寫自定義Loader的文章。

希望本文對大家Loader的使用有所幫助。

相關閱讀:
我的Android博文整理彙總
深入原始碼解析Android中Loader、AsyncTaskLoader、CursorLoader、LoaderManager
使用詳解及原始碼解析Android中的Adapter、BaseAdapter、ArrayAdapter、SimpleAdapter和SimpleCursorAdapter

相關文章