Cursor原理解析及自定义

这篇文章会分成两部分进行讲述以帮助大家更好地理解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操作的原子性。
    1
    2
    3
    4
    5
    6
    @Override
    public void close() {
    synchronized (mLock) {
    disposeLocked();
    }
    }

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

1
2
3
4
5
6
7
8
@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的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
...
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对象并返回给调用者的。

1
2
3
4
5
6
7
8
9
10
11
12
...
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有关的一些操作。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
...
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中获取相关信息的逻辑。如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
...
@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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 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也印证了这一点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
...
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读取原理

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//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的数据结构如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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的实现类

1
2
3
4
5
6
7
8
9
10
11
12
13
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中并返回

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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的定义:

1
2
3
4
5
6
7
<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类型中的内容:

1
2
3
4
5
6
7
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()

测试结果:

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