ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Android Cursor源码笔记(2):从AbstractCursor到CrossProcessCursor的跨进程实现

2026/10/2 16:58:10 拓冰建站 浏览量
Android Cursor源码笔记(2):从AbstractCursor到CrossProcessCursor的跨进程实现 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、getStringCrossProcessCursor跨进程扩展接口fillWindow、getWindow、onMoveAbstractCursor通用基类moveToPosition(final)、onMove、fillWindowAbstractWindowedCursor带 Window 的基类getWindow、setWindowSQLiteCursor具体实现重写fillWindow走 nativeCursorWindow共享内存缓冲区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()此时position0Window 被填充了前 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(); }跑通之后你会看到日志里count1000row999 namexxxwindow rowsxxx。如果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 objectCursor 已经 close 了还在读。跨进程场景下客户端 Cursor 关闭后服务端的 Window 可能还被引用。排查方向确认BulkCursorToCursorAdaptor的close()是否被正确调用检查是否有多个线程共享同一个 Cursor。这个报错在AbstractCursor.checkPosition()里会抛因为mClosed标志被置位了。报错三local proxy failed或reading choices相关错误这类报错通常出现在你用辅助工具做模型调用时网络层或代理配置有问题。排查方向确认 Base URL 配置正确Key 没有过期。如果你在用 TaoToken 做源码辅助阅读Base URL 应该是https://taotoken.net/apiKey 从 API Keys 获取。reading choices一般是响应体解析失败检查返回的 JSON 结构是否符合预期。报错四401 UnauthorizedKey 无效或没带上。排查方向检查请求头里的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 的帧。这个观察比任何文档都来得直接。