资讯动态

Cursor 与 CursorAdapter 的观察者模式:从源码到可复用配置骨架

发布时间:2026/9/28 4:29:35 来源:尧图企业网站定制
1. 从一次「数据改了界面不动」说起如果你写过 Android 里ListViewCursorAdapter的组合大概率遇到过这种场景在后台线程往SQLite里插了一条记录ContentProvider的insert也正常返回了可列表就是不刷新非得手动调一次adapter.notifyDataSetChanged()才肯动。更迷惑的是有时候它又自己刷新了行为飘忽不定。这背后其实是 Android 一套挺精巧的观察者模式在起作用Cursor自己是个被观察者CursorAdapter是观察者ContentProvider通过Uri把变更事件广播出去最后一路传导到notifyDataSetChanged()。整条链路涉及AbstractCursor、ContentObservable、DataSetObservable、SelfContentObserver、ChangeObserver、MyDataSetObserver好几个类名字长得像绕口令但拆开看逻辑并不复杂。这篇就按「源码链路 → 可复制骨架 → 日志验证 → 常见坑」的顺序走一遍。适合已经用过CursorAdapter、但没搞清自动刷新原理的同学也适合想把这套机制封装成可复用配置骨架的人。我会给出一个能直接抄的CursorAdapter子类配上ContentObserver的注册与注销代码再用Log把每一步回调打出来让你亲眼看到数据变更怎么触发刷新。顺带说一句如果你在写这类 Android 代码时想用 AI 辅助生成骨架或排查回调顺序把 Key 和 API 通道统一管起来会省不少事后面会提到怎么用 TaoToken 做这件事。2. 观察者模式在 Cursor 链路里的三层结构先把整体结构理清楚不然看源码容易迷路。这套机制其实是三层观察者嵌套第一层ContentProvider和Cursor之间靠Uri通信。query()返回Cursor之前会调setNotificationUri()AbstractCursor内部会 new 一个SelfContentObserver注册到这个Uri上。当ContentProvider的insert/update/delete里调了notifyChange()这个SelfContentObserver就会收到回调。第二层Cursor和CursorAdapter之间。CursorAdapter构造时会 new 出ChangeObserver和MyDataSetObserver分别通过registerContentObserver()和registerDataSetObserver()注册到Cursor内部的两个主题对象mContentObservable和mDataSetObservable上。第三层Cursor内部的两个主题对象。ContentObservable和DataSetObservable都继承自Observable是标准的观察者模式主题类负责遍历通知所有注册的观察者。链路串起来就是notifyChange()→SelfContentObserver.onChange()→AbstractCursor.onChange()→mContentObservable.dispatchChange()→ChangeObserver.onChange()→onContentChanged()→mCursor.requery()→mDataSetObservable.notifyChanged()→MyDataSetObserver.onChanged()→notifyDataSetChanged()。看到没最后一步才是真正让界面刷新的。中间任何一环断了列表都不会动。最常见的断点就是FLAG_REGISTER_CONTENT_OBSERVER没设导致ChangeObserver压根没注册。3. TaoToken 前置把 AI 辅助配置的 Key 统一管起来写 Android 这类回调链路代码时我经常用 AI 帮忙生成骨架或者解释某段源码的执行顺序。但工具一多Key 散落在各个配置文件里改一次要翻好几个地方挺烦的。TaoToken 在这里的作用就是提供一个统一的 Key 和 API 通道入口把模型对话、编码辅助这些能力收敛到一处管理。它的官网入口是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 地址是 https://taotoken.net/api 注意 API 这个地址不带 UTM 参数直接填就行。如果你只是想让 AI 解释一下dispatchChange和notifyChange的区别用模型对话就够了如果是长期写 Android 项目、想让 AI 持续参与编码那 Coding Plan 更合适。具体操作上先去控制台建一个 Key然后按文档把 Key 配到你的开发环境里。这一步不用写进 Android 工程代码只是给你本地写代码时用的辅助通道。下面给个用 curl 验证 Key 是否可用的例子确认通道通了再往下写业务代码curl https://taotoken.net/api/v1/chat/completions \ -H Content-Type: application/json \ -H Authorization: Bearer $TAOTOKEN_API_KEY \ -d { model: claude-sonnet-4-20250514, messages: [ {role: user, content: 用一句话解释 Android Cursor 的 dispatchChange 和 notifyChange 区别} ] }返回里能看到正常的choices结构就说明通道没问题。这一步和后面的 Android 代码是分开的别把 Key 硬编码进CursorAdapter里那是两码事。4. 可复制的 CursorAdapter 子类骨架下面这个骨架是我实际项目里用过的简化版去掉了业务字段保留了观察者注册和注销的完整逻辑。关键点是构造时传FLAG_REGISTER_CONTENT_OBSERVER以及在swapCursor和close时正确注销。public class NoteCursorAdapter extends CursorAdapter { private static final String TAG NoteCursorAdapter; private ContentObserver mExtraObserver; public NoteCursorAdapter(Context context, Cursor c) { // 关键必须带 FLAG_REGISTER_CONTENT_OBSERVER super(context, c, FLAG_REGISTER_CONTENT_OBSERVER); } Override public View newView(Context context, Cursor cursor, ViewGroup parent) { return LayoutInflater.from(context) .inflate(R.layout.item_note, parent, false); } Override public void bindView(View view, Context context, Cursor cursor) { TextView title view.findViewById(R.id.tv_title); String text cursor.getString(cursor.getColumnIndexOrThrow(title)); title.setText(text); Log.d(TAG, bindView: text); } Override public Cursor swapCursor(Cursor newCursor) { Log.d(TAG, swapCursor: old getCursor() , new newCursor); return super.swapCursor(newCursor); } Override public void changeCursor(Cursor cursor) { Log.d(TAG, changeCursor called); super.changeCursor(cursor); } Override public void notifyDataSetChanged() { Log.d(TAG, notifyDataSetChanged triggered); super.notifyDataSetChanged(); } }这里有个容易忽略的点FLAG_AUTO_REQUERY在 API 11 之后已经废弃了别再用它。用FLAG_REGISTER_CONTENT_OBSERVER就够了它会让CursorAdapter在init()里 new 出ChangeObserver和MyDataSetObserver并注册到Cursor上。如果你还想监听别的Uri比如另一张表的变更可以自己加一个ContentObserver在swapCursor里注册、在close里注销public void registerExtraObserver(ContentResolver resolver, Uri uri) { if (mExtraObserver null) { mExtraObserver new ContentObserver(new Handler(Looper.getMainLooper())) { Override public void onChange(boolean selfChange) { Log.d(TAG, extra observer onChange, selfChange selfChange); // 这里可以触发重新 query 或局部刷新 } }; } resolver.registerContentObserver(uri, true, mExtraObserver); Log.d(TAG, extra observer registered for uri); } public void unregisterExtraObserver(ContentResolver resolver) { if (mExtraObserver ! null) { resolver.unregisterContentObserver(mExtraObserver); Log.d(TAG, extra observer unregistered); mExtraObserver null; } }注销这一步千万别省。ContentObserver如果注册了不注销Activity销毁后Cursor还持有引用轻则内存泄漏重则在后台收到回调操作已销毁的 View 直接崩。5. 用日志验证数据变更触发刷新光看代码不够直观咱们用日志把整条链路打出来。在ContentProvider的insert方法末尾加上notifyChange然后在CursorAdapter的几个关键回调里打日志。// ContentProvider 里 Override public Uri insert(Uri uri, ContentValues values) { SQLiteDatabase db mOpenHelper.getWritableDatabase(); long id db.insert(note, null, values); if (id 0) { Uri itemUri ContentUris.withAppendedId(CONTENT_URI, id); getContext().getContentResolver().notifyChange(CONTENT_URI, null); Log.d(NoteProvider, insert done, notifyChange: CONTENT_URI); return itemUri; } return null; }然后在Activity里触发一次插入观察 Logcat 输出顺序。正常应该看到类似这样的序列NoteProvider: insert done, notifyChange: content://com.example.app/note NoteCursorAdapter: notifyDataSetChanged triggered NoteCursorAdapter: bindView: 新插入的标题如果只看到第一行后面两行没出现说明观察者链路断了。这时候按顺序排查CursorAdapter构造时有没有传FLAG_REGISTER_CONTENT_OBSERVERContentProvider的query里有没有调setNotificationUrinotifyChange传的Uri和setNotificationUri注册的是不是同一个。我试过在AbstractCursor.onChange里加日志需要临时改源码或用反射能看到dispatchChange确实被调用了但ChangeObserver.onChange没触发最后发现是deliverSelfNotifications()返回了false。SelfContentObserver的deliverSelfNotifications返回false而ChangeObserver返回true这个差异决定了dispatchChange里if (!selfChange || observer.deliverSelfNotifications())这个条件能不能过。6. 本篇常见错排查6.1 列表不刷新日志里没有 notifyDataSetChanged最常见的原因是构造CursorAdapter时用了super(context, c)或super(context, c, 0)没传FLAG_REGISTER_CONTENT_OBSERVER。这样mChangeObserver和mDataSetObserver都是null压根没注册到Cursor上notifyChange自然传不到Adapter。改法就是构造时明确传FLAG_REGISTER_CONTENT_OBSERVER。如果你用的是CursorLoader它内部会处理好这些但手动管理Cursor时就得自己注意。6.2 报 IllegalArgumentException: column _id does not existCursorAdapter的init()里会调c.getColumnIndexOrThrow(_id)所以你的query投影里必须包含_id列。哪怕你不需要这个字段也得在SQLite表里建_id主键或者query时显式AS _id。SELECT id AS _id, title, content FROM note这个坑在自定义Cursor或联表查询时特别容易踩。6.3 内存泄漏Activity 销毁后仍收到 onChangeContentObserver注册后不注销ContentResolver会一直持有它而它内部可能持有Activity或View的引用。解决办法是在onDestroy或Adapter的close里调unregisterContentObserver。如果你用的是CursorAdapter自带的观察者swapCursor(null)或changeCursor(null)会帮你注销旧的但自己额外注册的必须手动注销。6.4 requery 在主线程卡顿onContentChanged()里调的是mCursor.requery()这是同步操作数据量大时会在主线程卡一下。如果列表数据多建议改用CursorLoader配合LoaderManager它在后台线程做查询查完再回调主线程更新。或者自己起线程重新query再swapCursor。6.5 notifyChange 传了 null 导致所有观察者都收到notifyChange(uri, null)里的第二个参数是ContentObserver传null表示通知所有注册在该Uri上的观察者。如果你只想通知特定的观察者可以传具体的 observer 实例。大多数场景传null就行但要知道它的含义避免误以为只通知自己。7. 把配置骨架沉淀下来这套观察者链路拆完其实可以沉淀成一个可复用的配置骨架一个带FLAG_REGISTER_CONTENT_OBSERVER的CursorAdapter基类一个负责注册/注销额外ContentObserver的工具方法再加上ContentProvider里统一的notifyChange封装。下次写新模块直接继承基类改改bindView就行。如果你在写这些骨架时想让 AI 帮忙补全回调顺序或者检查注销逻辑可以用 TaoToken 的模型对话快速问一下或者用 Coding Plan 让它持续参与整个 Android 模块的编码。Key 和通道在控制台统一管好接入文档里有各语言的具体配置示例。这样 AI 辅助和你的 Android 工程代码就分开了不会把 Key 混进业务逻辑里。最后留个实操建议在CursorAdapter的notifyDataSetChanged里打一行日志跑一次插入操作看日志顺序对不对。对上了说明整条观察者链路是通的对不上就按第 6 节的排查顺序逐个检查。这比盯着源码猜要快得多。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑