先把结论放在前面:ContentProvider 是四大组件里唯一"不是给界面用的",它本质是跨进程的表结构接口 + 数据通知机制。两件事必须立住:① 它的调用不经过 Binder 代理的常规路径——每次 query 都是一次完整的事务,因此计数严格;② 它的实例在应用进程启动时就被创建**,早于 Application.onCreate,这既是它的价值,也是它的坑。
ContentProvider 一次讲透:它是什么、怎么工作、项目里怎么用、坑在哪。
执行路径:从客户端到 provider 的一次完整事务
val cursor = contentResolver.query(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI, // 或自建 authority
arrayOf(MediaStore.Images.Media.DISPLAY_NAME, MediaStore.Images.Media.SIZE),
"${MediaStore.Images.Media.SIZE} > ?", // selection
arrayOf("1024"), // selectionArgs
"${MediaStore.Images.Media.DATE_ADDED} DESC" // sortOrder
)
cursor?.use {
while (it.moveToNext()) {
/* 读取列 */ } }
contentResolver.query 内部做的事,答出来就是机制层:
① 客户端把调用包成 IContentProvider 的 Binder 代理调用;② 系统按 authority 在 ActivityManagerService 找到目标进程(必要时先拉起它);③ 通过 Cursor 通道返回结果——注意数据量是分批传输的,CursorWindow 有容量上限(常见 2MB),数据量大时 Cursor 会自动 fillWindow 分批取,这也是"查大表要加分页或限制列"的原因。
ContentProvider 的线程模型也常被问:query/update/insert 默认跑在调用方进程的主线程(除非用 setCallingPackage 或在 provider 侧用 ContentProvider 的 attachInfo 时的 multiprocess 机制自行处理),所以绝不能在主线程查大表。applyBatch 可以在一个事务里执行多个操作,既省 IPC 又保证原子性。
最常见的坑是
在 ContentProvider 的 onCreate 里初始化重资源,拖慢所有进程冷启动。
因为 ContentProvider 的 onCreate 早于 Application.onCreate,而且它在每个访问它的进程里都会执行一次。于是两个后果:① 冷启动时你在 provider 里初始化 MMKV、加载配置、建数据库,会直接拉长启动时间;② 更隐蔽的是,如果你的 ContentProvider 被别的进程访问(比如系统或第三方 SDK 顺带查了一下你的 authority),那个进程会被拉起并执行你的 onCreate——即使它跟你毫无关系。
规范做法是把 onCreate 留空,重初始化放到 Application.onCreate,provider 里只做必须立刻可用的事:
class AppDbProvider : ContentProvider() {
// 故意保持极轻:不做任何重初始化
override fun onCreate(): Boolean = true
override fun query(
uri: Uri, projection: Array<out String>?, selection: String?,
selectionArgs: Array<out String>?, sortOrder: String?
): Cursor? {
// 延迟到真正要用时才拿数据库
return context?.contentResolver?.let {
db(it) }.query(
dbHelper.readableDatabase, TABLE, projection, selection, selectionArgs, null, sortOrder
)
}
// Android 11+ 必须在清单里显式导出:android:exported="false"
// 且要处理 <queries> 可见性,否则 Android 11 上查不到其它应用的数据
}
还有一个更隐蔽的坑:用到 ContentProvider 的代码旁边要写明适用前提,review 时才有据可依。可落地的检查项:① 清单里必须显式声明 android:exported(Android 12 起强制,否则安装失败);② onCreate 里不许有重逻辑,用 Lint 或自研扫描在 CI 阶段拦;③ query 走子线程(Kotlin 侧用 withContext(Dispatchers.IO),Java 侧用 AsyncTask 或 StrictMode 检测);④ 数据量大的接口要分页,避免 CursorWindow 溢出与 ANR。
三条高频追问
问:ContentProvider 怎么通知数据变化?
调用 contentResolver.notifyChange(uri, null),客户端用 ContentObserver 注册观察。notifyChange 传一个 null observer 表示需要通知所有注册的观察者。ContentResolver.registerContentObserver 的第三个参数 notifyForDescendants 表示是否通知 URI 的子路径——这个参数设错是"改了数据但界面不刷新"的常见原因。
// 客户端
contentResolver.registerContentObserver(
uri, true, object : ContentObserver(Handler(Looper.getMainLooper())) {
override fun onChange(selfChange: Boolean) = reload()
}
)
问:为什么说"ContentProvider 在 Application.onCreate 之前创建"?
ActivityThread.handleBindApplication 的顺序是:创建 Instrumentation → installContentProviders(遍历清单里的 provider 并调用 onCreate)→ mInstrumentation.callApplicationOnCreate。这个顺序是内容库初始化的基础(因为 Settings/Contacts 等系统 provider 必须在应用初始化前可用),但也导致 provider 的 onCreate 里拿不到 Application.onCreate 里才有的东西。实践上的应对就是"provider 保持极轻,重活交给 Application"。
问:自定义 ContentProvider 和直接用 SharedPreferences 有什么区别?
四点:① 跨进程——SharedPreferences 在多进程下会读到脏数据(早期版本还会抛 MODE_MULTI_PROCESS 的问题),ContentProvider 是 Binder 通信,天然一致;② 有通知机制——改完数据能通知观察者,SharedPreferences 只能主动轮询;③ 有权限体系——可以按读写粒度声明权限;④ 有事务——apply/insert 能在批量操作时保证原子性。选型判据:数据需要跨进程或需要变更通知才用 ContentProvider,只是本进程内的键值配置,SharedPreferences 更简单。
问:Android 11 的包可见性影响 ContentProvider 吗?
影响的是跨应用查询。系统会过滤掉目标应用不可见时返回的结果,即使 authority 正确也可能查不到,需要用 <queries> 声明目标包名。本应用内的 provider 不受影响。
落到项目里怎么做
一条能直接落地的规则:自建 ContentProvider 只在"确实需要跨进程或需要数据变更通知"时用,其余场景用 Room + Flow + 跨进程场景下的 Binder 回调替代。这条规则能挡掉大量"为了架构而架构"的 provider。
配套实践三条:① provider 拆分成"轻壳 + 真实实现"——onCreate 空实现,重逻辑委托给 Repository 单例(注意多进程下这个单例各进程各有一份,状态不共享,需要从数据源读而不是读静态字段);② 用 UriMatcher 明确路由表并为每个 path 写单元测试;③ 敏感数据分读写权限(readPermission / writePermission),别一个权限打通。
给正在准备面试的你
ContentProvider 这题属于"背了 API 就以为懂了"的类型。推荐这条答题线:先说清它不是给界面用的、是跨进程表结构接口 + 变更通知 → 讲执行路径(Binder 代理 → AMS 找进程 → CursorWindow 分批传输)→ 主动点出 onCreate 早于 Application.onCreate 且会被无关进程触发 → 落到"provider 保持极轻、重活交给 Application"的工程原则 → 补上变更通知与 ContentObserver 的 notifyForDescendants 参数、自定义 vs SharedPreferences 的选型判据。把这几点串起来,答案就轻描淡写了。
如果这篇文章对你有帮助,欢迎点赞、在看、转发三连。你的支持就是这个系列持续更新的动力。
「Android软件开发面试·从入门到精通」连载系列
上一篇:BroadcastReceiver:静态注册与动态注册的存亡
下一篇预告:Intent-与-IntentFilter:显式隐式跳转与匹配规则
有任何问题欢迎在评论区留言交流。