Android中CursorLoader的使用、原理及注意事項


前言

最近在項目中涉及到了即時聊天,因此不可避免地要用到實時刷新的功能,因為在以前的項目中見到別人使用CursorLoader+CursorAdapter+ContentProvider的機制來實現實時刷新,於是沒有多研究就直接照搬了這個機制,直到后來出現了發送消息后不能更新到界面上的問題,查了很久也查不出原因,於是就想從這個機制本身出發,看看有可能是在哪個環節出了問題。

使用

1.讓Activity或Fragment實現LoaderManager.LoaderCallbacks< D >接口

    由於我們的數據存儲在數據庫中,因此這里的泛型應該替換為Cursor 
    這個接口中有三個方法:

// 這個方法在初始化Loader時回調,我們要在這個方法中實例化CursorLoader
public Loader<D> onCreateLoader(int id, Bundle args);
// 加載數據完成后回調到這個方法,我們一般在這里調用CursorAdapter的changeCursor或swapCursor進行界面刷新的操作
public void onLoadFinished(Loader<D> loader, D data);
// 這個方法是在重啟Loader時調用,一般可以不管
public void onLoaderReset(Loader<D> loader);

2.創建對應的ContentProvider

@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
        synchronized (DBLOCK) {
            SQLiteDatabase db = WeChatDBManager.getInstance(getContext()).getDatabase();
            SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
            Cursor cursor = null;
            switch (sUriMatcher.match(uri)) {
                case CODE_CHAT_HISTORY:
                    queryBuilder.setDistinct(false);
                    queryBuilder.setTables(uri.getQuery());
                    cursor = queryBuilder.query(db, 
                            projection, 
                            selection, 
                            selectionArgs,
                            null, 
                            null, 
                            sortOrder 
                    );
                    break;
            }
            // 對查詢到的結果集對應的Uri設置觀察者
            if (cursor != null)
                cursor.setNotificationUri(getContext().getContentResolver(), uri);
            return cursor;
        }
}

@Override
    public Uri insert(Uri uri, ContentValues values) {
        ...
        // 通知對應的Uri數據發生改變
        getContext().getContentResolver().notifyChange(uri, null);
    }

    @Override
    public int delete(Uri uri, String selection, String[] selectionArgs) {
        ...
        // 通知對應的Uri數據發生改變
        getContext().getContentResolver().notifyChange(uri, null);
    }

    @Override
    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
        ...
        // 通知對應的Uri數據發生改變
        getContext().getContentResolver().notifyChange(uri, null);
    }

3.調用getLoaderManager().initLoader(int id, Bundle args, LoaderManager.LoaderCallbacks< D> callback)初始化

原理

1.initLoader

public <D> Loader<D> initLoader(int id, Bundle args, LoaderManager.LoaderCallbacks<D> callback) {
        if (mCreatingLoader) {
            throw new IllegalStateException("Called while creating a loader");
        }

        LoaderInfo info = mLoaders.get(id);

        if (DEBUG) Log.v(TAG, "initLoader in " + this + ": args=" + args);

        if (info == null) {
            // 創建Loader
            info = createAndInstallLoader(id, args,  (LoaderManager.LoaderCallbacks<Object>)callback);
            if (DEBUG) Log.v(TAG, "  Created new loader " + info);
        } else {
            if (DEBUG) Log.v(TAG, "  Re-using existing loader " + info);
            info.mCallbacks = (LoaderManager.LoaderCallbacks<Object>)callback;
        }

        if (info.mHaveData && mStarted) {
            // Loader中已經有數據,這里最終會回調到onLoadFinished方法
            info.callOnLoadFinished(info.mLoader, info.mData);
        }

      return (Loader<D>)info.mLoader
}

這里主要關注createAndInstallLoader方法

  private LoaderInfo createAndInstallLoader(int id, Bundle args,
            LoaderManager.LoaderCallbacks<Object> callback) {
        try {
            mCreatingLoader = true;
            // 這里首先會創建LoaderInfo對象
            LoaderInfo info = createLoader(id, args, callback);
            // 然后啟動LoaderInfo
            installLoader(info);
            return info;
        } finally {
            mCreatingLoader = false;
        }
    }

首先來看看createLoader

  private LoaderInfo createLoader(int id, Bundle args,
            LoaderManager.LoaderCallbacks<Object> callback) {
        LoaderInfo info = new LoaderInfo(id, args,  callback);
        // 這里回調到了我們要實現的onCreateLoader方法
        Loader<Object> loader = callback.onCreateLoader(id, args);
        info.mLoader = loader;
        return info;
    }

在onCreateLoader中我們創建了具體的Loader,即CursorLoader

  @Override
    public Loader<Cursor> onCreateLoader(int id, Bundle args) {
        return new CursorLoader(...);
    }

接着執行到installLoader

void installLoader(LoaderInfo info) {
        // 把上一步創建的LoaderInfo對象存到列表中
        mLoaders.put(info.mId, info);
        if (mStarted) {
            // 啟動Loader
            info.start();
        }
    }

    void start() {
            ...
            // start方法中我們只關注startLoading方法
            mLoader.startLoading();
            ...
        }

    public final void startLoading() {
        mStarted = true;
        mReset = false;
        mAbandoned = false;
        // onStartLoading是個空方法,我們要看CursorLoader中的具體實現
        onStartLoading();
    }

    @Override
    protected void onStartLoading() {
        // 更新數據
        if (mCursor != null) {
            deliverResult(mCursor);
        }
        // 初始化時調用 主要看這里,這里又調到父類Loader中的forceLoad
        if (takeContentChanged() || mCursor == null) {
            forceLoad();
        }
    }

    public void forceLoad() {
        // 這里的onForceLoad又是一個空方法,調用的是子類AsyncTaskLoader中的onForceLoad
        onForceLoad();
    }

    @Override
    protected void onForceLoad() {
        super.onForceLoad();
        cancelLoad();
        // 這里執行了一個異步任務,接下來看看這個異步任務具體做了什么事
        mTask = new LoadTask();
        if (DEBUG) Log.v(TAG, "Preparing load: mTask=" + mTask);
        executePendingTask();
    }

接下來具體看一下這個異步任務,具體關注其中的doInBackground和onPostExecute

 @Override
    protected D doInBackground(Void... params) {
        if (DEBUG) Log.v(TAG, this + " >>> doInBackground");
        try {
            // 這里執行了onLoadInBackground
            D data = AsyncTaskLoader.this.onLoadInBackground();
            if (DEBUG) Log.v(TAG, this + "  <<< doInBackground");
            return data;
        } catch (OperationCanceledException ex) {
            if (!isCancelled()) {
                throw ex;
            }
            if (DEBUG) Log.v(TAG, this + "  <<< doInBackground (was canceled)", ex);
            return null;
        }
    }

    protected D onLoadInBackground() {
        // 這里調用到CursorLoader的loadInBackground
        return loadInBackground();
    }

    @Override
    public Cursor loadInBackground() {
        synchronized (this) {
            if (isLoadInBackgroundCanceled()) {
                throw new OperationCanceledException();
            }
            mCancellationSignal = new CancellationSignal();
        }
        try {
            // 這里調用ContentResolver進行查詢,查詢條件就是前面我們在onCreateLoader創建CursorLoader對象時
            // 傳入的,這里最終會調用我們的ContentProvider,我們在ContentProvider的query中對Cursor對象設置了監聽的Uri
            Cursor cursor = ContentResolverCompat.query(getContext().getContentResolver(),
                    mUri, mProjection, mSelection, mSelectionArgs, mSortOrder,
                    mCancellationSignal);
            if (cursor != null) {
                try {
                    // 這里給Cursor對象注冊了一個內容觀察者,而在上面我們設置了要監聽的Uri,因此當數據變化時,首先會通知Cursor,然后Cursor再觸發ForceLoadContentObserver中的onChange
                    cursor.getCount();
                    cursor.registerContentObserver(mObserver);
                } catch (RuntimeException ex) {
                    cursor.close();
                    throw ex;
                }
            }
            return cursor;
        } finally {
            synchronized (this) {
                mCancellationSignal = null;
            }
        }
    }

    public final class ForceLoadContentObserver extends ContentObserver {
        public ForceLoadContentObserver() {
            super(new Handler());
        }

        @Override
        public boolean deliverSelfNotifications() {
            return true;
        }

        @Override
        public void onChange(boolean selfChange) {
            onContentChanged();
        }
    }

    public void onContentChanged() {
        if (mStarted) {
            // 這里又回到了forceLoad方法,接下來就是重復一遍上面的流程
            forceLoad();
        } else {
            mContentChanged = true;
        }
    }

異步任務最后會執行onPostExecute

  @Override
    protected void onPostExecute(D data) {
        if (DEBUG) Log.v(TAG, this + " onPostExecute");
        try {
            AsyncTaskLoader.this.dispatchOnLoadComplete(this, data);
        } finally {
            mDone.countDown();
        }
    }

    void dispatchOnLoadComplete(LoadTask task, D data) {
        if (mTask != task) {
            if (DEBUG) Log.v(TAG, "Load complete of old task, trying to cancel");
            dispatchOnCancelled(task, data);
        } else {
            if (isAbandoned()) {
                // This cursor has been abandoned; just cancel the new data.
                onCanceled(data);
            } else {
                commitContentChanged();
                mLastLoadCompleteTime = SystemClock.uptimeMillis();
                mTask = null;
                if (DEBUG) Log.v(TAG, "Delivering result");
                // 傳遞數據
                deliverResult(data);
            }
        }
    }

    public void deliverResult(D data) {
        if (mListener != null) {
            // 回調到LoaderManager中的onLoadComplete
            mListener.onLoadComplete(this, data);
        }
    }

    @Override
    public void onLoadComplete(Loader<Object> loader, Object data) {
        ...
        // 這里我們只關注callOnLoadFinished,這個方法中最終會回調到我們的onLoadFinished
        if (mData != data || !mHaveData) {
            mData = data;
            mHaveData = true;
            if (mStarted) {
                callOnLoadFinished(loader, data);
            }
        }
        ...
    }

    void callOnLoadFinished(Loader<Object> loader, Object data) {
            if (mCallbacks != null) {
                String lastBecause = null;
                if (mHost != null) {
                    lastBecause = mHost.mFragmentManager.mNoTransactionsBecause;
                    mHost.mFragmentManager.mNoTransactionsBecause = "onLoadFinished";
                }
                try {
                    if (DEBUG) Log.v(TAG, "  onLoadFinished in " + loader + ": "
                            + loader.dataToString(data));
                    // 回調到我們的onLoadFinished
                    mCallbacks.onLoadFinished(loader, data);
                } finally {
                    if (mHost != null) {
                        mHost.mFragmentManager.mNoTransactionsBecause = lastBecause;
                    }
                }
                mDeliveredData = true;
            }
        }

整個流程還是比較清晰的,再梳理一遍:

  1. 初始化並啟動Loader,會在createLoader方法中回調我們的onCreateLoader,我們在這里生成一個CursorLoader對象,其中設置了要查詢的條件
  2. 在CursorLoader的onForceLoad實現中有一個異步任務,這個異步任務的loadInBackground方法中根據我們設置的查詢條件查詢數據庫,最終會調用到我們的ContentProvider的query方法進行查詢
  3. 查詢完成會得到一個Cursor對象,我們調用cursor.setNotificationUri(getContext().getContentResolver(), uri)為這個Cursor設置要監聽的Uri,同時CursorLoader會為這個Cursor對象注冊一個內容觀察者ForceLoadContentObserver
  4. 異步任務執行完成后會回調我們的onLoadFinished方法,我們在onLoadFinished方法中調用CursorAdapter的changeCursor或swapCursor方法,最終就能讓我們的界面自動刷新
  5. 當我們用ContentProvider增刪改數據時,只需在最后調用getContext().getContentResolver().notifyChange(uri, null),就會通知到上面的Cursor對象(因為Uri相同),再由Cursor觸發內容觀察者的onChange方法,最終又會調用到onForceLoad,重復上述過程

遇到的問題

根據上面的流程我們可以知道,每次數據發生改變時,最后都會觸發loadInBackground中的查詢,但是這里的查詢條件一直是我們在創建CursorLoader時設置的查詢條件,而我的項目中涉及到了分頁查詢(應用場景就是類似手機qq查看歷史聊天記錄),發生的問題就是當新增的數據達到一定數量時,界面就不會更新了,即如果在查詢條件中含有動態改變的limit條件(如分頁查詢時的頁數),就會產生問題。

解決方法

我的解決方法是每次數據庫變化之后都通過CursorLoader的一系列set方法更新查詢條件


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM