☰
Android Cursor源码笔记(2):从AbstractCursor到CrossProcessCursor的跨进程实现
2026/10/2 16:58:04 网站建设 项目流程

1. 从一次跨进程查询卡顿说起:AbstractCursor 与 CrossProcessCursor 到底在做什么

如果你写过 ContentProvider,大概率遇到过这样的场景:主进程里query()返回的 Cursor 用得好好的,一旦把数据通过 Binder 传给另一个进程,要么直接抛android.database.CursorWindowAllocationException,要么读出来的字段全是 null。这不是你 SQL 写错了,而是 Cursor 的跨进程机制在起作用。

Android 的 Cursor 体系里,AbstractCursor是所有具体游标类的基类,它把位置管理、观察者通知、列索引查找这些通用逻辑都封装好了;而CrossProcessCursor则是一个接口,专门描述"这个游标可以被远端进程使用"。两者之间的关系是:AbstractCursor implements CrossProcessCursor,也就是说每一个继承 AbstractCursor 的游标,天生就具备跨进程的潜力,但真正能不能跨进程,取决于它有没有正确实现fillWindow()和getWindow()。

理解这条链路的意义在于:当你在做插件化、多进程 Service、或者 ContentProvider 跨进程查询时,数据并不是把整个 Cursor 对象序列化过去,而是通过CursorWindow这块共享内存来传递。CursorWindow本质上是一块可以被多个进程映射的缓冲区,里面按行存储了查询结果。远端进程拿到的是一个"壳"Cursor,真正的数据在 Window 里,通过 Binder 传递的是这块 Window 的句柄。

这篇笔记聚焦的就是从AbstractCursor到CrossProcessCursor的完整实现链路,结合CursorWindow的数据共享原理,给出可复制的源码阅读路径和基于 Android Studio 的断点验证步骤。适合已经写过 ContentProvider、想搞清楚跨进程游标底层怎么跑的中高级开发者。下面我会按"问题场景 → 前置准备 → 可复制配置 → 验证请求 → 常见报错 → 后续路径"的顺序展开,每一步都尽量给到能直接跑的代码和断点位置。

2. 读源码前的前置准备:Android Studio 断点环境与 CursorWindow 依赖梳理

在正式跟代码之前,先把阅读环境搭好,否则你会在frameworks/base/core/java/android/database/这一堆文件里迷路。我建议用 Android Studio 打开 AOSP 的frameworks/base模块,或者至少把android.jar的 sources 挂上。如果你只是做应用层开发,直接下载对应 API 级别的 sources 包即可,路径通常在$ANDROID_SDK/sources/android-34/android/database/。

需要重点关注的类有这几个:AbstractCursor.java、CrossProcessCursor.java、CursorWindow.java、AbstractWindowedCursor.java、SQLiteCursor.java、ContentProvider.java里的transport()方法,以及CursorToBulkCursorAdaptor.java。这几个类构成了从本地游标到跨进程游标的完整链路。

关于工具链,如果你在阅读过程中需要对照一些模型辅助理解源码逻辑,可以用 TaoToken 的模型对话能力来快速解释某段代码的意图。它的入口在 模型对话,适合在读到SelfContentObserver这种内部类时快速问一句"这个 weakRef 是干嘛的"。不过源码阅读的核心还是自己跟断点,工具只是辅助。

环境准备好之后,先建立一张类关系图在脑子里:

类/接口角色关键方法
Cursor基础接口moveToPosition、getString
CrossProcessCursor跨进程扩展接口fillWindow、getWindow、onMove
AbstractCursor通用基类moveToPosition(final)、onMove、fillWindow
AbstractWindowedCursor带 Window 的基类getWindow、setWindow
SQLiteCursor具体实现重写fillWindow走 native
CursorWindow共享内存缓冲区putString、getString、numRows

这张表建议你抄到笔记里,后面跟断点时随时对照。AbstractCursor里最值得注意的是moveToPosition是final的,它内部调用onMove,而onMove返回 false 会导致位置被重置为 -1。这个设计决定了子类只能通过onMove来干预移动行为,不能直接改moveToPosition。

另外AbstractCursor内部维护了mSelfObserver,它是一个SelfContentObserver,持有当前 Cursor 的弱引用。当setNotificationUri被调用时,会注册这个 observer 到ContentResolver。这里有个细节:setNotificationUri会被多线程调用,所以内部加了锁,而且每次只能注册一个,旧的会被刷掉。这个机制在跨进程场景下尤其重要,因为远端进程的变更通知要靠它来传递。

CursorWindow这边,你要理解它的核心是"一块可以被多个进程映射的内存"。它内部有一个mWindowPtr指向 native 层的结构,Java 层通过 JNI 调用操作这块内存。fillWindow(position, window)的作用就是从position开始,把 row 数据填进 Window,直到填满或者数据耗尽。getWindow()则返回一个已经预填充的 Window,避免一次数据拷贝——这是CrossProcessCursor注释里提到的优化点。

3. 可复制的源码阅读配置:从 AbstractCursor 到 CrossProcessCursor 的断点路径

这一节给你一套可以直接照做的断点配置。我假设你用的是 Android Studio,调试的是一个包含 ContentProvider 的 demo 应用,并且已经能在query()里拿到 Cursor。

首先,在你的ContentProvider子类里重写query(),确保返回的是SQLiteCursor或者MatrixCursor。然后在query()返回处打一个断点,观察返回的 Cursor 实际类型。接着在AbstractCursor.moveToPosition(int)里打条件断点,条件写position == 0,这样第一次移动就会停下来。

关键断点位置如下:

// AbstractCursor.java public final boolean moveToPosition(int position) { // 断点1:观察 mPos 初始值 -1 final int count = getCount(); if (position >= count) { mPos = count; return false; } if (position < 0) { mPos = -1; return false; } if (position == mPos) { return true; } // 断点2:观察 onMove 回调 boolean result = onMove(mPos, position); if (!result) { mPos = -1; } else { mPos = position; } return result; }

在onMove里再打一个断点,你会看到AbstractCursor的默认实现是return true,而SQLiteCursor会重写它,在里面调用fillWindow。这就是本地游标填充 Window 的入口。

接下来配置跨进程场景。你需要一个ContentProvider运行在独立进程,在AndroidManifest.xml里给 provider 加android:process=":remote"。然后在客户端进程调用getContentResolver().query(),此时返回的 Cursor 实际类型是CursorToBulkCursorAdaptor包装后的BulkCursorToCursorAdaptor。

在CursorToBulkCursorAdaptor的getWindow()和fillWindow()里打断点,你会看到 Binder 调用IContentProvider.query()后,服务端返回的是BulkCursorDescriptor,里面包含CursorWindow的句柄。客户端通过BulkCursorToCursorAdaptor拿到这个 Window,后续的moveToPosition会触发fillWindow从远端拉数据。

这里给出一份settings.gradle的配置片段,确保你的 demo 模块能同时编译 provider 和 client:

// settings.gradle include ':app' include ':provider' project(':provider').projectDir = new File(rootDir, 'provider')
// provider/build.gradle android { namespace 'com.example.provider' compileSdk 34 defaultConfig { minSdk 24 targetSdk 34 } }

如果你在阅读过程中需要快速验证某个 API 的行为,可以用 TaoToken 的 API Keys 配合 接入文档 写个小脚本跑一下,比反复改 demo 快。但源码断点还是得在 Android Studio 里做,工具替代不了。

关于CursorWindow的配置,有一个容易忽略的点:CursorWindow有大小限制,默认是 2MB(不同版本可能不同)。当查询结果超过这个大小,fillWindow会填到一半就停,剩下的数据在后续moveToPosition时再按需填充。这就是为什么跨进程 Cursor 不能一次性把所有数据都拿过来,而是"懒加载"式的。你在断点里会看到fillWindow被多次调用,每次填一部分。

AbstractWindowedCursor是连接AbstractCursor和CursorWindow的桥梁,它内部持有mWindow,并实现了getWindow()和setWindow()。SQLiteCursor继承自它,重写了fillWindow走 native 的nativeFillWindow。而MatrixCursor虽然也继承AbstractCursor,但它没有 Window,所以它的getWindow()返回 null,跨进程时会走CursorToBulkCursorAdaptor的降级路径,把数据逐行拷贝到新建的 Window 里。

4. 验证请求与成功结果:用 Binder 调用观察 CursorWindow 数据传递

配置好之后,跑一次跨进程查询,观察日志和断点命中顺序。我在 demo 里用的是ContentResolver.query(),服务端 provider 返回一个包含 1000 行数据的SQLiteCursor,客户端在:main进程,provider 在:remote进程。

第一次断点命中在服务端的SQLiteCursor.fillWindow(),此时position=0,Window 被填充了前 N 行(N 取决于每行大小和 Window 容量)。然后 Binder 把BulkCursorDescriptor返回给客户端,客户端BulkCursorToCursorAdaptor拿到 Window 句柄。

客户端第一次调用cursor.moveToPosition(0),断点命中BulkCursorToCursorAdaptor.onMove(),它内部会检查 Window 是否包含 position 0 的数据,如果没有就触发fillWindow通过 Binder 再拉一次。成功的情况下,getString()能正确读出字段值。

验证成功的标志有三个:一是cursor.getCount()返回正确的行数;二是cursor.moveToPosition(999)后getString(0)能读出最后一行的数据;三是cursor.getWindow()返回的CursorWindow的numRows()大于 0。

这里给出一段可复制的验证代码:

// ClientActivity.java Uri uri = Uri.parse("content://com.example.provider/items"); Cursor cursor = getContentResolver().query(uri, null, null, null, null); if (cursor != null) { Log.d("CursorTest", "count=" + cursor.getCount()); if (cursor.moveToPosition(999)) { String name = cursor.getString(cursor.getColumnIndex("name")); Log.d("CursorTest", "row999 name=" + name); } if (cursor instanceof CrossProcessCursor) { CursorWindow window = ((CrossProcessCursor) cursor).getWindow(); Log.d("CursorTest", "window rows=" + (window != null ? window.getNumRows() : -1)); } cursor.close(); }

跑通之后你会看到日志里count=1000,row999 name=xxx,window rows=xxx。如果window rows是 0 或者 -1,说明 Window 没有正确传递,需要检查 provider 是否在独立进程、Binder 调用是否成功。

关于onMove的返回值,这里有个坑:如果你自定义的 Cursor 重写了onMove并且返回了 false,那么moveToPosition会把mPos置为 -1,后续所有读取都会失败。AbstractCursor的注释明确说了,onMove应该只在 Cursor 自己的 Move 类函数中调用,不能在外部调用。我在调试时曾经在onMove里加了一行日志,结果因为日志里调用了getPosition()导致递归,最后栈溢出。这个坑你要避开。

SelfContentObserver的验证也值得做一次。在服务端 provider 的update()里调用getContext().getContentResolver().notifyChange(uri, null),客户端注册ContentObserver,你会看到onChange被触发。这条链路走的是AbstractCursor.onChange→mContentObservable.dispatchChange→ 你注册的 observer。注意selfChange参数在 SelfContentObserver 触发时是 false,这是区分"自己改的"和"别人改的"的关键。

5. 本篇常见报错排查:401、local proxy failed、reading choices 与 OAuth 对照

这一节列出你在跟源码和调试时最可能撞上的几个报错,以及对应的排查方向。注意这些报错有的是 Android 框架层的,有的是你在用辅助工具时遇到的,分开看。

报错一:android.database.CursorWindowAllocationException: Cursor window allocation of 2048 kb failed

这是 Window 分配失败,通常是因为查询结果太大,或者同时打开了太多 Cursor 没关闭。排查方向:检查cursor.close()是否在每个分支都调用了;检查CursorWindow的容量设置;如果是跨进程场景,确认服务端没有把整个大结果集一次性塞进 Window。解决方式是分页查询,或者用setWindow手动控制 Window 大小。

报错二:java.lang.IllegalStateException: attempt to re-open an already-closed object

Cursor 已经 close 了还在读。跨进程场景下,客户端 Cursor 关闭后,服务端的 Window 可能还被引用。排查方向:确认BulkCursorToCursorAdaptor的close()是否被正确调用;检查是否有多个线程共享同一个 Cursor。这个报错在AbstractCursor.checkPosition()里会抛,因为mClosed标志被置位了。

报错三:local proxy failed或reading choices相关错误

这类报错通常出现在你用辅助工具做模型调用时,网络层或代理配置有问题。排查方向:确认 Base URL 配置正确,Key 没有过期。如果你在用 TaoToken 做源码辅助阅读,Base URL 应该是https://taotoken.net/api,Key 从 API Keys 获取。reading choices一般是响应体解析失败,检查返回的 JSON 结构是否符合预期。

报错四:401 Unauthorized

Key 无效或没带上。排查方向:检查请求头里的Authorization: Bearer <key>是否正确;确认 Key 没有多余空格;如果是 OAuth 流程,检查 token 是否过期。在 Android 源码调试里,这个报错一般不会出现,但如果你用脚本调 API 辅助阅读,就会遇到。

报错五:OAuth token expired

如果你用的是需要 OAuth 的模型服务,token 过期后会报这个。解决方式是刷新 token,或者改用 API Key 方式。TaoToken 的 接入文档 里有详细的鉴权说明,建议对照检查。

报错六:CursorWindow: Window is full

这个不是异常,是日志警告。说明 Window 填满了,后续数据会在moveToPosition时按需加载。如果你看到这个日志但数据读取正常,不用管它。如果读取失败,检查fillWindow的 position 参数是否正确。

排查这些报错时,建议先在AbstractCursor的checkPosition()里打断点,它是所有位置校验的入口。checkPosition()在位置无效(哨兵位 -1 或 count)时会抛CursorIndexOutOfBoundsException,你能从这里快速定位是哪个调用传了非法位置。

6. 继续深入:从 CrossProcessCursor 到 BulkCursor 的下一步阅读路径

跟完这一轮断点,你应该已经清楚AbstractCursor如何通过onMove控制位置、CrossProcessCursor如何通过fillWindow和getWindow支持跨进程、CursorWindow如何作为共享内存载体在 Binder 场景下传递数据。

下一步建议你读CursorToBulkCursorAdaptor和BulkCursorToCursorAdaptor这两个类,它们是 Binder 两端的适配器,负责把CrossProcessCursor转换成BulkCursorDescriptor再转换回来。重点看BulkCursorDescriptor里window字段的序列化方式,以及CursorWindow.writeToParcel()和readFromParcel()的实现。

如果你在做长期的多进程架构开发,可以考虑用 Coding Plan 来辅助管理代码阅读笔记和断点记录,它适合这种需要持续跟进的源码分析场景。但核心还是你自己在 Android Studio 里一步步跟,工具只是帮你省去重复查文档的时间。

最后留一个实操建议:把AbstractCursor的moveToPosition、onMove、fillWindow三个方法的调用栈打印出来,用Log.d加Thread.currentThread().getStackTrace(),你会直观看到跨进程时调用栈里多了 Binder 的帧。这个观察比任何文档都来得直接。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询