ARTICLE DETAIL

资讯详情

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

LoaderManager源码分析之二:LoadTask、AsyncTask与CursorLoader的协作链路拆解

LoaderManager源码分析之二:LoadTask、AsyncTask与CursorLoader的协作链路拆解 1. 从一次联系人列表卡顿说起LoadTask 到底在背后做了什么如果你写过 Android 联系人列表、短信会话列表或者任何用CursorLoader加载数据库的页面大概率遇到过这样的困惑明明只调用了getLoaderManager().initLoader()数据怎么就自己回来了数据库一改界面怎么又自己刷新了这背后其实是一条相当长的链路——LoaderManager负责调度LoaderInfo负责记账LoadTask负责跑腿AsyncTask负责线程CursorLoader负责真正查库ForceLoadContentObserver负责监听变化。这篇就聚焦这条链路里最容易被忽略、但最核心的一环LoadTask如何借助AsyncTask驱动CursorLoader完成异步加载。我会把任务创建、启动、回调、销毁四个阶段拆开配上可对照源码复现的验证步骤。适合已经用过 Loader 但没读过源码的 Android 开发者也适合想搞清楚「异步加载 数据监听」这套组合拳怎么落地的人。读完之后你应该能自己在本地把LoadTask的生命周期完整跑一遍而不是停留在「大概知道它是个 AsyncTask」的层面。2. 前置准备TaoToken 接入与源码阅读环境读源码这件事最怕的是环境没搭好翻到一半被编译错误打断。我建议先把两件事准备好一个是能快速验证模型对源码理解的环境另一个是能直接跳转的 AOSP 或 support 库源码。如果你手头没有现成的 API Key可以先去 TaoToken 官网注册一个账号然后在控制台创建 API Key。整个过程不复杂注册后在 console 页面点「API Keys」就能生成。拿到 Key 之后你可以用它来跑一些辅助理解源码的对话比如把AsyncTaskLoader的片段贴进去问「这里的 mTask 在 cancel 之后为什么还能复用」比自己硬啃快很多。具体入口我列一下方便你按需跳转用途地址官网首页https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentAPI 接入地址https://taotoken.net/api模型对话问源码https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodel_chatutm_campaignrewrite控制台建 Keyhttps://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewriteAPI Keys 管理https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite接入文档https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite源码这边LoaderManager、LoadTask、AsyncTaskLoader都在androidx.loader或老版本的android.support.v4.content包里。用 Android Studio 打开任意一个用了 Loader 的项目在initLoader上按住 Ctrl 点进去就能一路跟到LoadTask。如果你习惯看在线源码AOSP 的frameworks/support/loader目录也能找到对应文件。注意不同 AndroidX 版本里LoadTask的字段名可能略有差异比如mTask、mCancelled这些但整体结构是一致的。读的时候以你项目里实际依赖的版本为准。3. LoadTask 三部曲doInBackground、onPostExecute、onCancelled 的可复制配置LoadTask是AsyncTaskLoader的内部类继承自AsyncTask。它重写了三个方法doInBackground、onPostExecute、onCancelled。注意它没有重写onProgressUpdate所以进度更新这条路是断的——这也是为什么 Loader 不适合做带进度条的加载。3.1 doInBackground真正查库的地方doInBackground跑在线程池的线程里这是整条链路里唯一不在主线程执行的环节。它的核心逻辑是调用mLoader.loadInBackground()而CursorLoader对这个方法的实现就是一次ContentResolver.query。// CursorLoader.loadInBackground 的核心片段 public Cursor loadInBackground() { synchronized (this) { if (isLoadInBackgroundCanceled()) { throw new OperationCanceledException(); } mCancellationSignal new CancellationSignal(); } try { Cursor cursor getContext().getContentResolver().query( mUri, mProjection, mSelection, mSelectionArgs, mSortOrder, mCancellationSignal); if (cursor ! null) { try { cursor.getCount(); // 确保 cursor window 被填充 cursor.registerContentObserver(mObserver); } catch (RuntimeException ex) { cursor.close(); throw ex; } } return cursor; } finally { synchronized (this) { mCancellationSignal null; } } }这里有两个细节值得注意。第一cursor.getCount()不是可有可无的它强制把 CursorWindow 填满否则后面读数据可能触发二次查询。第二registerContentObserver把ForceLoadContentObserver挂到了 Cursor 上这是后面数据库变化能通知到 UI 的关键。有些项目会像ContactEntryListFragment那样重写onLoadInBackground但只是包一层 try-catch 打个 log实际逻辑还是走 super。这种重写对链路没有影响读源码时可以直接跳过。3.2 onCancelled主线程上的清理onCancelled跑在主线程负责在任务被取消时关闭 Cursor。CursorLoader对它的实现很直接Override public void onCanceled(Cursor cursor) { if (cursor ! null !cursor.isClosed()) { cursor.close(); } }这一步很重要。如果 Cursor 不关CursorWindow 的内存就泄漏了。所以 Loader 被 reset 或 destroy 时这条路径必须走到。3.3 onPostExecute把结果递回去onPostExecute同样在主线程它调用mLoader.deliverResult(data)而deliverResult又会触发mListener.onLoadComplete(this, data)。这个mListener是谁就是LoaderInfo。public void deliverResult(D data) { if (mListener ! null) { mListener.onLoadComplete(this, data); } }到这里数据从后台线程回到了主线程并且交给了LoaderInfo。接下来就是回调的分发。4. 注册与回调LoaderInfo 如何把结果送到 onLoadFinishedLoaderInfo是LoaderManager的内部类它实现了两个接口Loader.OnLoadCompleteListener和Loader.OnLoadCanceledListener。在LoaderInfo.start()里会把自己注册给 LoadermLoader.registerListener(mId, this); mLoader.registerOnLoadCanceledListener(this);registerListener的实现很简单就是把mListener指向传入的 listener同时记下 idpublic void registerListener(int id, OnLoadCompleteListenerD listener) { if (mListener ! null) { throw new IllegalStateException(There is already a listener registered); } mListener listener; mId id; }当onLoadComplete被触发时LoaderInfo会调用callOnLoadFinished最终走到mCallbacks.onLoadFinished(loader, data)。这个mCallbacks就是你在 Activity 或 Fragment 里实现的LoaderManager.LoaderCallbacks接口。所以整条回调链是LoadTask.onPostExecute→Loader.deliverResult→LoaderInfo.onLoadComplete→LoaderInfo.callOnLoadFinished→LoaderCallbacks.onLoadFinished你可以用下面这段代码在onLoadFinished里打个断点然后顺着调用栈往上翻就能看到完整的链路Override public void onLoadFinished(LoaderCursor loader, Cursor data) { Log.d(LoaderChain, onLoadFinished, loader loader , cursor data); // 在这里打断点查看调用栈 mAdapter.swapCursor(data); }实测下来调用栈里会依次出现LoadTask.onPostExecute、ModernAsyncTask.finish、LoaderInfo.onLoadComplete这几层和源码结构完全对得上。5. 观察者模式数据库一变UI 怎么知道前面提到CursorLoader.loadInBackground里注册了ForceLoadContentObserver。这个类是Loader的内部类继承自ContentObserverpublic final class ForceLoadContentObserver extends ContentObserver { public ForceLoadContentObserver() { super(new Handler()); } Override public boolean deliverSelfNotifications() { return true; } Override public void onChange(boolean selfChange) { onContentChanged(); } }onContentChanged会触发onForceLoad而AsyncTaskLoader.onForceLoad的实现是Override protected void onForceLoad() { super.onForceLoad(); cancelLoad(); mTask new LoadTask(); executePendingTask(); }也就是说数据库一变旧的 LoadTask 被 cancel新的 LoadTask 被创建并丢进线程池重新查询。查完之后再走一遍onPostExecute→onLoadFinishedUI 就更新了。这就是「数据库变化自动刷新」的完整闭环。你可以这样验证在联系人列表页面打开后用 adb 往数据库插一条记录观察 logcat 里是否出现onForceLoad和新的onLoadFinished。adb shell content insert --uri content://com.android.contacts/raw_contacts \ --bind account_name:s:test --bind account_type:s:test如果链路正常你会看到列表自动多出一项同时 logcat 里有对应的回调日志。6. 本篇常见错排查问题一onLoadFinished 不回调。先检查initLoader是否传了正确的 id以及LoaderCallbacks是否实现完整。如果onCreateLoader返回 null链路直接断掉。问题二Cursor 泄漏警告。多半是onCancelled没走到或者onLoadFinished里 swapCursor 之后没有 close 旧 cursor。用 StrictMode 可以快速定位。问题三数据库变了但 UI 不刷新。检查registerContentObserver是否执行。如果loadInBackground里 query 返回 null注册就不会发生。另外如果 ContentProvider 没有正确 notifyChange观察者也不会收到通知。问题四LoadTask 重复创建导致查询抖动。频繁触发onForceLoad时每次都会 cancel 旧任务再建新任务。如果数据库变化很频繁可以考虑加防抖或者改用其他加载方案。问题五AsyncTask 线程池被占满。LoadTask用的是AsyncTask的线程池如果同时有大量 Loader 在跑可能排队。这种情况一般出现在多 Tab 同时加载的场景需要评估是否要限制并发。如果你在排查过程中需要快速验证某个 API 的行为可以用 TaoToken 的模型对话把源码片段贴进去问比翻文档快。接入方式参考文档里的说明API 地址是https://taotoken.net/apiKey 在控制台的 API Keys 页面生成。7. 把链路跑通之后下一步做什么读到这里你应该能把LoadTask的生命周期完整串起来了initLoader触发LoaderInfo.startstart创建LoadTask并注册监听LoadTask.doInBackground调CursorLoader.loadInBackground查库并注册观察者查完onPostExecute把结果递给LoaderInfoLoaderInfo再回调onLoadFinished。数据库一变ForceLoadContentObserver触发onForceLoad整个流程重跑一遍。如果你想继续深入可以顺着LoaderManagerImpl看initLoader和restartLoader的区别或者对比AsyncTaskLoader和Loader在取消逻辑上的差异。长期做 Android 编码和 Agent 相关开发的话可以考虑用 Coding Plan 把这类源码阅读和验证流程固化下来减少重复搭环境的时间。
返回列表