Android提供了几种异步加载数据的方式,AsyncTaskLoader就是其中一种,这里对它的用法做一个总结。
Android在3.0引入了Loader(加载器),支持在Activity或Fragment中异步加载数据。想要在低版本上使用Loader可以用v4兼容包。
3.0之后官方文档强烈推荐使用Loader加载数据。
Loader的特性
- 可用于每个 Activity 和 Fragment。
- 支持异步加载数据。
- 监控其数据源并在内容变化时传递新结果。
- 在某一配置改变(如横竖屏切换)后重建Loader时,会自动重新连接上一个Loader的游标(cursor)。 因此,它们无需重新查询数据。
LoaderManager
一个与 Activity 或 Fragment 相关联的的抽象类,用于管理一个或多个 Loader 实例。 这有助于应用管理与 Activity 或 Fragment 生命周期相关联的、运行时间较长的操作。
它最常见的用法是与 CursorLoader 一起使用,你也可以实现自己的Loader来加载其他类型的数据。
每个 Activity 或Fragment中只有一个 LoaderManager。但一个 LoaderManager 可以有多个Loader。
LoaderManager.LoaderCallbacks
一个回调接口,用于客户端与 LoaderManager 进行交互。例如,你可以使用 onCreateLoader() 回调方法创建新的Loader。
Loader
一个执行异步数据加载的抽象类,是Loader的基类。
通常使用 CursorLoader,你也可以实现自己的子类。
Loader处于活动状态时,它们应该监视它们的数据源并且在数据改变时传送新的结果。
Loader本身并不支持异步加载机制,所以当我们编写自己的Loader的时候,不应该直接继承Loader类,应该继承AsyncTaskLoader,AsyncTaskLoader支持异步加载机制。
AsyncTaskLoader
一个使用AsyncTask来执行异步加载工作的抽象类。
CursorLoader
AsyncTaskLoader的子类,查询ContentResolver然后返回一个Cursor。
该类以标准游标查询实现了Loader协议,它的查询是通过AsyncTaskLoader在后台线程中执行,所以不会阻塞UI线程。使用这个Loader是从ContentProvider异步加载数据的最好方式。
框架结构
启动Loader
通常要在Activity的onCreate()方法中或Fragment的onActivityCreated()方法中初始化Loader,可以按照下面的方式创建:
//准备Loader,重连一个已存在的Loader或者启动新的Loader
//id:一个唯一的ID用于标识这个Loader,在这个例子中是0;
//args:可选的参数,在Loader初始化时作为参数传入,本例中是null;
//callbacks:一个LoaderManager.LoaderCallbacks的实现,LoaderManager 将调用此实现来报告Loader事件;在这个例子中,当前类实现了这个接口,所以传的是自身的引用:this。
getLoaderManager().initLoader(0, null, this);
initLoader() 确保了Loader已初始化且处于活动状态。这可能会出现两种结果:
- 如果 ID 所指的Loader已存在,则将重复使用上次创建的Loader,这时候args会被忽略,因为重用了之前的Loader。
- 如果 ID 所指的Loader不存在,则 initLoader() 将触发 LoaderManager.LoaderCallbacks 中的回调方法 onCreateLoader()。在此方法中,你可以在这里实例化并返回新的Loader。
在这两种情况下,传入的LoaderManager.LoaderCallbacks的实现都与Loader绑定在一起,并且会在Loader状态变化时被调用;
如果在调用这个initLoader()方法时,Loader已经处于启动状态(也就是说这个Loader已存在),并且所请求的Loader已产生了数据,那么系统会马上调用onLoadFinished(),所以你必须为这种情况的发生做好准备。
intiLoader()会返回一个创建的Loader,但是你不用获取它的引用,因为LoadeManager会自动管理该Loader的生命周期,你只用在它回调提供的生命周期方法中做自己数据逻辑的处理即可。
重启Loader
当你使用initLoader()时,如果指定ID的Loader已经存在,则它使用这个Loader;如果不存在,它将创建一个新的。但是有的时候你却想丢弃旧的Loader然后开始一个新的Loader。
要想丢弃旧的Loader,应该使用restartLoader()。例如,下面这个SearchView.OnQueryTextListener的实现在用户查询发生改变时重启了Loader,Loader需要重启从而才能使用新的搜索过滤词来进行一次新的查询。
public boolean onQueryTextChanged(String newText) {
// 当搜索内容变化时,更新搜索过滤词,重启Loader并用这个搜索过滤词重新查询
mCurFilter = !TextUtils.isEmpty(newText) ? newText : null;
getLoaderManager().restartLoader(0, null, this);
return true;
}
使用LoaderManager中的回调
LoaderManager.LoaderCallbacks是一个回调接口,它使得客户端可以与LoaderManager进行交互。
Loader,尤其是CursorLoader,我们希望在它停止后依然保持数据,这让应用可以在Activity或Fragment的onStop()和onStart()方法保持数据;当用户返回应用时,他们不需要再等待数据加载,你可以使用LoaderManager.LoaderCallbacks中的方法,在需要时创建新的Loader,并且告诉应用什么时候要停止使用Loader中的数据。
LoaderManager.LoaderCallbacks包含以下方法:
onCreateLoader()
根据传入的ID,初始化并返回一个新的Loader。当你试图去操作一个Loader时(比如,调用initLoader()方法),它会检查是否指定ID的Loader已经存在,如果不存在,将会触发LoaderManager.LoaderCallbacks中的onCreateLoader(),你需要在这里创建一个新Loader并返回。
onLoadFinished():当一个Loader完成了数据加载之后调用。
这个方法确保会在Loader上的数据被释放之前被调用。在此方法中,你必须移除所有对旧数据的使用(因为它们将很快会被删除),但是不要自己去释放它们,因为Loader会去做这些事情。
Loader一旦了解到应用不再使用这些数据时,将马上释放这些数据。
例如,如果数据是一个从CursorLoader来的Cursor,你不应该自己调用cursor的close()方法;如果cursor被放置在一个CursorAdapter中,你可以使用swapCursor()方法进行新数据交换,这样,旧的cursor就不会被关闭(CursorLoader的实现中会自动帮你关闭旧的cursor),就不会导致Adapter加载异常。
onLoaderReset():当一个Loader被重置,从而使得其数据无效时被调用。
这个回调告诉你什么时候数据将被释放,所以你可以释放对它的引用。
下面实现了调用参数为null的swapCursor()
public void onLoaderReset(Loader<Cursor> loader) {
mAdapter.swapCursor(null);
}
例子
下面的例子实现了从通讯录加载用户昵称,通过SearchView过滤搜索结果。
因为要读取通讯录,所以要现在Manifest中声明READ_CONTACTS 权限
public class CursorLoaderActivity
extends Activity
implements SearchView.OnQueryTextListener,
LoaderManager.LoaderCallbacks<Cursor> {
private SimpleCursorAdapter mAdapter;
private String mCurFilter;// 搜索过滤器
private ListView mListView;
private TextView mTxtEmpty;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_loader);
mListView = (ListView) findViewById(R.id.listview);
mTxtEmpty = (TextView) findViewById(R.id.txt_empty);
mListView.setEmptyView(mTxtEmpty);//没有数据时显示的view
mAdapter = new SimpleCursorAdapter(this,
android.R.layout.simple_list_item_2, null,
new String[]
ContactsContract.Contacts.DISPLAY_NAME,
ContactsContract.Contacts.CONTACT_STATUS},
new int[]{android.R.id.text1,
android.R.id.text2}, 0);
mListView.setAdapter(mAdapter);
// 启动loader,可以重连一个已经存在的也可以启动一个新的
getLoaderManager().initLoader(0, null, this);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
//在ActionBar上显示SearchView
MenuItem item = menu.add("Search");
item.setIcon(android.R.drawable.ic_menu_search);
item.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
SearchView sv = new SearchView(this);
sv.setOnQueryTextListener(this);
item.setActionView(sv);
return super.onCreateOptionsMenu(menu);
}
public boolean onQueryTextChange(String newText) {
//当搜索字符串变化时调用
//使用新的搜索过滤器重启loader
mCurFilter = !TextUtils.isEmpty(newText) ? newText : null;
getLoaderManager().restartLoader(0, null, this);
return true;
}
@Override
public boolean onQueryTextSubmit(String query) {
return true;
}
static final String[] CONTACTS_SUMMARY_PROJECTION = new String[]{
ContactsContract.Contacts._ID,
ContactsContract.Contacts.DISPLAY_NAME,
ContactsContract.Contacts.CONTACT_STATUS,
ContactsContract.Contacts.CONTACT_PRESENCE,
ContactsContract.Contacts.PHOTO_ID,
ContactsContract.Contacts.LOOKUP_KEY,
};
/**
* 创建一个CursorLoader并返回,如果initLoader()指定id的loader不存在,会触发这个回调方法
*
* @param id 给loader指定的id,如果只有一个loader,就不用管id
* @param args 传递的参数
* @return
*/
@Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
//根据是否对结果过滤来确定uri
Uri baseUri;
if (mCurFilter != null) {
baseUri = Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_FILTER_URI,
Uri.encode(mCurFilter));
} else {
baseUri = ContactsContract.Contacts.CONTENT_URI;
}
String select = "((" + ContactsContract.Contacts.DISPLAY_NAME + " NOTNULL) AND ("
+ ContactsContract.Contacts.HAS_PHONE_NUMBER + "=1) AND ("
+ ContactsContract.Contacts.DISPLAY_NAME + " != '' ))";
CursorLoader cursorLoader = new CursorLoader(
this,
baseUri,//uri:要获取内容的uri
CONTACTS_SUMMARY_PROJECTION, //要返回的列的集合,传入null将会返回所有列(比较低效)
select,//一个过滤器,表示哪些行将会被返回,格式化成类似SQL where子句的样子,传入null将返回所有行
null,//selectionArgs:可以在selection中包含一些'?',它将被本参数的值替换掉,这些值出现的顺序与'?'在selection中出现的顺序一致,值将作为字符串
ContactsContract.Contacts.DISPLAY_NAME + " COLLATE LOCALIZED ASC"//sortOrder:排序字符串,格式化成类似于SQL ORDER BY子句样子(除去ORDER BY),传入null将使用默认顺序,默认顺序可能是无序的
);
return cursorLoader;
}
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
//释放cursor(系统会帮我们关闭旧的cursor)
mAdapter.swapCursor(data);
}
public void onLoaderReset(Loader<Cursor> loader) {
//当onLoadFinished()中的cursor将要被关闭的时候调用,我们需要确保它不再被使用
mAdapter.swapCursor(null);
}
}
什么时候不该用Loader
如果你需要确保后台任务执行完成,就不要用Loader,因为当Activity或Fragment被销毁的时候,这个Activity或Fragment里面的Loader也会被销毁。如果要执行后台任务,可以使用Service。
要记得Loader是用来帮助你创建相应的UI,异步加载数据到这些UI上。这就是Loader和创建它的组件的生命周期绑定到一起的原因,不要滥用Loader!
在Service中使用
LoaderManager明显的是为Activity或Fragment生命周期而设计的来管理Loader的类,因为Service没有这些状态变化,所以没必要在Service中使用LoaderManager。
但是如果你已经为Activity或Fragment写好了Loader,现在要在Service中重用这些代码,可以不使用LoaderManager,直接操作Loader。
在Service被创建的时候,create, register, start你的Loader
@Override
public void onCreate() {
mCursorLoader = new CursorLoader(context, contentUri, projection, selection, selectionArgs, orderBy);
mCursorLoader.registerListener(LOADER_ID_NETWORK, this);
mCursorLoader.startLoading();
}
使Service implements OnLoadCompleteListener接口,在回调函数onLoadComplete中绑定数据和UI
@Override
public void onLoadComplete(Loader<Cursor> loader, Cursor data) {
}
在onDestroy中销毁Loader
@Override
public void onDestroy() {
// Stop the cursor loader
if (mCursorLoader != null) {
mCursorLoader.unregisterListener(this);
mCursorLoader.cancelLoad();
mCursorLoader.stopLoading();
}
}
当然你也可以这样写
private MyCursorLoader mMyCursorLoader;
Loader.OnLoadCompleteListener<Cursor> mCursorListener = new Loader.OnLoadCompleteListener<ConnectionStatus>() {
@Override
public void onLoadComplete(Loader<Cursor> loader, Cursor data) {
}
};
Bundle bundle = new Bundle();
bundle.putString(BUNDLE_KEY_UID, uid);
mMyCursorLoader = new MyCursorLoader(mContext, bundle, null, this);
mMyCursorLoader.registerListener(LOADER_1, mCursorListener);
mMyCursorLoader.startLoading();
AsyncTaskLoader & AsyncTAsk
AsyncTaskLoader
优势:会自动刷新数据变化,会自动处理Activity配置变化造成的影响,适合处理纯数据加载。
劣势:不能实时通知UI刷新,不能在onLoadFinished时主动切换生命周期(比如replace fragment)。
AsyncTask
优势:可以与UI实时交互及replace操作。
劣势:不会自动处理Activity配置变化造成的影响。
Loader生命周期
…
几个坑
Loader id重复导致数据逻辑异常
多线程中restartLoader导致Loader抛出异常(都在UI线程中执行就行了)
参考:
- https://developer.android.com/guide/components/loaders.html?hl=zh-cn#app
- http://2dxgujun.com/post/2014/11/14/Use-Loaders-in-Android.html
- 源码分析:http://wangkuiwu.github.io/2014/06/25/Loader/
- http://www.grokkingandroid.com/using-loaders-in-android/
- http://stackoverflow.com/questions/8696146/can-you-use-a-loadermanager-from-a-service
- 用法介绍及源码分析:http://blog.csdn.net/yanbober/article/details/48861457
扩展阅读:
http://codetheory.in/asynchronous-background-execution-and-data-loading-with-loaders-framework-in-android/