Cursor原理解析及自定义

3,733 阅读8分钟

这篇文章会分成两部分进行讲述以帮助大家更好地理解Cursor的使用方法。

  • IPC获取Cursor原理、Cursor读取原理
    了解通过ContentProvider获取Cursor的原理以及过程中一些关键点;了解Client在拿到Cursor资源对象后获取其中内容、移动游标位置的原理。
  • 自定义Cursor
    如何通过ContentProvider返回一个自定义Cursor。

IPC获取Cursor原理

从通过ContentResolver拿到 ContentProvider.query() 返回的查询结果Cursor,到操作Cursor的位置指针获取其中的内容,再到最后通过 close() 方法释放Cursor资源。这一系列过程中涵盖了大量的Binder IPC操作,本小节通过解析源码和IPC通信的过程来揭示Client APP是如何拿到可使用Cursor对象的。

一、IBulkCursor

IBulkCursor Binder接口为Cursor IPC操作提供控制支持,是除去Cursor本身最重要的辅助类。例如在Client APP中使用 Cursor.moveToNext() 移动指示行的标识位置时,正是通过 IBulkCursor.onMove() 映射到Server端的Cursor对象。

IBulkCursor Binder IPC接口及实现类:

  • CursorToBulkCursorAdaptor 定义为final类,是IBulkCursor中所定义方法的具体实现所在。作用是接收Client对Cursor操作的IPC请求,完成远端操作对原始Cursor对象的映射。
  • CursorToBulkCursorAdaptor 通过使用 监视器锁 来保证IPC调用时,IBulkCursor操作的原子性。
    @Override
    public void close() {
        synchronized (mLock) {
            disposeLocked();
        }
    }

举个例子: 当在Client APP中使用Cursor的moveToNext()方法时,会调用到IBulkCursor接口的onMove()方法,通过Binder IPC到CursorToBulkCursorAdaptor的onMove()实现中。 CursorToBulkCursorAdaptor.java

    @Override
    public void onMove(int position) {
        synchronized (mLock) {
            throwIfCursorIsClosed();

            mCursor.onMove(mCursor.getPosition(), position);
        }
    }

由此完成Client APP在移动Cursor的游标位置后,真正Cursor对象的游标位置也能随之移动。

NOTE: 需要注意的是在当前Android SDK的实现里面,客户端对Cursor的读取操作和游标移动都是代理给了其中的CursorWindow。IBulkCursor、及其Native、Proxy也都是隐藏的API。对于应用工程师来说,了解到在执行Cursor的requery()、close()等方法时会调用IBulkCursor的相关接口通知Server端就够了。

二、如何拿到Cursor

Note: 这里不涉及Server如何查询生成Cursor对象,以及Client如何通过ContentResolver调用ContentProvider接口(这些都是ContentProvider的内容)。

1.ContentProviderNative中,可以看到是如何将Cursor对象的操作句柄传送给Client APP的:

...
Cursor cursor = query(url, projection, selection, selectionArgs, sortOrder,
        cancellationSignal);
if (cursor != null) {
    CursorToBulkCursorAdaptor adaptor = new CursorToBulkCursorAdaptor(
            cursor, observer, getProviderName());
    BulkCursorDescriptor d = adaptor.getBulkCursorDescriptor();

    reply.writeNoException();
    reply.writeInt(1);
    d.writeToParcel(reply, Parcelable.PARCELABLE_WRITE_RETURN_VALUE);
} else {
    reply.writeNoException();
    reply.writeInt(0);
}
...

步骤:

  • 将query()生成的Cursor包装在控制辅助类CursorToBulkCursorAdaptor中。
  • 生成操作句柄BulkCursorDescriptor。
  • 成功标识符(1) 以及 BulkCursorDescriptor 放入Parcel中,写回给Client APP。

2.ContentProviderProxy中,则可以看到是如何通过句柄生成一个可以被使用的Cursor对象并返回给调用者的。

...
mRemote.transact(IContentProvider.QUERY_TRANSACTION, data, reply, 0);
DatabaseUtils.readExceptionFromParcel(reply);
if (reply.readInt() != 0) {
    BulkCursorDescriptor d = BulkCursorDescriptor.CREATOR.createFromParcel(reply);
    adaptor.initialize(d);  //adaptor的类型为BulkCursorToCursorAdaptor
} else {
    adaptor.close();
    adaptor = null;
}
return adaptor;
...

调用者拿到的是类型为BulkCursorToCursorAdaptor的Cursor对象。 BulkCursorToCursorAdaptor适配器将IBulkCursor适配成Cursor,让Client APP在执行很多操作时完全感受不到IBulkCursor的IPC调用。

3. BulkCursorDescriptor

  • 通过句柄拿到了Cursor对象Binder IPC的操作接口IBulkCursor。Server端的IBulkCursor实现类对象CursorToBulkCursorAdaptor持有真正Cursor资源对象的引用
  • 列名数组以及row数量等重要信息。
  • CursorWindow。携带着多条Cursor row的数据结构,实现了Parcelable接口,可以通过Binder IPC在进程间传递。在Client APP中使用Cursor接口读取的内容便来自CursorWindow

Cursor读取原理

经过上面一个小节了解了Cursor及其关联对象是如何通过Binder IPC在Server/Client两端传递的。本小节就看看系统是如何在Client中使用通过ContentResolver拿到的Cursor对象。

Cursor接口及其实现类图:

先对其中几个重要的类做一下说明:

  • AbstractCursor: 主要目标在于实现跟Cursor游标位置、Observer有关的一些操作。
...
    public final boolean move(int offset) {
        return moveToPosition(mPos + offset);
    }

    public final boolean moveToFirst() {
        return moveToPosition(0);
    }

    public final boolean moveToLast() {
        return moveToPosition(getCount() - 1);
    }

    public final boolean moveToNext() {
        return moveToPosition(mPos + 1);
    }

    public final boolean moveToPrevious() {
        return moveToPosition(mPos - 1);
    }

    public final boolean isFirst() {
        return mPos == 0 && getCount() != 0;
    }

    public final boolean isLast() {
        int cnt = getCount();
        return mPos == (cnt - 1) && cnt != 0;
    }
    
...
  • AbstractWindowedCursor:
    首先,作为AbstractCursor的直接子类,应该完成的最主要任务是实现从每一个Cursor row中获取相关信息的逻辑。如:
...
    @Override
    public int getInt(int columnIndex) {
        checkPosition();
        return mWindow.getInt(mPos, columnIndex);
    }

    @Override
    public long getLong(int columnIndex) {
        checkPosition();
        return mWindow.getLong(mPos, columnIndex);
    }

    @Override
    public float getFloat(int columnIndex) {
        checkPosition();
        return mWindow.getFloat(mPos, columnIndex);
    }

    @Override
    public double getDouble(int columnIndex) {
        checkPosition();
        return mWindow.getDouble(mPos, columnIndex);
    }
...

与MaxtrixCursor、SortCursor等不同的是,在这里所有的Cursor数据装载到CursorWindow中。

一、Client中Cursor对象调用关系

在Client中通过ContentResolver的query()方法拿到Cursor对象并不是前面看到通过Binder IPC返回的BulkCursorDescriptor对象生成的Cursor操作具体实现类BulkCursorToCursorAdaptor。而是外面包了一层装饰者CursorWrapperInner:

ContentResolver.java

// query() 方法节选
Cursor qCursor;
try {
    qCursor = unstableProvider.query(uri, projection,
            selection, selectionArgs, sortOrder, remoteCancellationSignal);
} catch (DeadObjectException e) {
...
    qCursor = stableProvider.query(uri, projection,
            selection, selectionArgs, sortOrder, remoteCancellationSignal);
}
if (qCursor == null) {
    return null;
}
...
CursorWrapperInner wrapper = new CursorWrapperInner(qCursor,
        stableProvider != null ? stableProvider : acquireProvider(uri));
stableProvider = null;
return wrapper;

CursorWrapperInner的作用是在释放Cursor资源时顺带减少Cursor关联的ContentProvider引用计数。稍微了解装饰者设计模式就可以一眼看出来真正执行Cursor接口相关方法的是传给CursorWrapperInner构造器中的 qCursor对象。 CursorWrapperInner的父类CursorWrapper也印证了这一点:

...
    public int getInt(int columnIndex) {
        return mCursor.getInt(columnIndex);
    }

    public long getLong(int columnIndex) {
        return mCursor.getLong(columnIndex);
    }

    public short getShort(int columnIndex) {
        return mCursor.getShort(columnIndex);
    }

    public String getString(int columnIndex) {
        return mCursor.getString(columnIndex);
    }
...

跟上一小节 Binder IPC原理 串联起来,可以得出以下小结:
Client在平时操作从ContentResolver拿到的Cursor对象时,本质就是访问通过ContentProvider IPC拿到的BulkCursorToCursorAdaptor。 通过前面的介绍,我们知道BulkCursorToCursorAdaptor是AbstractWindowedCursor的子类,则Client在访问Cursor中内容时,便是转换给CursorWindow对象来执行。该CursorWindow对象由ContentProvider的Server生成并返回给调用Client。

二、CursorWindow操作原理

什么是CursorWindow?

1. 一个包含了多条Cursor row的内存缓冲区。
2. 在Server端创建时处于可读写的状态,如果一直在该进程内使用也会一直是可读写;但是如果经过Parcel的IPC传送之后,在远端进程中只能是只读的状态。
3. 典型的生产者-消费者结构,在生产者Server端分配内存空间,填充数据然后发回给Client端,而Client APP仅仅读取其中的内容。
4. 无论读还是写都是native方法,Java只是包了一层Java API。

以putLong()方法为例,看看是如何使用这片缓存区的:

//Java 方法
public boolean putLong(long value, int row, int column) {
    acquireReference();
    try {
        return nativePutLong(mWindowPtr, value, row - mStartPos, column);
    } finally {
        releaseReference();
    }
}

//native 方法
status_t CursorWindow::putLong(uint32_t row, uint32_t column, int64_t value) {
    if (mReadOnly) {
        return INVALID_OPERATION;
    }

    FieldSlot* fieldSlot = getFieldSlot(row, column);
    if (!fieldSlot) {
        return BAD_VALUE;
    }

    fieldSlot->type = FIELD_TYPE_INTEGER;
    fieldSlot->data.l = value;
    return OK;
}
  • mReadOnly为true时则不允许写入。mReadOnly在调用create()方法构造CursorWindow时会被赋值为false;在通过createFromParcel反序列化回CursorWindow时会被赋值为true。
  • 空间不足时则不允许写入,返回false。
  • 缓存区不是直接保存数值,而是保存一种数据结构FieldSlot。FieldSlot的数据结构如下:
    struct FieldSlot {
    private:
        int32_t type;   //类型描述
        union {
            double d;   //float、double使用
            int64_t l;  //int、long使用
            struct {    //blot、String、Null使用...
                uint32_t offset;
                uint32_t size;
            } buffer;
        } data;

        friend class CursorWindow;
    } __attribute((packed));

以上可得出结论:CursorWindow拥有一块共享的内存区域,只要能正确调整指向不同内存区域的位置指针,就能够拿到对应的Cursor值。平时所理解的行、列索引,其实代表着指向着特定内存区域的指针。

自定义Cursor

对于Cursor的使用,按照使用区域的不同可以分成 生成进程内使用跨进程使用。在生成Cursor进程内使用Cursor的场景很少,同时Android SDK本身也提供了很多API为这个场景提供支持(如上面提到的MatrixCursorMergeCursor)。 因此这里我们只聚焦在Cursor最主要使用的场景 —— 跨进程使用
虽然根据获取的方式不同,跨进程使用还可以分为:

  • 通过ContentProvider获取
  • 通过自定义Binder IPC接口获取

但是由于Cursor IPC传递的本质还是传递CursorWindow,从数据流动的角度来说都是一样的。这里我想通过第一种方式就足以证明如何自定义一个在IPC过程中传送的Cursor。

通过ContentProvider获取

通过ContentProvider获取Cursor流程一般都是这样的:通过ContentResolver查询到ContentProvider Binder IPC的接口,然后获取query() 调用返回的Cursor。第一小节的 IPC获取Cursor原理 亦是基于这一套流程进行分析。

1.实现一个继承自AbstractWindowedCursor的实现类

class MyWindowedCursor(window: CursorWindow) : AbstractWindowedCursor() {
    init {
        setWindow(window)
    }
    //定义Client拿到的Cursor的行数
    override fun getCount(): Int {
        return window.numRows
    }
    //定义Cursor的每一列的列名
    override fun getColumnNames(): Array<String> {
        return arrayOf("key", "value")
    }
}

2.实现ContentProvider,并将填充好内容的CursorWindow装进Cursor中并返回

    override fun query(uri: Uri, projection: Array<String>?, selection: String?,
                       selectionArgs: Array<String>?, sortOrder: String?): Cursor? {
        val window = CursorWindow("CursorProvider")
        // 必须先要设置每一行有多少列才能往里面添加数据
        window.setNumColumns(2)
        // 分配每一行的内存地址,建议每次需要新添加一行时新分配一行的内存。
        window.allocRow()
        // 行、列索引都是从零开始
        window.putString("key1", 0, 0)
        window.putLong(1, 0, 1)
        window.allocRow()
        window.putString("key2", 1, 0)
        window.putLong(2, 1, 1)
        Log.d("Test", "current rows: ${window.numRows}")
        return MyWindowedCursor(window)
    }

ContentProvider的定义:

        <provider
            android:name=".CursorProvider"
            android:authorities="com.wusp.cursor"
            android:enabled="true"
            android:exported="true"
            android:multiprocess="false"
            android:process=":cursor_provider" />

至此,就因为完成了自定义Cursor最基本的实现。只要Client调用到该ContentProvider的query()接口,就能够拿到往自定义Cursor类MyWindowedCursor中填充的CursorWindow,并将其包装在CursorWrapperInner中。

3.测试 在APP中运行下面的代码测试一下是否能正常拿到自定义Cursor类型中的内容:

        val uri: Uri = Uri.parse("content://com.wusp.cursor/test")
        val cursor = contentResolver.query(uri, null, null, null, null)
        Log.d("Test", "Activity cursor count: ${cursor.count}, ${cursor.columnCount}")
        while (cursor.moveToNext()) {
            Log.d("Test", "Cursor.Content: ${cursor.getString(0)} - ${cursor.getInt(1)}")
        }
        cursor.close()

测试结果:

D/Test: Activity cursor count: 2, 2
D/Test: Cursor.Content: key1 - 1
D/Test: Cursor.Content: key2 - 2