1. 从一次跨进程查询失败说起SQLiteOpenHelper 与 ContentProvider 到底怎么配合Android 本地数据层最常见的组合就是 SQLiteOpenHelper 管建库建表、ContentProvider 管对外暴露。听起来简单但真正跑起来很多人会卡在同一个地方自己 App 里getContentResolver().query()能查到数据换个进程或者换个应用就报Unknown URL或者Failed to find provider info。这不是数据库的问题而是 ContentProvider 的注册、authorities、UriMatcher 三件事没对齐。SQLiteOpenHelper 是什么它是 Android 提供的数据库版本管理抽象类负责在第一次访问时创建数据库和表并在版本号变化时触发升级逻辑。ContentProvider 是什么它是 Android 四大组件之一用来把数据以 URI 的形式暴露给其他应用或进程访问底层可以接 SQLite、文件、网络甚至内存。两者结合就是「本地数据库 跨应用数据访问」的标准做法。适合谁看如果你正在写一个需要被其他 App 读取数据的模块比如设置类应用、设备信息共享、轻量级数据同步或者你只是想把数据库操作从 Activity 里抽出来这套组合都值得掌握。我试过在同一个工程里先用 SQLiteOpenHelper 建表再用 ContentProvider 包一层结果发现真正花时间的不是写 CRUD而是把 authorities、UriMatcher、MIME 类型和权限声明对齐。这篇会按「先建库、再暴露、后验证」的顺序走一遍同时把 TaoToken 的统一 Key 和 Base URL 配置嵌进 Android 工程里让鉴权信息和数据层配置放在同一个可复制的结构里。目标很明确一次跑通数据读写和跨进程查询并且知道每个报错对应哪一行配置。2. TaoToken 前置统一 Key 与 Base URL 在 Android 工程中的定位在讲 Provider 之前先把 TaoToken 的接入位置说清楚。TaoToken 提供的是统一的 API 通道官网入口是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 地址是 https://taotoken.net/api 。在 Android 工程里它通常出现在两个地方一是网络层请求大模型接口时的 Base URL二是本地配置文件中保存的鉴权信息。为什么数据层文章要提这个因为很多 Android 项目的数据层不只是本地 SQLite还会把本地数据同步到远端或者用大模型做本地数据的语义处理。这时候统一 Key 的价值就出来了你不需要在每个模块里散落不同的鉴权配置而是把 Base URL 和 Key 集中管理。TaoToken 的 API Key 可以在控制台创建地址是 https://taotoken.net/console 创建后配合 https://taotoken.net/api-keys 管理。具体到 Android 工程我建议把 Base URL 和 Key 放在local.properties或者BuildConfig里不要硬编码在 Java/Kotlin 源码中。原因很简单源码会进版本库Key 一旦提交就很难彻底清除。你可以这样组织# local.properties taotoken.base.urlhttps://taotoken.net/api taotoken.api.keysk-your-key-here然后在build.gradle里读取并注入BuildConfigandroid { defaultConfig { buildConfigField String, TAOTOKEN_BASE_URL, \${taotokenBaseUrl}\ buildConfigField String, TAOTOKEN_API_KEY, \${taotokenApiKey}\ } }这样数据层在需要调用远端接口时直接引用BuildConfig.TAOTOKEN_BASE_URL和BuildConfig.TAOTOKEN_API_KEY既统一又不会泄露。如果你用的是 Claude Code 或者 Coding Plan 做长期编码可以把这套配置写进项目的settings或auth.json里让工具链也走同一个通道。模型对话入口在 https://taotoken.net/models 接入文档在 https://taotoken.net/doc 需要对照参数时可以直接查。这里要强调一点TaoToken 是 API 通道不是数据库替代品。SQLiteOpenHelper 和 ContentProvider 负责本地数据TaoToken 负责远端鉴权和模型调用两者是配合关系不是替代关系。把 Base URL 和 Key 配好之后本地数据层该怎么写还是怎么写只是多了一个统一的出口。3. 可复制配置Provider 声明、UriMatcher 与 settings 片段这一节是整篇的核心所有代码都可以直接复制到工程里改包名使用。先看数据库部分。SQLiteOpenHelper 的子类负责建表注意原 excerpt 里的建表语句有个常见笔误interger应该是integerrowid和_id的关系也要理清。下面是我整理后的版本public class WifiSsidDatabaseHelper extends SQLiteOpenHelper { private static final String DATABASE_NAME settings.db; private static final int DATABASE_VERSION 1; public static final String TABLE_SSID table_ssid; public static final String TABLE_MAC table_mac; private static final String CREATE_TABLE_SSID_SQL create table TABLE_SSID (_id integer primary key autoincrement, name text, status integer); private static final String CREATE_TABLE_MAC_SQL create table TABLE_MAC (_id integer primary key autoincrement, name text, status integer); public WifiSsidDatabaseHelper(Context context) { super(context, DATABASE_NAME, null, DATABASE_VERSION); } Override public void onCreate(SQLiteDatabase db) { db.execSQL(CREATE_TABLE_SSID_SQL); db.execSQL(CREATE_TABLE_MAC_SQL); } Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { db.execSQL(drop table if exists TABLE_SSID); db.execSQL(drop table if exists TABLE_MAC); onCreate(db); } }注意onCreate只在数据库第一次创建时执行getReadableDatabase()或getWritableDatabase()被调用时才会触发。如果你发现表没建出来先确认有没有真正调用过这两个方法。接下来是 ContentProvider重点是 UriMatcher 和 authorities 对齐public class WifiSsidProvider extends ContentProvider { private static final String AUTHORITY com.android.setting.wifi.provider.wifissidprovider; public static final Uri SSID_CONTENT_URI Uri.parse(content:// AUTHORITY /ssid); public static final Uri MAC_CONTENT_URI Uri.parse(content:// AUTHORITY /mac); private static final int SSID_DIR 1; private static final int SSID_ITEM 2; private static final int MAC_DIR 3; private static final int MAC_ITEM 4; private static final UriMatcher URI_MATCHER new UriMatcher(UriMatcher.NO_MATCH); static { URI_MATCHER.addURI(AUTHORITY, ssid, SSID_DIR); URI_MATCHER.addURI(AUTHORITY, ssid/#, SSID_ITEM); URI_MATCHER.addURI(AUTHORITY, mac, MAC_DIR); URI_MATCHER.addURI(AUTHORITY, mac/#, MAC_ITEM); } private WifiSsidDatabaseHelper dbOpenHelper; Override public boolean onCreate() { dbOpenHelper new WifiSsidDatabaseHelper(getContext()); return true; } Override public Cursor query(Uri uri, String[] projection, String where, String[] selectionArgs, String sortOrder) { SQLiteDatabase db dbOpenHelper.getReadableDatabase(); String table; switch (URI_MATCHER.match(uri)) { case SSID_DIR: case SSID_ITEM: table WifiSsidDatabaseHelper.TABLE_SSID; break; case MAC_DIR: case MAC_ITEM: table WifiSsidDatabaseHelper.TABLE_MAC; break; default: throw new IllegalArgumentException(Unknown URI: uri); } return db.query(table, projection, where, selectionArgs, null, null, sortOrder); } Override public Uri insert(Uri uri, ContentValues values) { SQLiteDatabase db dbOpenHelper.getWritableDatabase(); String table (URI_MATCHER.match(uri) SSID_DIR) ? WifiSsidDatabaseHelper.TABLE_SSID : WifiSsidDatabaseHelper.TABLE_MAC; long id db.insert(table, null, values); getContext().getContentResolver().notifyChange(uri, null); return Uri.withAppendedPath(uri, String.valueOf(id)); } Override public int delete(Uri uri, String where, String[] selectionArgs) { SQLiteDatabase db dbOpenHelper.getWritableDatabase(); String table (URI_MATCHER.match(uri) SSID_DIR) ? WifiSsidDatabaseHelper.TABLE_SSID : WifiSsidDatabaseHelper.TABLE_MAC; int count db.delete(table, where, selectionArgs); getContext().getContentResolver().notifyChange(uri, null); return count; } Override public int update(Uri uri, ContentValues values, String where, String[] selectionArgs) { SQLiteDatabase db dbOpenHelper.getWritableDatabase(); String table (URI_MATCHER.match(uri) SSID_DIR) ? WifiSsidDatabaseHelper.TABLE_SSID : WifiSsidDatabaseHelper.TABLE_MAC; int count db.update(table, values, where, selectionArgs); getContext().getContentResolver().notifyChange(uri, null); return count; } Override public String getType(Uri uri) { switch (URI_MATCHER.match(uri)) { case SSID_DIR: return vnd.android.cursor.dir/vnd.wifissid.ssid; case SSID_ITEM: return vnd.android.cursor.item/vnd.wifissid.ssid; default: return null; } } }然后是 AndroidManifest.xml 里的 provider 声明这是跨进程访问的入口缺了它就会报Failed to find provider infoprovider android:name.wifi.WifiSsidProvider android:authoritiescom.android.setting.wifi.provider.wifissidprovider android:exportedtrue android:grantUriPermissionstrue /如果你需要限制其他应用访问可以把android:exported设为false只在本应用内使用。跨应用访问时exportedtrue是必须的同时要注意 Android 12 及以上对exported的显式声明要求。最后是 TaoToken 的配置片段放在local.properties或settings里{ taotoken: { baseUrl: https://taotoken.net/api, apiKey: sk-your-key-here, modelId: your-model-id } }这三件套——Base URL、Key、Model ID——在 Claude Code、Cline MCP、Codex 的auth.json里也是同样的结构。如果你用 Coding Plan 做长期编码建议把这段配置放在项目根目录的.taotoken/settings.json里工具链会自动读取。需要创建 Key 时去 https://taotoken.net/api-keys 需要看接入示例去 https://taotoken.net/doc 。4. 验证请求与成功结果CRUD 与跨进程查询一次跑通配置写完之后必须验证。验证分两步先在本应用内用 ContentResolver 跑一遍 CRUD再用另一个进程或另一个应用查询确认跨进程通路是通的。先看插入ContentValues values new ContentValues(); values.put(name, MySSID); values.put(status, 1); Uri resultUri getContentResolver().insert(WifiSsidProvider.SSID_CONTENT_URI, values); Log.i(TaoTokenDemo, insert result: resultUri);如果resultUri返回类似content://com.android.setting.wifi.provider.wifissidprovider/ssid/1说明插入成功。如果返回 null检查db.insert的返回值通常是表名或字段不匹配。查询Cursor cursor getContentResolver().query( WifiSsidProvider.SSID_CONTENT_URI, new String[]{_id, name, status}, null, null, null); if (cursor ! null) { while (cursor.moveToNext()) { int id cursor.getInt(0); String name cursor.getString(1); int status cursor.getInt(2); Log.i(TaoTokenDemo, row: id , name , status); } cursor.close(); }更新和删除类似注意 where 条件里用_id?而不是字符串拼接避免注入问题ContentValues updateValues new ContentValues(); updateValues.put(status, 0); int updated getContentResolver().update( WifiSsidProvider.SSID_CONTENT_URI, updateValues, _id?, new String[]{1}); Log.i(TaoTokenDemo, updated rows: updated); int deleted getContentResolver().delete( WifiSsidProvider.SSID_CONTENT_URI, _id?, new String[]{1}); Log.i(TaoTokenDemo, deleted rows: deleted);跨进程验证需要另一个应用或者另一个进程。最简单的方式是在同一个工程里开一个:remote进程的 Service在里面调用getContentResolver().query()。如果查询成功返回 Cursor说明 Provider 的exported和 authorities 配置正确。如果报Unknown URL检查 UriMatcher 里有没有注册对应的路径如果报Failed to find provider info检查 AndroidManifest 里的 authorities 是否和代码里的 AUTHORITY 完全一致包括大小写。成功的结果应该是插入返回带 id 的 Uri查询返回至少一行数据更新和删除返回影响行数大于 0跨进程查询能拿到同样的 Cursor。到这一步数据层就算跑通了。5. 本篇常见错排查401、local proxy failed、reading choices、OAuth这一节对照真实报错把数据层和 TaoToken 接入过程中最容易踩的坑列出来。第一个是401 Unauthorized。这个通常出现在调用 TaoToken API 时Key 无效或者没带上。检查BuildConfig.TAOTOKEN_API_KEY是否为空请求头里有没有Authorization: Bearer key。如果你用的是 Claude Code 或 Cline MCP检查auth.json里的apiKey字段是否和 https://taotoken.net/api-keys 里创建的一致。401 不会因为 Base URL 写错而出现Base URL 写错通常是 404 或连接超时。第二个是local proxy failed。这个报错说明请求没有直接到达 TaoToken而是被本地某个转发层拦截了。检查你的网络配置里有没有多余的代理设置Android 工程里检查OkHttpClient是否配置了proxy()。TaoToken 的 API 地址是 https://taotoken.net/api 直接请求即可不需要额外转发。第三个是reading choices相关报错。这个通常出现在解析模型返回的 JSON 时choices字段为空或者结构不对。检查请求体里的model字段是否和 Model ID 一致messages格式是否符合接口要求。如果你在数据层里把本地 SQLite 数据拼成 prompt注意 JSON 转义避免引号嵌套导致请求体格式错误。第四个是OAuth相关报错。如果你用 Claude Code 的 OAuth 流程接入检查回调地址和 token 是否过期。TaoToken 的 Coding Plan 入口在 https://taotoken.net/coding-plan 里面有针对长期编码场景的配置说明。OAuth 报错通常和 Key 无关而是授权流程没走完。还有一个数据层特有的坑IllegalArgumentException: Unknown URI。这个一定是 UriMatcher 没匹配上。检查addURI里的 authority 和路径是否和实际请求的 Uri 一致。比如你请求的是content://authority/ssid/1但只注册了ssid没有注册ssid/#就会走到 default 分支抛异常。最后一个坑是CursorWindowAllocationException。查询返回的数据量太大时会出现解决办法是在 query 里加 limit或者分页查询。ContentProvider 的 query 方法支持sortOrder参数可以写成_id limit 50来限制返回行数。6. 语义一致 CTA把数据层和鉴权层放在同一个工程结构里数据层跑通之后下一步通常是把本地数据和远端能力结合起来。比如用本地 SQLite 存设备信息用 TaoToken 的模型对话能力做语义分析或者用 Coding Plan 做长期编码时让工具链读取同一份配置。这时候统一 Key 和 Base URL 的价值就体现出来了你不需要在每个模块里重复配置鉴权信息而是把https://taotoken.net/api和对应的 Key 放在项目级配置里数据层、网络层、工具链共用。如果你在排障阶段建议先看接入文档 https://taotoken.net/doc 里面有针对不同场景的配置示例。如果你需要验证模型是否可用可以去模型对话页面 https://taotoken.net/models 直接测试。如果你打算长期做 Android 数据层和 Agent 结合的开发Coding Plan 的入口在 https://taotoken.net/coding-plan 适合把配置固化下来。回到工程本身我建议把 Provider 的 authorities、UriMatcher 的路径、TaoToken 的 Base URL 和 Key 都写进一个AppConfig类里避免散落在多个文件中。这样下次改配置时只需要改一处跨进程查询和远端调用都不会因为配置不一致而失败。数据层的稳定性往往就藏在这些对齐细节里。
阅读完成 · 觉得有帮助?