Android中CursorLoader的使用、原理及注意事项
阅读原文时间:2023年07月10日阅读:2

前言

最近在项目中涉及到了即时聊天,因此不可避免地要用到实时刷新的功能,因为在以前的项目中见到别人使用CursorLoader+CursorAdapter+ContentProvider的机制来实现实时刷新,于是没有多研究就直接照搬了这个机制,直到后来出现了发送消息后不能更新到界面上的问题,查了很久也查不出原因,于是就想从这个机制本身出发,看看有可能是在哪个环节出了问题。

使用

1.让Activity或Fragment实现LoaderManager.LoaderCallbacks< D >接口

由于我们的数据存储在数据库中,因此这里的泛型应该替换为Cursor 
    这个接口中有三个方法:

// 这个方法在初始化Loader时回调,我们要在这个方法中实例化CursorLoader
public Loader onCreateLoader(int id, Bundle args);
// 加载数据完成后回调到这个方法,我们一般在这里调用CursorAdapter的changeCursor或swapCursor进行界面刷新的操作
public void onLoadFinished(Loader loader, D data);
// 这个方法是在重启Loader时调用,一般可以不管
public void onLoaderReset(Loader 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 Loader initLoader(int id, Bundle args, LoaderManager.LoaderCallbacks 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 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 callback) {
LoaderInfo info = new LoaderInfo(id, args, callback);
// 这里回调到了我们要实现的onCreateLoader方法
Loader loader = callback.onCreateLoader(id, args);
info.mLoader = loader;
return info;
}

在onCreateLoader中我们创建了具体的Loader,即CursorLoader

@Override
public Loader 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方法更新查询条件