
1. 为什么我总在 ContentProvider 里用 MatrixCursor 造数据做 Android 开发到一定年头你会发现一个很尴尬的场景UI 层用的是SimpleCursorAdapter、CursorLoader或者RecyclerView.Adapter配合 Cursor 的封装整套数据流都建立在Cursor接口之上。但数据来源偏偏不是 SQLite——可能是网络返回的 JSON、本地缓存的 Map、甚至是一段写死的配置。这时候你只有两条路要么把数据塞进一个临时数据库表要么用MatrixCursor在内存里伪造一个 Cursor。MatrixCursor是android.database包下的一个轻量实现它继承自AbstractCursor把行数据存在一个Object[]数组里。你可以把它理解成用二维数组模拟出来的表构造时声明列名之后每调一次addRow就相当于插入一条记录。它不需要 SQLite、不需要文件 IO、不需要 ContentResolver纯内存操作构造完就能直接丢给 Adapter 或通过 ContentProvider 返回给调用方。它适合谁三类人最该掌握一是写 ContentProvider 需要返回聚合数据的同学比如把多个子查询结果拼成一张虚拟表二是做单元测试时想快速造 Cursor 数据的同学不用起 Robolectric 的数据库三是做跨进程数据传递、需要把内存对象序列化成 Cursor 再走 Binder 的同学。我试过在搜索建议、设置项列表、以及跨进程配置下发这几个场景里用它代码量比建临时表少一大截。这篇会从最基础的构建讲起一路走到 ContentProvider 跨进程返回再补上 adb 验证命令和几个真实踩过的报错。调试期如果涉及接口调用我会用 TaoToken 统一管理 Key 和 API 通道避免在多个工具间来回切配置。2. MatrixCursor 与普通 Cursor 的差异及适用边界2.1 结构差异内存数组 vs 数据库游标普通Cursor比如SQLiteCursor背后是一个查询引擎数据留在数据库文件里游标只持有当前行的位置和窗口。你调moveToNext时底层可能触发页读取。而MatrixCursor在构造那一刻就把所有数据放在Object[] rows里moveToPosition只是改一个mPos整数没有任何 IO。这个差异带来两个直接后果。第一MatrixCursor的getCount()是 O(1)因为它就是rowArrays.size()而数据库游标可能要等查询完成。第二MatrixCursor不支持requery、不支持getExtras里的复杂 Bundle也不支持setNotificationUri去监听数据变化——它就是个静态快照。2.2 能力边界哪些方法能用哪些会抛异常MatrixCursor实现了AbstractCursor的全部抽象方法但有些方法语义上是空实现。比如close()只是把mClosed置 truedeactivate()什么都不做。真正需要注意的是getBlob、getString这些取值方法它们依赖你addRow时传入的对象类型。如果你声明列是INTEGER却塞了个StringgetInt会走Cursor的类型转换逻辑可能返回 0 而不是报错这种静默失败最难查。适用边界我总结成一句话数据量小几百到几千行、结构固定、只读、生命周期短。超过这个范围比如上万行或者需要频繁更新老老实实建表。MatrixCursor每次addRow都会扩容数组数据量大时内存和 GC 压力都不小。2.3 跨进程传递时的序列化行为这是很多人忽略的点。当MatrixCursor通过ContentProvider返回时如果调用方在另一个进程Binder 会调用CursorToBulkCursorAdaptor把 Cursor 序列化成BulkCursorDescriptor。MatrixCursor支持这个过程因为它实现了CrossProcessCursor接口AbstractCursor默认实现。但要注意序列化的是数据快照不是引用。调用方拿到的是反序列化后的新 Cursor你在 Provider 进程里再改原对象对方看不到。另外MatrixCursor的getType在跨进程时依赖getColumnType而MatrixCursor对未显式设置类型的列返回FIELD_TYPE_NULL。如果你在 Adapter 里依赖类型判断最好在构造时用MatrixCursor(String[] columnNames, int initialCapacity)之外再通过setType显式声明——不过MatrixCursor没有公开的setType所以实际做法是保证addRow传入的对象类型一致。3. 可复制的 MatrixCursor 构建与 ContentProvider 返回配置3.1 基础构建三步造出一个 Cursor先看最小可用版本。假设我们要模拟一张商品表列是_id、name、pricepublic class ProductRepository { private static final String[] COLUMNS {_id, name, price}; public static MatrixCursor buildProducts() { MatrixCursor cursor new MatrixCursor(COLUMNS, 4); cursor.addRow(new Object[]{1, zhangsan, 39}); cursor.addRow(new Object[]{2, lisi, 40}); cursor.addRow(new Object[]{3, wangwu, 41}); cursor.addRow(new Object[]{4, zhaoliu, 42}); return cursor; } }addRow接收Object[]数组长度必须和列数一致否则抛IllegalArgumentException。如果你更喜欢链式写法用RowBuilderMatrixCursor cursor new MatrixCursor(COLUMNS, 4); MatrixCursor.RowBuilder builder cursor.newRow(); builder.add(1).add(zhangsan).add(39); builder cursor.newRow(); builder.add(2).add(lisi).add(40);RowBuilder.add返回this所以可以连续调用。但注意add的次数不能超过列数超了会抛CursorIndexOutOfBoundsException: No more columns left这个报错后面会专门讲。3.2 ContentProvider 返回 MatrixCursor 的完整写法在 Provider 的query方法里我们通常从数据库查但也可以直接返回内存 Cursorpublic class ProductProvider extends ContentProvider { public static final String AUTHORITY com.example.product.provider; public static final Uri CONTENT_URI Uri.parse(content:// AUTHORITY /products); Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { MatrixCursor cursor new MatrixCursor( new String[]{_id, name, price}, 4); cursor.addRow(new Object[]{1, zhangsan, 39}); cursor.addRow(new Object[]{2, lisi, 40}); cursor.addRow(new Object[]{3, wangwu, 41}); cursor.addRow(new Object[]{4, zhaoliu, 42}); // 关键设置通知 URI否则调用方无法监听变化 cursor.setNotificationUri(getContext().getContentResolver(), uri); return cursor; } // onCreate / insert / update / delete 略 }setNotificationUri对MatrixCursor来说只是记录一个 URI不会真的触发通知但调用方如果调了registerContentObserver不会崩。这一步在跨进程场景下建议保留保持接口一致性。3.3 调试期用 TaoToken 统一 Key 与 API 通道写 Provider 时经常要联调后端接口把网络数据转成 Cursor。如果每个模块各自配 Key很容易出现这个模块能跑、那个模块 401的情况。我的做法是用 TaoToken 统一管理调试期的 Key 和 API 通道Base URL 固定为https://taotoken.net/api模型 ID 按需选。如果你用 Claude Code 或 Cline 这类工具做辅助开发配置可以写成这样以 Claude Code 的 settings 为例{ env: { ANTHROPIC_BASE_URL: https://taotoken.net/api, ANTHROPIC_API_KEY: sk-你的TaoTokenKey, ANTHROPIC_MODEL: claude-sonnet-4-20250514 } }三件套就是 Base URL、Key、Model ID缺一不可。Key 在控制台的 API Keys 页面生成模型 ID 在模型对话页能看到当前可用的列表。这样配置后调试期所有接口调用走同一个通道排查问题时只需要看一个日志出口。4. 验证请求与成功结果adb 命令端到端读取4.1 用 content query 直接读 ProviderProvider 写好后不用写 Activity 就能验证。先安装 App然后adb shell content query --uri content://com.example.product.provider/products成功时输出类似Row: 0 _id1, namezhangsan, price39 Row: 1 _id2, namelisi, price40 Row: 2 _id3, namewangwu, price41 Row: 3 _id4, namezhaoliu, price42如果 Provider 没在 Manifest 里注册或者 authority 写错会报Unknown URI或Failed to find provider info。这时候先检查provider标签的android:authorities是否和代码里的AUTHORITY一致。4.2 带 projection 和 selection 的验证content query支持--projection和--whereadb shell content query \ --uri content://com.example.product.provider/products \ --projection _id:name \ --where price40注意MatrixCursor本身不解析selection和selectionArgs这些参数在query方法里是给你自己用的。如果你直接返回全量数据--where不会生效。要支持筛选得在query里手动过滤if (selection ! null selection.contains(price)) { // 手动解析并过滤或者用 MatrixCursor 遍历后重建 }这也是MatrixCursor的一个边界它不提供查询能力筛选逻辑得自己写。4.3 跨进程读取的验证如果调用方在另一个 App用ContentResolver.query拿到的 Cursor 已经是反序列化后的。验证时可以打印cursor.getCount()和cursor.getColumnNames()Cursor cursor getContentResolver().query( ProductProvider.CONTENT_URI, null, null, null, null); if (cursor ! null) { Log.d(MatrixCursorTest, count cursor.getCount()); Log.d(MatrixCursorTest, columns Arrays.toString(cursor.getColumnNames())); while (cursor.moveToNext()) { Log.d(MatrixCursorTest, cursor.getInt(0) / cursor.getString(1)); } cursor.close(); }成功时 Logcat 会输出count4和列名数组。如果count0先确认 Provider 进程是否启动、query是否真的被调用加日志。5. 本篇常见报错排查从 CursorIndexOutOfBoundsException 到 4015.1 CursorIndexOutOfBoundsException: No more columns left这是MatrixCursor最经典的报错堆栈指向MatrixCursor$RowBuilder.add。原因就一个add的次数超过了构造时声明的列数。比如列是 3 个你add了 4 次MatrixCursor cursor new MatrixCursor(new String[]{_id, name, price}); MatrixCursor.RowBuilder builder cursor.newRow(); builder.add(1).add(zhangsan).add(39).add(999); // 第4次 add 抛异常排查方法数一下add链的长度和COLUMNS.length对比。如果列是动态的用循环for (int i 0; i columns.length; i) builder.add(values[i]);更安全。5.2 IllegalArgumentException: columnNames cannot be null构造MatrixCursor时传了 null 列名数组。检查COLUMNS是否在某个分支里被重新赋值成 null。另外列名数组里不能有重复项否则getColumnIndex返回第一个匹配容易取错列。5.3 local proxy failed / 401 与接口调试调试期如果 Provider 里调了后端接口可能遇到401 Unauthorized或local proxy failed。401 通常是 Key 没配或过期去 TaoToken 控制台重新生成 API Key确认ANTHROPIC_API_KEY或对应字段填的是新 Key。local proxy failed一般是 Base URL 写错检查是不是漏了/api或者多了斜杠。正确写法是https://taotoken.net/api不要加 UTM 参数到 API 地址上。5.4 reading choices 报错与 OAuth 问题如果你用 Codex 或类似工具reading choices报错通常出现在响应格式不符合预期时。检查ANTHROPIC_MODEL是否填了模型对话页里列出的有效 ID。OAuth 相关报错则多见于 Claude Code 首次登录如果已经用 API Key 模式就不需要走 OAuth把ANTHROPIC_API_KEY配好即可。CC Switch 这类工具切换配置时记得三件套一起换只换 Key 不换 Base URL 会导致请求打到旧通道。5.5 跨进程时 CursorWindow 相关警告如果数据量较大Logcat 可能出现CursorWindow分配失败的警告。MatrixCursor跨进程时会走BulkCursor数据超过 1MB 左右可能触发TransactionTooLargeException。解决办法是分页返回或者改用ParcelFileDescriptor传文件。几百行的小数据不会有这个问题。6. 把 MatrixCursor 用顺手的几个实用技巧第一列名用常量数组别在多个地方硬编码字符串。getColumnIndex拼错列名会返回 -1然后getString(-1)抛CursorIndexOutOfBoundsException这个报错和前面的No more columns left长得像但原因完全不同。第二addRow传的对象类型要和取值方法匹配。声明price是整数就传int别传String再指望getInt帮你转。MatrixCursor的类型转换是宽松的静默返回 0 比抛异常更难查。第三跨进程返回前调setNotificationUri即使MatrixCursor不真正发通知也能让调用方的registerContentObserver不报错。第四调试期用adb shell content query验证比写测试 Activity 快得多。配合 TaoToken 的统一 Key 管理接口调用和 Provider 验证可以在同一条命令链里完成。需要生成新 Key 时去 API Keys 页面模型 ID 在模型对话页确认接入细节看接入文档。长期做编码和 Agent 任务的话Coding Plan 能把额度集中管理省得每个工具单独配。最后MatrixCursor不是银弹。数据要更新、要查询、要持久化就老老实实用 SQLite。它的价值在于快速把内存数据伪装成 Cursor把这个定位守住代码会干净很多。