Appearance
ContentProvider:发布时机、查询链路与跨进程游标
本页导读:ContentProvider 是四大组件里最「隐形」的一个——它比你最早醒来,也在你最意想不到的线程上被调用。本页讲它为什么在
Application.onCreate之前就发布、一次query()从 Binder 到 CursorWindow 的完整链路、跨进程游标的共享内存真相,以及文件共享的 FileProvider/DocumentsProvider 规范。前置:Activity、架构篇 Binder 页。
一、发布时机:比你更早的组件
ContentProvider 的发布嵌在应用绑定的关键路径里,ActivityThread.handleBindApplication 的骨架(简化):
text
handleBindApplication
├ makeApplication() // 反射 new Application
├ application.attachBaseContext() // ← 基类挂钩点
├ installContentProviders() // ← 所有 provider:new + attachInfo + onCreate()
├ application.onCreate() // ← 业务初始化才开始
└ 开始处理 Activity/Service/Broadcast 消息结论与推论:
- provider 的
onCreate()先于Application.onCreate()——launcher 冷启动你的 provider 时,你的全局初始化还没跑,依赖注入框架(Hilt/手工单例)的 DAO 会直接 NPE。provider 内要做极简初始化或自建依赖图。 - 发布是「同步阻塞」在 bindApplication 里的:AMS 等所有 provider
publish回执后才认为进程 ready,慢 provider 直接拖慢整个进程启动(冷启动优化必查项)。 - 系统还会为 provider 单独拉进程:你没打开过某 App,但短信/联系人查询可能把它进程拽起来——这就是「没点过的 App 也在后台」的主要合法来源。
bash
adb shell dumpsys activity providers | head -30 # 看已注册 provider、authority 归属、进程
adb shell dumpsys package providers # authority 全局路由表(同 authority 抢占规则)二、URI 与路由:authority 是门牌号
一个 content URI 的三要素:content://<authority>/<path>#<fragment>。authority 全局唯一路由(同 authority 冲突时系统安装期拒绝/隐藏,以 AOSP PackageManager 行为为准);path 由 provider 自己解释,AOSP 惯用 UriMatcher 建表匹配:
常见系统 authority 顺手记几个(命名格式即最佳示范):
| authority | 提供方 | 用途 |
|---|---|---|
| contacts | AOSP 通讯录 | 联系人/电话档案 |
| media | 媒体库 | 照片/音视频元数据 |
| downloads | 下载管理 | 下载任务状态 |
| settings | 设置库 | Settings.System/Secure/Global |
java
// 经典模式(AOSP 通讯录/媒体等 provider 同款思路)
static final UriMatcher sMatcher = new UriMatcher()
.match("content://com.example.provider/notes", NOTES) // 集合
.match("content://com.example.provider/notes/#", NOTE_ID) // 单条
.match("content://com.example.provider/notes/*/comments", COMMENTS); // 子资源
// query() 第一行 match uri,落到哪个码走哪条分支;不认识的直接抛
// IllegalArgumentException——不要返回 null,调用方无从处理权限模型挂在 authority 或 path 粒度上:
android:permission/android:readPermission/android:writePermission:authority 级;pathPermissions(API 19+):pathPattern 级,例如/notes/*要求NOTE_OWNER权限;- 临时授权:
grantUriPermission/FLAG_GRANT_READ_URI_PERMISSION——跨进程给单个 URI 开临时门,intent 生命周期结束自动回收。 - exported=false 的 provider 只同包可用(跨进程拿不到),这是「隐形组件」也必须是显式安全决策的原因(target 31+ 无 filter 组件 exported 必须显式声明)。
三、一次 query 的完整链路
text
ContentResolver.query(uri, projection, ...)
├ getContentProvider(uri) ── Binder ──▶ AMS/ProviderHelper:authority 路由 + 权限/导出校验
│ └ 目标进程未起则先拉进程(等 publish)
├ IContentProvider.query(...)(ContentProviderNative 的 Binder 代理)
│ └ 目标进程 ContentProvider.Transport 直接收 binder 线程调用
│ └ 回到你的 query()——注意:跑在 Binder 线程池,不是主线程!
└ 返回 Cursor:实际是 CursorWindow + 继续查询句柄 的窗口化游标四个工程要点:
- 线程安全是硬要求:
Transport收 binder 线程调用,多个客户端并发打你时 query 并发执行(除非你覆写让回调转投主线程的旧写法——性能灾难,勿念)。 - CursorWindow 是共享内存(
frameworks/base/core/java/android/database/CursorWindow.java,native 实现在frameworks/base/core/jni/android_database_CursorWindow.cpp):query 只回传第一窗口(默认典型约 2MB,可配置,以 AOSP 为准)+ 一个 binder 句柄,后续moveToPosition越窗时经IContentProvider.transactContinueQuery续拉(Android 12 起为显式的续查协议,接口细节以 AOSP 为准)。 - stable vs unstable:
acquireUnstableProvider拿到的 binder 死亡你自理(游标变空),acquireProvider/stable 则系统代管死信;自己写客户端时用ContentProviderClient并正确release()。 - 超时:客户端等 provider 就绪有超时(历史 10s 量级,
publishContentProviders/getContentProvider路径,以 AOSP 为准),慢 init 的 provider 会以「query 超时/ANR」形式报复回来。
四、跨进程游标的真面目:窗口化、非快照
直觉里 Cursor 是「一次查询的结果集」,实际是按需分页的活物:
- 客户端游标 = CursorWindow(本地映射的共享内存)+ 远端游标句柄;窗口装不下的行在遍历时才续拉。
- 因此
cursor.getCount()可能触发远端往返;遍历时持有 Cursor 跨越 UI 动画/协程挂起,远端 provider 进程可能已死 →StaleDataException。
现代客户端正确用法(拿到的是「租约」,记得还):
kotlin
val client = context.contentResolver
.acquireUnstableProviderForMediaType(uri, MediaTypes.APP_MEDIA)? // API 30+
?: return
client.use { c-> // Closeable,自动 release
c.query(uri, arrayOf("title", "created"), "size>?",
arrayOf("1048576"), null)?.use { cur ->
while (cur.moveToNext()) {
val title = cur.getString(0) // 行数据来自共享内存窗口
}
}
}- 大结果集的正确姿势:限定 projection、加 selection、分页自己控(
LIMIT/OFFSET由你传进 selectionArgs 语义);绝不要把整表拉进内存。 - 写操作同理:
insert/update/delete直接走 binder 调用到 Binder 线程,notifyChange再经 ContentObserver 链回到注册方(观察者注册要记得registerContentObserver/unregister成对)。
Binder 传输的微观层面可以一句话讲死:游标数据本体走共享内存(CursorWindow 的 ashmem/memfd),Binder 只传句柄、窗口描述与续查请求——这是「跨进程大结果」不炸 Binder 缓冲区的根本原因,也是为什么它不能跨「provider 数据实时变更」给你一致性保证。
text
窗口化游标时序(极简版):
客户端 query ──▶ 服务端 open:先 COUNT + 定位器
◀── CursorWindow#1(前 N 行)+ token
遍历到窗口外 ──▶ transactContinueQuery(token, startPos, 2)
◀── CursorWindow#2(下一段)
中途服务端数据变更/进程死亡 ──▶ 客户端读到 StaleDataException/空游标五、文件共享:FileProvider 与 DocumentsProvider
文件不能用 file:// URI 传(target 24+ 直接 FileUriExposureException/StrictMode 击杀),规范通道是 content URI + 临时读权限:
- FileProvider(AndroidX
androidx.core.content.FileProvider):manifest 配<paths>白名单目录,把文件映射成content://authority/子目录/文件名,发 intent 时带FLAG_GRANT_READ_URI_PERMISSION;跨重启长期访问用takePersistableUriPermission。 - MediaStore(Android 10+ 分区存储):媒体文件优先走
MediaStore的 content URI,配合MediaColumns查询;MediaStore.Downloads还允许「带来源标记的下载」。 - DocumentsProvider(framework 基类
frameworks/base/core/java/android/provider/DocumentsProvider.java):系统级文件提供接口,云盘/SD 卡以DocumentsContract文档树形式暴露;App 想「当网盘」就实现它,用户经 SAF(系统文件选择器)授权后,通过takePersistableUriPermission拿到跨重启的长期读权。
AOSP 自己的文件类 provider(下载管理、媒体库、设置导出等)都遵循同一套「content URI + grantUriPermissions + path 白名单」的纪律,写新 provider 时先看它们抄纪律,而不是自己发明。
xml
<!-- FileProvider 的 paths 白名单(res/xml/file_paths.xml) -->
<paths>
<files-path name="docs" path="reports/"/>
<cache-path name="tmp" path="share/"/>
<external-path name="ext" path="Download/"/>
</paths>六、写一个生产级 Provider 的检查单
- 冷启动:onCreate 里只做本 provider 必需初始化;数据库 migration 别挡 publish 路径(放首次 query 或懒建)。
- 并发:以 Binder 线程视角审查每一行代码;SQLite 用 WAL + 单连接管理;绝不在 query 里做网络 IO。
- 契约:暴露
CONTENT_URI常量与列名(契约类),selectionArgs 用占位符防注入;不认识的 URI 抛异常。 - 性能:
notifyChange别高频广播全表失效;大表提供「增量查询」参数(since=时间戳类)。 - 安全:exported + 权限 + URI 授权三件套按需组合;查询列白名单(别人传什么都接 = 列注入风险)。
- 测试:用
adb shell content query/read/write直测 authority:
bash
adb shell content query --uri content://com.example.provider/notes --projection title
adb shell content insert --uri content://com.example.provider/notes \
--bind title:s:"hello" --bind body:s:"from adb"
adb shell content read --uri content://com.example.provider/notes/1七、常见误区问答
Q1:provider 能跑后台任务吗? 它只是数据入口。进程可能被 provider 调用拉活,但 Android 8 后它不给你起后台服务的豁免;把「provider 唤醒」当保活通道是给自己埋雷。
Q2:为什么我的 Cursor 在子线程崩了? 游标不是线程安全的:跨线程使用要加锁,或者读完立刻关,把需要的行物化进局部变量。
Q3:同 authority 的两个 App 会怎样? 后安装的拿不到该 authority 的路由(同签名共享 android:sharedUserId 的老玩法已被时代淘汰),表现为 Unknown URL——全局唯一命名空间,authority 必须带包名。
Q4:query 里能做业务校验/限流吗? 可以且应该——query/update 就是你的服务端入口,越权、超频、超大结果都在这里挡;返回空集合不是万能回答,权限不足该抛 SecurityException。
Q5:provider 之间能互相查询吗? 能,但按普通跨进程客户端对待;环形依赖(A 查 B、B 查 A)会在冷启动期互相等 publish,表现为超时。跨 provider 聚合应该放到应用层或系统服务层做。
版本注记:基于 Android 16 / API 36;Android 7 差异:
file://暴露开始被杀;Android 10 差异:分区存储重塑媒体 provider 使用面;Android 11 差异:QUERY_ALL_PACKAGES/包可见性影响 provider 探测;Android 12 差异:续查窗口化协议调整(以 AOSP 为准);Android 13~16 差异:媒体权限细分(READ_MEDIA_*、部分照片访问)叠加在查询语义之上。
本页小结:ContentProvider 的三件本质事——它是最早发布的组件(先于 Application.onCreate,慢即罪)、Binder 线程上的公开 API(并发与安全都要按服务端标准写)、窗口化的流式游标(共享内存翻页而不是快照拷贝)。文件共享按 FileProvider/MediaStore/DocumentsProvider 三条规范通道走,URI 授权机制贯穿始终。
参考链接
- https://developer.android.com/guide/components/contentproviders
- https://developer.android.com/reference/androidx/core/content/FileProvider
- https://developer.android.com/reference/android/provider/DocumentsProvider
- https://cs.android.com/android/platform/frameworks/base/+/main:core/java/android/content/ContentProvider.java
- https://cs.android.com/android/platform/frameworks/base/+/main:core/java/android/database/CursorWindow.java
- 站内:Activity、权限模型、架构篇 Binder