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 | @Override |
由此完成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 | ... |
步骤:
- 将query()生成的Cursor包装在控制辅助类CursorToBulkCursorAdaptor中。
- 生成操作句柄BulkCursorDescriptor。
- 将 成功标识符(1) 以及 BulkCursorDescriptor 放入Parcel中,写回给Client APP。
2.在ContentProviderProxy中,则可以看到是如何通过句柄生成一个可以被使用的Cursor对象并返回给调用者的。
1 | ... |
调用者拿到的是类型为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 | ... |
- AbstractWindowedCursor:
首先,作为AbstractCursor的直接子类,应该完成的最主要任务是实现从每一个Cursor row中获取相关信息的逻辑。如:
1 | ... |
与MaxtrixCursor、SortCursor等不同的是,在这里所有的Cursor数据装载到CursorWindow中。
一、Client中Cursor对象调用关系
在Client中通过ContentResolver的query()方法拿到Cursor对象并不是前面看到通过Binder IPC返回的BulkCursorDescriptor对象生成的Cursor操作具体实现类BulkCursorToCursorAdaptor。而是外面包了一层装饰者CursorWrapperInner:
ContentResolver.java
1 | // query() 方法节选 |
CursorWrapperInner的作用是在释放Cursor资源时顺带减少Cursor关联的ContentProvider引用计数。稍微了解装饰者设计模式就可以一眼看出来真正执行Cursor接口相关方法的是传给CursorWrapperInner构造器中的 qCursor对象。
CursorWrapperInner的父类CursorWrapper也印证了这一点:
1 | ... |
跟上一小节 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 | //Java 方法 |
- mReadOnly为true时则不允许写入。mReadOnly在调用create()方法构造CursorWindow时会被赋值为false;在通过createFromParcel反序列化回CursorWindow时会被赋值为true。
- 空间不足时则不允许写入,返回false。
- 缓存区不是直接保存数值,而是保存一种数据结构FieldSlot。FieldSlot的数据结构如下:
1 | struct FieldSlot { |
以上可得出结论:CursorWindow拥有一块共享的内存区域,只要能正确调整指向不同内存区域的位置指针,就能够拿到对应的Cursor值。平时所理解的行、列索引,其实代表着指向着特定内存区域的指
自定义Cursor
对于Cursor的使用,按照使用区域的不同可以分成 生成进程内使用 和 跨进程使用。在生成Cursor进程内使用Cursor的场景很少,同时Android SDK本身也提供了很多API为这个场景提供支持(如上面提到的MatrixCursor、MergeCursor)。
因此这里我们只聚焦在Cursor最主要使用的场景 —— 跨进程使用。
虽然根据获取的方式不同,跨进程使用还可以分为:
- 通过ContentProvider获取
- 通过自定义Binder IPC接口获取
但是由于Cursor IPC传递的本质还是传递CursorWindow,从数据流动的角度来说都是一样的。这里我想通过第一种方式就足以证明如何自定义一个在IPC过程中传送的Cursor。
通过ContentProvider获取
通过ContentProvider获取Cursor流程一般都是这样的:通过ContentResolver查询到ContentProvider Binder IPC的接口,然后获取query() 调用返回的Cursor。第一小节的 IPC获取Cursor原理 亦是基于这一套流程进行分析。
1.实现一个继承自AbstractWindowedCursor的实现类
1 | class MyWindowedCursor(window: CursorWindow) : AbstractWindowedCursor() { |
2.实现ContentProvider,并将填充好内容的CursorWindow装进Cursor中并返回
1 | override fun query(uri: Uri, projection: Array<String>?, selection: String?, |
ContentProvider的定义:
1 | <provider |
至此,就因为完成了自定义Cursor最基本的实现。只要Client调用到该ContentProvider的query()接口,就能够拿到往自定义Cursor类MyWindowedCursor中填充的CursorWindow,并将其包装在CursorWrapperInner中。
3.测试
在APP中运行下面的代码测试一下是否能正常拿到自定义Cursor类型中的内容:
1 | val uri: Uri = Uri.parse("content://com.wusp.cursor/test") |
测试结果:
1 | D/Test: Activity cursor count: 2, 2 |