ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Android ContentObserver实战:从原理到避坑,轻松监听数据变化

Android ContentObserver实战:从原理到避坑,轻松监听数据变化 1. 为什么我们需要ContentObserver从一次“相册不刷新”的线上事故说起先讲一个我自己的真实案例。几年前接手过一个社交类App里面有“发布动态后自动刷新图片列表”的功能。当时的实现方式很粗暴用户在发布页选完图回传一个结果码列表页在onResume里重新查一次数据库。听起来没毛病对吧直到QA提了一个bug从系统相册删除一张照片回到App后列表里的缩略图还在点进去却加载失败。我当时第一反应是“缓存没清”排查了一下午最后发现问题的根子是App根本不知道系统相册里的数据变了。数据库里的记录还在缩略图缓存还在但实际文件已经被删了。就算你把onResume里的查询逻辑写得再完美也架不住“数据源变了但没人通知你”这个事实。后来我换了思路不再轮询、不再靠生命周期回调碰运气而是给相册的MediaStore注册了一个ContentObserver。效果立竿见影系统相册有增删改App立刻收到通知我再在回调里重新查询并刷新界面。那个bug从此消失而且顺带优化掉了原来onResume里那套重复查询的逻辑。这个经历让我意识到一个问题很多Android开发者对ContentObserver的认知停留在“知道有这个类”“面试背过题”的层面但真正遇到“数据变化需要感知”的场景时要么用轮询硬扛要么用广播绕远路很少有人能第一时间想到ContentObserver才是那个最贴合系统设计哲学的答案。这篇文章不打算教你背API而是想通过“什么时候必须用它、它底层怎么工作、实际项目里怎么用不踩坑”这条线把这个组件彻底讲透。无论你是刚入门Android的新人还是写了两三年业务代码但很少碰系统组件的进阶开发者这篇文章都能帮你省掉一些我当年走过的弯路。2. ContentObserver到底是什么先搞懂它和ContentProvider的“共生关系”很多教程一上来就甩ContentObserver的代码但如果你不理解它和ContentProvider的关系代码背得再熟也容易用错。这一节我们先把底层逻辑说清楚。2.1 数据变化通知的完整链路从“数据库变了”到“界面刷新”ContentObserver的全称意思是“内容观察者”它观察的不是任意数据而是通过ContentProvider暴露的数据。在Android系统里ContentProvider是跨进程共享数据的标准通道——系统相册、通讯录、短信、媒体库都是通过各自的ContentProvider把数据暴露给其他App的。你自己的App如果建了数据库也可以用ContentProvider把数据共享给别的进程。整个通知链路是这样的数据变更某个进程对ContentProvider背后的数据源做了增、删、改操作比如往相册插入了一张新图片。发送通知操作方通过ContentResolver.notifyChange(uri, observer)主动告知系统“这个uri代表的数据变了”。注意ContentProvider本身不会自动感知数据变化必须由操作方显式调用notifyChange这是理解整个机制的关键。系统转发ContentResolver内部通过IObserver跨进程通信把变更事件发给所有注册了该uri的ContentObserver。回调执行你的ContentObserver.onChange()被触发你在回调里重新查询数据、刷新界面。我第一次画这张链路图的时候心里冒出的想法是这不就是观察者模式嘛。对ContentObserver本质就是观察者模式在跨进程场景下的具体实现只不过中间那个“被观察者”不是普通的类而是Android系统里的ContentProvider。2.2 一个容易搞混的概念ContentObserver和FileObserver的区别我在技术社群里经常看到有人把ContentObserver和FileObserver混为一谈。这俩名字很像但完全是两码事对比维度ContentObserverFileObserver观察对象ContentProvider 暴露的数据数据库、文件等文件系统的目录和文件触发时机有人调用notifyChange时才触发文件被创建、删除、修改、移动时触发是否需要权限取决于访问对应ContentProvider的权限不需要但Android高版本对某些目录有访问限制跨进程能力支持跨进程监听仅限本进程内监听典型场景监听相册、短信、通讯录变化监听自己App数据库变化监听App私有目录下的配置文件修改有一个很常见的误用场景你的App往私有目录写了一个配置文件你想监听这个文件的变化。很多人第一反应是“我用ContentObserver行不行”答案是不行——私有目录的文件不经过ContentProviderContentObserver根本感知不到。这种情况应该用FileObserver或者干脆在写入的地方手动回调。反过来如果你要监听的“变化”是某个App通过ContentProvider暴露的业务数据那FileObserver也完全派不上用场。两个组件各有各的适用边界选错了就是白折腾。2.3 和监听相关的那些“兄弟组件”选型之前先对比一轮在Android开发中“监听数据变化”这件事有好几种实现方式ContentObserver只是其中一种。我整理了一个对比表格方便你遇到具体场景时快速选型ContentObserver感知ContentProvider数据变化跨进程、精准、轻量缺点是只能监听ContentProvider暴露的数据且依赖对方主动调用notifyChange。BroadcastReceiver系统全局广播或自定义广播跨进程能力很强但静态广播在Android 8.0后受限制动态广播需要手动注册注销而且广播是“一次性的”没有“持续观察”的概念。LiveData生命周期感知适合UI层观察数据但基本只用于同一个进程内且观察的是内存中的对象变化不是磁盘或数据库的变化。Flow/Coroutine响应式编程配合room的Flow扩展可以实现数据库变化的观察但那是Room框架帮你封装好的底层依然是查询轮询或InvalidationTracker和系统级ContentObserver不是一个层面的东西。我的选型经验只要数据是通过ContentProvider暴露的优先用ContentObserver因为它是系统原生机制省电、省资源、实时性有保证。如果只是观察自己应用内的内存状态用LiveData或Flow更合适。如果观察的是文件系统变化用FileObserver。三者分工明确互相不能完全替代。3. 动手实现从注册到回调再到注销一次把代码写对这一节进入实操。ContentObserver的使用套路非常固定一共三步创建观察者、注册观察者、注销观察者。但实际操作中每一步都有一些细节值得展开说说。3.1 第一步创建你自己的ContentObserver子类ContentObserver是一个抽象类你需要继承它并重写onChange方法。先看一个实际项目中监听系统相册变化的完整代码class MediaObserver( private val onChange: (Uri) - Unit ) : ContentObserver(Handler(Looper.getMainLooper())) { override fun onChange(selfChange: Boolean, uri: Uri?) { super.onChange(selfChange, uri) // uri可能为null做一层保护 if (uri ! null) { onChange.invoke(uri) } } }这里有两个细节值得说明第一个细节构造函数里的Handler参数。ContentObserver的构造方法接收一个Handler这个Handler决定了onChange回调在哪个线程执行。如果你传Handler(Looper.getMainLooper())回调就在主线程如果你不传或者传null回调就会在Binder线程池里执行。这个选择很关键——如果你的回调里要直接更新UI就要传主线程的Handler如果回调里要做耗时操作就别传主线程的Handler等拿到结果后再切主线程更新UI。第二个细节onChange有两个重载版本。老版本是onChange(boolean selfChange)新版本是onChange(boolean selfChange, Uri uri)。如果你不重写带Uri参数的版本回调时拿到uri永远是null在某些场景下就没法区分是哪个数据变了。所以只要你的目标是Android 4.4以上API 19务必重写带Uri参数的那个重载。3.2 第二步注册观察者Uri匹配的精确性决定你的性能注册的核心代码是contentResolver.registerContentObserver(uri, notifyForDescendants, observer)。这个方法的三个参数每一个都有讲究参数含义实际建议uri要监听的数据的Uri能精确就精确实在需要监听整个目录再用父UrinotifyForDescendants是否监听该Uri下所有子Uri的变化监听目录时设为true监听具体某一行时设为falseobserver上一步创建的ContentObserver实例要持有引用注销时要用同一个实例以监听系统相册为例常见写法是// 获取系统相册的ContentResolver val resolver context.contentResolver // 监听MediaStore中所有图片的变化 val uri MediaStore.Images.Media.EXTERNAL_CONTENT_URI observer MediaObserver { changedUri - // 收到通知后重新查询相册 reloadGallery() } resolver.registerContentObserver(uri, true, observer)这里notifyForDescendants设成了true意思是“只要external/images/media这个路径下的任何子路径有变化都会通知我”。比如插入一张新图片系统会notifyChange(具体某一行图片的uri)通知会冒泡到我注册的父Uri上我就能收到。反过来如果你只关心特定某一行的变化比如监听某条联系人记录被修改val singleUri ContentUris.withAppendedId( ContactsContract.Contacts.CONTENT_URI, contactId ) resolver.registerContentObserver(singleUri, false, observer)此时notifyForDescendants设为false意思是“只有这个具体的Uri变化我才关心子路径的变化跟我无关”。这个参数直接影响你的回调触发频率——监听范围越大无效回调越多你的代码就得做越多的过滤判断。3.3 第三步注销观察者不注销就是在给内存泄漏留门注销的代码很简单override fun onDestroy() { super.onDestroy() resolver.unregisterContentObserver(observer) }但“简单”不等于“不重要”。ContentResolver.registerContentObserver是一个跨进程注册系统服务ContentService会在它的Binder对象池里为你的进程维护一个观察者列表。如果你注册了不注销那个观察者对象就不会被回收你的Activity或Fragment即使已经销毁仍然会被系统持有。这就是标准的内存泄漏路径。我的项目规范里有一条硬性要求注册和注销必须成对出现且要保证注销方法一定被执行。在Activity里registerContentObserver写在onStart或onCreate那注销就一定要写在onStop或onDestroy在Fragment里同理。如果你用ViewModel做数据层监听可以在onCleared()里统一注销这样就不怕旋转屏幕导致重复注册。还需要特别提醒一点同一个ContentObserver实例不能重复注册同一个Uri否则会收到重复回调。如果你在onResume里注册、onPause里注销要确保注册前先调用一次注销避免“重复注册但只注销了一次”的尴尬override fun onResume() { super.onResume() resolver.unregisterContentObserver(observer) // 防止重复注册 resolver.registerContentObserver(uri, true, observer) }之前在群里看到有人问“为什么我收到两遍回调”十有八九就是没做这个防重复注册处理。4. 从“能跑”到“好用”一个监听相册变化的完整实战案例代码能跑通和代码好用是两回事。这一节我用一个“监听系统相册变化自动更新最新一张图片”的完整场景把前面讲的知识点串起来同时看看真实项目里会遇到哪些细节问题。4.1 需求拆解与方案设计先看需求App首页有一张横幅展示相册里最新的一张图片。要求是用户从相册删除图片、新增图片、编辑图片保存后这张横幅都要自动更新。而且App本身没有前台服务用户可能切到相册操作后再回到App也可能在App内通过系统拍照功能新增图片。这个需求如果用轮询最简单粗暴的做法是每隔几秒查一次相册——但耗电、卡顿、不优雅而且用户操作后要等下一个轮询周期才能看到更新体验很差。用广播呢系统确实会在媒体库变化时发ACTION_MEDIA_SCANNER_FINISHED这类广播但那是“扫描完成”的时机不是“数据变化”的时机而且高版本系统对隐式广播的限制越来越多。用ContentObserver的优势就非常明显注册一次持续有效数据一变更立刻收到回调精确到Uri不用轮询、不怕广播限制。4.2 完整代码实现查询最新图片的细节先写一个查询最新图片的工具类object MediaQueryHelper { fun getLatestImageUri(context: Context): Uri? { val resolver context.contentResolver val projection arrayOf( MediaStore.Images.Media._ID, MediaStore.Images.Media.DATE_ADDED ) // 按时间倒序取最新一条 val sortOrder ${MediaStore.Images.Media.DATE_ADDED} DESC LIMIT 1 return resolver.query( MediaStore.Images.Media.EXTERNAL_CONTENT_URI, projection, null, null, sortOrder )?.use { cursor - if (cursor.moveToFirst()) { val id cursor.getLong( cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID) ) ContentUris.withAppendedId( MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id ) } else { null } } } }注意sortOrder里的LIMIT 1——ContentResolver.query的排序参数是直接拼到SQL里的所以支持LIMIT。这个写法比查完所有数据再取第一条高效得多尤其是相册里图片很多的时候。再写一个封装好的监听器处理注册、回调、注销的生命周期class GalleryChangeObserver( private val context: Context, private val onNewImage: (Uri?) - Unit ) { private val resolver context.applicationContext.contentResolver private val observer object : ContentObserver(Handler(Looper.getMainLooper())) { override fun onChange(selfChange: Boolean, uri: Uri?) { // 哪怕是删除操作也重新查一次最新图片 onNewImage.invoke(MediaQueryHelper.getLatestImageUri(context)) } } fun start() { resolver.unregisterContentObserver(observer) // 防止外部重复调用start() resolver.registerContentObserver( MediaStore.Images.Media.EXTERNAL_CONTENT_URI, true, observer ) } fun stop() { resolver.unregisterContentObserver(observer) } }然后在使用方这么接入class HomeFragment : Fragment() { private lateinit var galleryObserver: GalleryChangeObserver override fun onStart() { super.onStart() galleryObserver GalleryChangeObserver(requireContext()) { newUri - binding.banner.load(newUri) // 更新横幅图片 } galleryObserver.start() } override fun onStop() { super.onStop() galleryObserver.stop() } }4.3 UI层刷新策略回调是高频的界面刷新是要节流的我的onChange回调是在主线程执行的而且系统相册的任何增删改都会触发它。如果你在回调里直接做图片加载和界面刷新遇到用户批量删除几十张照片的场景回调会瞬间触发几十次你的UI就会被反复刷新几十次。实际上onChange回调本身可能已经做了合并但几十张图批量删除时你依然可能收到多次回调。这时候就要考虑在UI层做“节流”或“去重”。我常用的做法是加一个简单的Debounceprivate var refreshJob: Job? null private fun onGalleryChanged() { refreshJob?.cancel() refreshJob lifecycleScope.launch { // 200毫秒内的多次变更只响应最后一次 delay(200) val latestUri MediaQueryHelper.getLatestImageUri(requireContext()) binding.banner.load(latestUri) } }这样一来用户批量操作时你的App最多在操作停止后200毫秒刷新一次体验和性能都能兼顾。这个小技巧在线下分享时经常被问到原理也很简单把高频事件聚合成低频事件避免无意义的重复工作。4.4 别忘了Android 10的分区存储权限适配如果你的App要在Android 10API 29及以上监听相册变化还要注意一个权限变化分区存储。在分区存储模式下App访问其他App创建的媒体文件不需要READ_EXTERNAL_STORAGE权限但你无法直接通过文件路径访问这些文件只能通过MediaStore的uri访问。好消息是ContentObserver监听的是MediaStore的ContentProvider和分区存储适配得很好——你注册观察者监听的本来就是MediaStore的uri回调时拿到的也是uri不需要文件路径。坏消息是如果你在回调里尝试用旧方式比如Environment.getExternalStorageDirectory()拼路径去访问图片文件在Android 10上大概率会碰壁Android 11上更是直接抛异常。所以监听相册变化的逻辑写好了还不够回调里访问图片的方式也要按新规则来。正确姿势是拿到uri后用ContentResolver.openInputStream(uri)读取内容或者用ImageDecoder解码而不是拼文件路径。5. 避坑手册ContentObserver开发中最容易踩的6个坑写完上面的实战案例下面聊一聊我这些年用ContentObserver过程中踩过的坑有些坑是自己在线上环境被用户投诉后才意识到的这里一次性都列出来帮你提前躲开。5.1 坑一自定义ContentProvider的数据变化没触发onChange这是最经典的坑。很多人发现自己写的ContentObserver永远收不到回调排查半天发现不是观察者的问题而是提供数据的一方压根没有调用notifyChange。前面讲过ContentProvider的数据变化通知不是系统自动检测的而是操作方在修改数据后手动通知的。比如你在ContentProvider的insert方法里写了数据库插入逻辑但忘了加这一行override fun insert(uri: Uri, values: ContentValues?): Uri? { val rowUri db.insert(...) // 通知观察者数据变了 context.contentResolver.notifyChange(uri, null) return rowUri }那所有注册了这个uri的ContentObserver都收不到任何通知。所以排查ContentObserver失灵问题时第一步不是看观察者代码而是看数据提供方有没有调用notifyChange。这是很多人容易忽略的盲区。5.2 坑二selfChange参数的理解误区onChange回调里的selfChange参数表示“这次变化是不是我这个进程自己造成的”。如果你在App内通过ContentResolver更新了数据然后另一处又监听了同一个uri回调里的selfChange会是true。但这里有个陷阱selfChange的判定机制在不同版本的Android上并不完全一致。在部分系统实现中只有调用notifyChange时传入了同一个ContentObserver实例selfChange才会被判定为true。所以如果你把selfChange作为“自己进程的变化就不处理”的过滤条件在某些系统上可能会漏掉一些本该处理的回调也可能出现“自己进程的变化也触发了”的情况。我的建议是不要过度依赖selfChange做逻辑判断。如果你能通过uri精确判断数据变化的来源优先用uri做判断如果你需要过滤自己进程的变化更靠谱的方式是在回调里做业务校验而不是只看selfChange这个布尔值。5.3 坑三主线程回调中的耗时操作我见过不止一个项目在ContentObserver.onChange里直接做数据库查询、文件读取、甚至网络请求。如果你的Handler传的是主线程Looper回调本身就在主线程再来一波耗时操作轻则界面卡顿重则ANR。正确的思路是回调里只做“知道数据变了”这件事把耗时的数据查询和UI更新放到工作线程或异步任务里。比如前面实战案例里的方案回调里启动协程在Default或IO调度器上重新查询数据拿到结果后再切回主线程更新UI。这样回调本身很快对主线程几乎没有影响。5.4 坑四观察整个数据库目录导致回调风暴有些开发者在监听自己App的数据库时图省事把整个content://com.example.app.provider都注册了notifyForDescendants还设成了true。结果App里任何一个表的数据变化都会触发回调回调里再做一次全量数据刷新性能一下就崩了。精确的范围总是优于宽泛的范围。如果只想监听某张表的变化就注册这张表对应的uri如果只想监听某一行就把uri精确到那一行。notifyForDescendants也不是无脑设true只有在确实需要监听“某类数据的所有变化”时才应该打开。5.5 坑五进程被杀后观察者自动失效恢复时机要处理好ContentObserver的注册是跟进程生命周期绑定的。如果App的进程被系统回收了比如长时间在后台重新回到前台时进程会重建你之前注册的观察者自然就没了。如果你只在onCreate里注册一次没有在onStart或onResume里做恢复就会出现“冷启动后第一次能收到通知之后被杀掉再回来就收不到”的诡异现象。我的实践方案是把注册放在onStart里注销放在onStop里确保每个前台周期都重新注册。如果你用ViewModel持有观察者用onCleared()来注销也要确保ViewModel是“每个生命周期都重建的”否则进程恢复后ViewModel还在但观察者没重新注册一样收不到回调。5.6 坑六混淆规则和APK瘦身时的“小坑”很多App上线前会开启混淆如果ContentObserver的子类没有被正确保留回调可能失效。实际上onChange是ContentObserver基类的公有方法正常情况下混淆不会影响它但一些激进的自定义混淆规则可能重命名你的子类字段导致来回映射出问题。稳妥做法是给ContentObserver相关的类加上混淆保留规则-keep class * extends android.database.ContentObserver { *; }写不写这行规则的区别在开发环境下通常看不出来但一旦上线后用户手机系统版本繁杂各种诡异的“通知时灵时不灵”就有可能跟这个有关。反正加一行也不费事建议直接加上。6. 进阶玩法不止监听系统数据还能做广告归因和风控埋点基础用法讲完了下面说说ContentObserver在真实业务里的两个进阶应用场景帮你打开思路。6.1 进阶场景一监听短信验证码自动填充输入框很多App有“接收验证码后自动填充”的功能。实现方案有好几种比如申请READ_SMS权限后直接读收件箱或者用SMS Retriever API。但ContentObserver其实也有一套可行的实现注册监听content://sms数据库的变化收到回调后读最新一条短信提取验证码。class SmsObserver( private val context: Context, private val onSmsReceived: (String) - Unit ) : ContentObserver(null) { override fun onChange(selfChange: Boolean) { super.onChange(selfChange) val resolver context.contentResolver val cursor resolver.query( Uri.parse(content://sms/inbox), arrayOf(body), null, null, date DESC LIMIT 1 ) cursor?.use { if (it.moveToFirst()) { val body it.getString(0) // 用正则从短信内容里提取验证码 val code Regex(\\d{4,6}).find(body)?.value if (code ! null) { onSmsReceived.invoke(code) } } } } }注意监听content://sms需要READ_SMS权限这是敏感权限申请时一定要给用户明确说明用途。我个人建议优先用官方的SMS Retriever API因为不需要额外权限而且更安全但如果你的产品确实需要兼容老版本或者手上有READ_SMS权限ContentObserver方案依然是一个可行的备选。6.2 进阶场景二用ContentObserver实现广告归因和用户行为统计广告归因有一个经典问题用户点击了广告下载并安装了App第一次打开App时你怎么知道这次安装是哪个广告渠道带来的常见的做法是服务端下发的渠道包名匹配或者Install ReferrerAPI。但还有一个额外的手段如果渠道包在安装后往某个ContentProvider写入了标识信息你的App第一次启动时就可以通过ContentObserver监听这个ContentProvider的变化拿到渠道标识从而完成归因。类似地在风控场景里如果你的App有多个进程主进程需要感知其他进程产生的关键数据变化比如登录态被踢下线、账号被异地登录库表用ContentProvider暴露后主进程注册一个ContentObserver监听变化就可以实时感知并触发重新登录流程。这种方式比进程间用广播通信更轻量也比自己维护Binder跨进程接口更简单。这些场景的共同特点是数据跨进程共享且变化时机不可预测。只要满足这两个条件ContentObserver都是一个值得优先考虑的方案。6.3 进阶场景三用Handler实现节流之外的精细控制前面提过ContentObserver构造方法里的Handler参数能决定回调线程。这里再补充一个进阶技巧你可以自定义一个Handler的子类在分发消息时做延时节流这样就能在框架层面控制回调频率避免每个业务回调都自己写一遍Debounce逻辑。class ThrottleHandler( private val looper: Looper, private val interval: Long 300L ) : Handler(looper) { override fun handleMessage(msg: Message) { super.handleMessage(msg) // 只保留消息队列里最后一个消息中间的丢弃 removeMessages(msg.what) sendMessageDelayed(Message.obtain(msg), interval) } }onChange回调里通过sendMessage或者post把“通知UI刷新”这件事交给这个HandlerHandler会丢弃中间重复的消息只保留一个延迟执行的刷新任务。这样就实现了“无论底层回调多频繁UI最多每300毫秒刷新一次”的效果。6.4 和一个新同学的技术讨论能不能用ContentObserver监听整个App的数据库有一次团队里一个新同学问我能不能给App的数据库根uri注册一个ContentObserver这样所有表的变化都能统一感知我的回答是“能但不建议”。能的原因SQLite的ContentProvider支持通过notifyChange通知任意uri注册根uri确实能收到所有子uri冒泡上来的通知。不建议的原因有三个。第一粒度太粗任何一张表的任何一行变化都会触发回调业务逻辑根本没办法区分到底是哪个数据变了除非你解析uri的path但那很繁琐。第二性能开销通知频率高回调频繁即使做了节流也浪费了不少CPU。第三维护成本项目里新加一个表数据不需要改动注册代码看起来省事了但排查问题的难度直线上升——所有表的变化都汇到一个回调里出了问题你根本不知道是哪个模块触发的。我的建议是保持“按业务域注册”的思路一个业务组件只监听自己关心的uri范围。这样逻辑清晰排查问题也快。7. 写在最后的几点个人经验文章到这里关于ContentObserver的原理、实践、进阶用法已经讲得比较全了。最后分享几个我在实际项目中的个人习惯算不上标准答案但都是从踩坑中沉淀下来的经验。第一监听范围宁精确勿宽泛。无论是uri还是notifyForDescendants范围越大无效回调越多排查问题越困难。一个良好的观察者注册应该让人一看就知道“它在关心哪一块数据”。第二所有系统级监听都必须有配套的注销逻辑。ContentObserver不像LiveData那样自带生命周期管理它就是一个纯粹的观察者不会因为你的Activity销毁就自动解绑。把“注册/注销”成对写在同一个生命周期回调里是最不容易出错的习惯。第三回调里别做重活。就算你把Handler传成了主线程的也不代表回调就是安全的。数据变化通知只是一个信号重活放到工作线程去处理这是所有观察者模式的通用准则。第四多看系统源码。如果你想知道某个系统数据源在什么时机发出了notifyChange直接看AOSP里对应ContentProvider的源码就行。理解了通知时机你就能更好地设计自己的观察逻辑。ContentObserver这个组件单看API很简单但它背后是Android“数据共享主动通知”的设计哲学。用好了它你的App会变得更灵敏、更省电、更体面用不好轻则白折腾一场重则引入线上bug和性能问题。希望这篇文章能帮你在下次遇到“数据变化需要感知”的场景时不再犹豫直接掏出ContentObserver这个趁手的工具。
返回列表