前言
最近在項目中涉及到了即時聊天,因此不可避免地要用到實時刷新的功能,因為在以前的項目中見到別人使用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; } }
整個流程還是比較清晰的,再梳理一遍:
- 初始化並啟動Loader,會在createLoader方法中回調我們的onCreateLoader,我們在這里生成一個CursorLoader對象,其中設置了要查詢的條件
- 在CursorLoader的onForceLoad實現中有一個異步任務,這個異步任務的loadInBackground方法中根據我們設置的查詢條件查詢數據庫,最終會調用到我們的ContentProvider的query方法進行查詢
- 查詢完成會得到一個Cursor對象,我們調用cursor.setNotificationUri(getContext().getContentResolver(), uri)為這個Cursor設置要監聽的Uri,同時CursorLoader會為這個Cursor對象注冊一個內容觀察者ForceLoadContentObserver
- 異步任務執行完成后會回調我們的onLoadFinished方法,我們在onLoadFinished方法中調用CursorAdapter的changeCursor或swapCursor方法,最終就能讓我們的界面自動刷新
- 當我們用ContentProvider增刪改數據時,只需在最后調用getContext().getContentResolver().notifyChange(uri, null),就會通知到上面的Cursor對象(因為Uri相同),再由Cursor觸發內容觀察者的onChange方法,最終又會調用到onForceLoad,重復上述過程
遇到的問題
根據上面的流程我們可以知道,每次數據發生改變時,最后都會觸發loadInBackground中的查詢,但是這里的查詢條件一直是我們在創建CursorLoader時設置的查詢條件,而我的項目中涉及到了分頁查詢(應用場景就是類似手機qq查看歷史聊天記錄),發生的問題就是當新增的數據達到一定數量時,界面就不會更新了,即如果在查詢條件中含有動態改變的limit條件(如分頁查詢時的頁數),就會產生問題。
解決方法
我的解決方法是每次數據庫變化之后都通過CursorLoader的一系列set方法更新查詢條件