ARTICLE DETAIL

资讯详情

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

Loader(加载器) 在 Android 中的演进:从 LoaderManager 到 CursorLoader 与 AsyncTaskLoader 的实践指南

Loader(加载器) 在 Android 中的演进:从 LoaderManager 到 CursorLoader 与 AsyncTaskLoader 的实践指南 1. 从一次通讯录卡顿说起Loader 到底解决了什么问题如果你在 Android 项目里做过列表数据加载大概率遇到过这样的场景Activity 一打开就去查数据库或 ContentProvider主线程被 Cursor 查询卡住列表白屏几百毫秒甚至直接 ANR。早期很多人的做法是把查询丢进 Thread 或 AsyncTask查完再 runOnUiThread 更新 UI。代码能跑但问题一堆Activity 旋转重建后线程还在跑回调打到已经销毁的 View 上数据源变了比如通讯录新增联系人界面不会自动刷新多个异步任务的生命周期没人统一管理。Loader 就是 Android 3.0 引入的一套专门解决这类问题的机制。它是什么一句话Loader 是在 Activity/Fragment 中异步加载数据、并自动监听数据源变化的组件。它能做什么异步查询不阻塞 UI、数据源内容变化时自动重新加载、配置变更如旋转屏幕后自动重连并复用已有数据不用重新查询。适合谁适合还在维护传统 View 体系、使用 ContentProvider/Cursor 做数据展示的 Android 开发者尤其是做通讯录、短信、媒体库、本地数据库列表这类场景的同学。这套机制的核心是三个角色LoaderManager 负责生命周期管理和实例调度CursorLoader 负责查询 ContentResolver 并返回 CursorAsyncTaskLoader 则是更通用的异步加载基类。理解它们的分工比死记 API 更重要。下面我会从演进脉络讲到可复制代码再到 Android Studio 里的验证步骤和常见报错排查尽量让你看完就能在项目里落地。需要说明的是Loader 在 Jetpack 时代已经被 ViewModel LiveData/Flow Room 逐步替代但大量存量项目、系统级应用、以及需要直接操作 Cursor 的场景仍然在用。所以这不是一篇过时技术考古而是一篇存量项目选型与迁移的实战指南。2. LoaderManager 生命周期管理initLoader 与 restartLoader 的取舍LoaderManager 是整条链路的调度中心。每个 Activity 或 Fragment 只有一个 LoaderManager 实例但它可以管理多个 Loader用整型 ID 区分。你不需要手动 new 它通过getLoaderManager()Fragment 里是getLoaderManager()Activity 里是getLoaderManager()或getSupportLoaderManager()拿到即可。初始化的标准写法是在onCreate()或 Fragment 的onActivityCreated()里调用// 准备 Loader要么重连已有的要么新建一个 getLoaderManager().initLoader(0, null, this);initLoader三个参数分别是Loader 的唯一 ID、传给 Loader 构造方法的可选 Bundle、以及实现了LoaderManager.LoaderCallbacks接口的实例通常传 this。它的行为有两种分支如果该 ID 的 Loader 已存在直接复用如果不存在触发回调里的onCreateLoader()去创建。无论哪种情况回调都会和这个 Loader 绑定状态变化时被调用。这里有个容易踩的坑如果调用initLoader时 Loader 已经启动并产生了数据系统会在initLoader执行过程中立即回调onLoadFinished()。也就是说你不能假设onLoadFinished一定在onCreate之后异步发生初始化代码里要能容忍数据提前到达。当数据需要刷新时用restartLoader而不是再次initLoader。典型场景是搜索框文字变化public boolean onQueryTextChange(String newText) { // 搜索文字变化时更新过滤条件重启 Loader 做新查询 mCurFilter !TextUtils.isEmpty(newText) ? newText : null; getLoaderManager().restartLoader(0, null, this); return true; }restartLoader会丢弃旧数据、重新走一遍创建流程保证查询用的是最新的过滤条件。而initLoader在 Loader 已存在时不会重新查询这就是两者最本质的区别。LoaderManager 最值钱的能力是生命周期托管。当 Activity 执行onStop()时Loader 不会销毁数据保留用户返回时onStart()数据还在不需要重新查询。配置变更旋转屏幕后LoaderManager 会自动重连新的 Loader 并从新 Cursor 取数据。这套机制让你基本不用手动碰 Loader 实例本身只需要在 Callbacks 里响应事件。如果你在 Fragment 里用注意getLoaderManager()在 API 28 之后被标记废弃推荐用LoaderManager.getInstance(this)或直接迁移到 ViewModel。但在存量项目里旧写法仍然能正常工作迁移时再逐步替换即可。3. CursorLoader 与 AsyncTaskLoader 的可复制配置这一节给你可以直接粘贴进项目的代码。先看 CursorLoader它是 AsyncTaskLoader 的子类专门查询 ContentResolver 返回 Cursor查询在后台线程执行不阻塞 UI。在 Fragment 中实现LoaderManager.LoaderCallbacksCursor核心是onCreateLoaderstatic final String[] CONTACTS_SUMMARY_PROJECTION new String[] { Contacts._ID, Contacts.DISPLAY_NAME, Contacts.CONTACT_STATUS, Contacts.CONTACT_PRESENCE, Contacts.PHOTO_ID, Contacts.LOOKUP_KEY, }; Override public LoaderCursor onCreateLoader(int id, Bundle args) { Uri baseUri; if (mCurFilter ! null) { baseUri Uri.withAppendedPath(Contacts.CONTENT_FILTER_URI, Uri.encode(mCurFilter)); } else { baseUri Contacts.CONTENT_URI; } String select (( Contacts.DISPLAY_NAME NOTNULL) AND ( Contacts.HAS_PHONE_NUMBER 1) AND ( Contacts.DISPLAY_NAME ! )); return new CursorLoader(getActivity(), baseUri, CONTACTS_SUMMARY_PROJECTION, select, null, Contacts.DISPLAY_NAME COLLATE LOCALIZED ASC); }CursorLoader 构造参数对应 SQL 查询的各个部分uri 是内容检索地址projection 是要返回的列传 null 返回所有列但效率低selection 是过滤条件相当于 WHERE 但不含 WHERE 本身selectionArgs 替换 selection 里的问号占位符sortOrder 是排序相当于 ORDER BY 但不含 ORDER BY。数据到达和重置的回调Override public void onLoadFinished(LoaderCursor loader, Cursor data) { // 换入新 Cursor框架会在返回后自动关闭旧 Cursor mAdapter.swapCursor(data); } Override public void onLoaderReset(LoaderCursor loader) { // 旧 Cursor 即将关闭确保不再使用它 mAdapter.swapCursor(null); }注意onLoadFinished里必须用swapCursor()而不是changeCursor()前者不会关闭旧 Cursor交给框架处理后者会关闭容易引发崩溃。onLoaderReset里把适配器的 Cursor 置空避免使用已失效的数据。如果你要加载的不是 ContentProvider 数据而是网络、文件或数据库就自定义 AsyncTaskLoader。核心是重写loadInBackground()public static class JsonAsyncTaskLoader extends AsyncTaskLoaderString { private String mData; private String mUrl; public JsonAsyncTaskLoader(Context context, String url) { super(context); mUrl url; } Override public String loadInBackground() { // 这里在后台线程执行可以放心做网络/IO return fetchJsonFromNetwork(mUrl); } Override public void deliverResult(String data) { if (isReset()) { return; } mData data; if (isStarted()) { super.deliverResult(data); } } Override protected void onStartLoading() { if (mData ! null) { deliverResult(mData); // 已有数据直接交付 } if (takeContentChanged() || mData null) { forceLoad(); // 数据变化或为空时重新加载 } } Override protected void onStopLoading() { cancelLoad(); } Override protected void onReset() { super.onReset(); onStopLoading(); mData null; } }这段代码的关键点onStartLoading里判断缓存有数据直接交付没有或数据变化才forceLoaddeliverResult里检查isReset()防止向已重置的 Loader 交付数据onReset里清理缓存。这套模板能避免重复加载和回调打到已销毁组件两个高频问题。4. 在 Android Studio 中验证请求与成功结果代码写完后怎么确认 Loader 真的按预期工作我给你一套可跟做的验证步骤。第一步配置权限。如果查询通讯录在AndroidManifest.xml里声明uses-permission android:nameandroid.permission.READ_CONTACTS /Android 6.0 以上还需要运行时申请权限否则 CursorLoader 返回的 Cursor 为空你会误以为代码有问题。第二步在 Fragment 里搭好 UI 和适配器。用SimpleCursorAdapter绑定 ListViewOverride public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); setEmptyText(暂无联系人); setHasOptionsMenu(true); mAdapter new SimpleCursorAdapter(getActivity(), android.R.layout.simple_list_item_2, null, new String[] { Contacts.DISPLAY_NAME, Contacts.CONTACT_STATUS }, new int[] { android.R.id.text1, android.R.id.text2 }, 0); setListAdapter(mAdapter); getLoaderManager().initLoader(0, null, this); }第三步加日志观察回调顺序。在onCreateLoader、onLoadFinished、onLoaderReset里各打一条 LogLog.d(LoaderDemo, onCreateLoader id id); Log.d(LoaderDemo, onLoadFinished count (data null ? 0 : data.getCount())); Log.d(LoaderDemo, onLoaderReset);第四步运行并观察 Logcat。正常流程应该是onCreateLoader→onLoadFinished countN。旋转屏幕后你会看到onCreateLoader可能不再触发Loader 被复用但onLoadFinished会带着数据再次回调界面数据不丢失。这就是 Loader 生命周期托管的价值。第五步验证数据监听。在系统通讯录里新增一个联系人回到你的 App观察 Logcat 是否自动出现新的onLoadFinished且 count 增加。如果出现说明 CursorLoader 对 ContentProvider 的监听生效了。第六步验证搜索过滤。在 SearchView 输入文字触发onQueryTextChange观察restartLoader后是否重新走onCreateLoader并返回过滤后的结果。实测下来这套验证流程能覆盖 Loader 的三大核心能力异步加载、数据监听、配置变更复用。如果某一步不符合预期对照下一节的排查清单。5. 本篇常见报错排查从 Cursor 空指针到 Loader 不回调Loader 相关的报错往往不直观我把高频问题和定位方法整理成对照表。第一个高频问题java.lang.IllegalStateException: attempt to re-open an already-closed object。这通常是在onLoadFinished里用了changeCursor()而不是swapCursor()导致旧 Cursor 被提前关闭而适配器还在引用它。解决方法是统一用swapCursor()并在onLoaderReset里swapCursor(null)。第二个问题Cursor 返回 null 或 count 为 0。先检查运行时权限是否授予再检查 ContentProvider 的 URI 是否正确。通讯录查询用Contacts.CONTENT_URI如果用了错误的 authority查询会静默返回空。可以在onLoadFinished里打印data null和data.getCount()来区分是没数据还是没权限。第三个问题onCreateLoader不回调。常见原因是initLoader的第三个参数传了 null或者传入的实例没有实现LoaderManager.LoaderCallbacks接口。另一个隐蔽原因是 Fragment 还没 attach 到 Activity 就调用了getLoaderManager()此时返回 null。确保在onActivityCreated或之后调用。第四个问题旋转屏幕后数据重复加载。如果你在onCreate里用了restartLoader而不是initLoader每次重建都会强制重新查询失去复用能力。初始化用initLoader只有数据条件变化时才用restartLoader。第五个问题onLoadFinished被调用多次界面闪烁。这通常是 AsyncTaskLoader 里没有正确处理takeContentChanged()和缓存导致onStartLoading每次都forceLoad。参考上一节的模板加上mData ! null判断和deliverResult缓存交付。第六个问题内存泄漏警告提示 Loader 持有 Activity 引用。自定义 Loader 时不要持有 Activity 的强引用用getContext()或 ApplicationContext。CursorLoader 内部已经处理好了自定义 AsyncTaskLoader 要注意。排查时建议打开 Logcat 过滤你的 TAG同时在onLoaderReset里加日志确认 Cursor 生命周期是否正常。大部分诡异问题都能通过观察回调顺序定位。6. 从 Loader 到现代方案选型建议与平滑迁移Loader 在 Android 3.0 到 Jetpack 普及之前是官方推荐的异步数据加载方案。它的设计目标很明确把异步、生命周期、数据监听三件事打包成一套可复用的机制。但它的局限也很明显API 偏重Cursor 和 ContentProvider 绑定较深自定义 Loader 模板代码多对非 Cursor 数据源不够友好。如果你在做新项目我的建议是直接用 ViewModel LiveData/Flow Room。Room 的FlowListT天然支持数据变化监听ViewModel 负责生命周期协程负责异步代码量比 Loader 少一半以上。但如果你在维护存量项目或者需要直接操作 Cursor比如系统级应用、媒体库扫描Loader 仍然是可靠选择不必为了技术新而强行重构。迁移路径可以这样走第一步把 CursorLoader 的查询逻辑抽到 Repository 层用 Room 或 ContentResolver 封装第二步用 ViewModel 持有数据LiveData 暴露给 UI第三步把onLoadFinished里的swapCursor替换成submitList或adapter.setData第四步删除 LoaderCallbacks 相关代码。整个过程可以按页面逐步替换不需要一次性重写。如果你在接入过程中需要统一管理 API Key、模型调用或编码 Agent 的配置可以到 TaoToken 的 API Keys 页面生成密钥配合接入文档完成配置。做模型对话验证可以走模型对话入口长期编码和 Agent 场景可以了解 Coding Plan。这些工具和 Loader 本身没有耦合只是在你做完整项目时可能用到的配套能力。最后留一个实用技巧无论用 Loader 还是 ViewModel判断数据是否真的变化永远比数据是否到达更重要。Loader 用takeContentChanged()帮你做了这件事现代方案里用distinctUntilChanged()达到同样效果。理解这个本质换不换技术栈都不会踩坑。
返回列表