Android 异步加载神器Loader全解析

眼前多少难甘事,自古男儿当自强。这篇文章主要讲述Android 异步加载神器Loader全解析相关的知识,希望能为你提供帮助。
在之前呢,我们经常会有这种需求,比如在某个activity,或者某个fragment里面,我们需要查找某个数据源,并且显示出来,当数据源自己更新的时候,界面也要及时响应。
当然咯,查找数据这个过程可能很短,但是也可能很漫长,为了避免anr,我们都是开启一个子线程去查找,然后通过handler来更新我们的ui界面。但是,考虑到activity和
fragment 复杂的生命周期,上述的方法 使用起来会很不方便,毕竟你要考虑到保存现场 还原现场 等等复杂的工作来保证你的app无懈可击。所以后来呢谷歌就帮我们推出了一个
【Android 异步加载神器Loader全解析】新的东西---Loader。他可以帮我们完成上述所有功能!实在是很强大。
如果你有阅读英文技术文档的习惯 那么谷歌官方的文档 也许比我所说的更加完美。具体可以参考如下:
http://developer.android.com/intl/zh-cn/reference/android/app/LoaderManager.html
http://developer.android.com/intl/zh-cn/reference/android/content/AsyncTaskLoader.html
http://developer.android.com/intl/zh-cn/guide/components/loaders.html
我所述的内容也是主要基于上述三篇文档。
首先呢,我们来看第一个例子,这个例子也是官方的推荐了,我给简化了一下,主要是监听手机里 联系人这个数据源。当数据源改变的时候 自动update 我们的ui。

package com.example.administrator.modifytestview; import android.app.Activity; import android.app.FragmentManager; import android.app.ListFragment; import android.app.LoaderManager; import android.content.CursorLoader; import android.content.Loader; import android.database.Cursor; import android.net.Uri; import android.os.Bundle; import android.provider.ContactsContract.Contacts; import android.util.Log; import android.view.View; import android.widget.ListView; import android.widget.SimpleCursorAdapter; public class MainActivity extends Activity {@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); FragmentManager fm = getFragmentManager(); CursorLoaderListFragment list = new CursorLoaderListFragment(); fm.beginTransaction().replace(R.id.root, list).commit(); }public static class CursorLoaderListFragment extends ListFragment implements LoaderManager.LoaderCallbacks< Cursor> {// This is the Adapter being used to display the list\'s data. SimpleCursorAdapter mAdapter; // If non-null, this is the current filter the user has provided. String mCurFilter; @Override public void onActivityCreated(Bundle savedInstanceState) {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); //这个地方初始化了我们的loader getLoaderManager().initLoader(0, null, this); super.onActivityCreated(savedInstanceState); }@Override public void onListItemClick(ListView l, View v, int position, long id) { // Insert desired behavior here. Log.i("FragmentComplexList", "Item clicked: " + id); }// These are the Contacts rows that we will retrieve. static final String[] CONTACTS_SUMMARY_PROJECTION = new String[]{ Contacts._ID, Contacts.DISPLAY_NAME, Contacts.CONTACT_STATUS, Contacts.CONTACT_PRESENCE, Contacts.PHOTO_ID, Contacts.LOOKUP_KEY, }; //只会调用一次 public Loader< Cursor> onCreateLoader(int id, Bundle args) { // This is called when a new Loader needs to be created.This // sample only has one Loader, so we don\'t care about the ID. // First, pick the base URI to use depending on whether we are // currently filtering. Uri baseUri; if (mCurFilter != null) { baseUri = Uri.withAppendedPath(Contacts.CONTENT_FILTER_URI, Uri.encode(mCurFilter)); } else { baseUri = Contacts.CONTENT_URI; }// Now create and return a CursorLoader that will take care of // creating a Cursor for the data being displayed. 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"); }//每次数据源都有更新的时候,就会回调这个方法,然后update 我们的ui了。 public void onLoadFinished(Loader< Cursor> loader, Cursor data) {// Swap the new cursor in.(The framework will take care of closing the // old cursor once we return.) mAdapter.swapCursor(data); // The list should now be shown. if (isResumed()) { setListShown(true); } else { setListShownNoAnimation(true); } }public void onLoaderReset(Loader< Cursor> loader) { // This is called when the last Cursor provided to onLoadFinished() // above is about to be closed.We need to make sure we are no // longer using it. mAdapter.swapCursor(null); } }}

可以仔细的观察一下这个代码,我们能发现 使用loader所需要的一些步骤:
1.需要一个activity或者是fragment,当然在上述的例子里 我们使用的是fragment。
2.一个LoaderManger的实例,注意看53行,我们get了一个loadermanager。这个地方就是获取实例了。
3.需要一个CursorLoader,并且从contentProvider获取数据源,90-97行 就是这么做的。
4.需要实现一个LoaderCallBack的这个接口,然后在几个回调方法里 写上我们自己业务的逻辑 即可。你看34行就是继承的接口。
还有3个回调方法在那,我们都在里面实现了自己的逻辑。
到这,其实一看,思路还是很清晰的。那到这里 有人肯定要说了。你这个没用啊,要实现contentprovider,我们的app不需要做
数据共享的,能否直接操作数据库呢?答案是可以的。在这里我们也可以构造出一个场景。假设有一张学生表。我们点击add
按钮,就自动往这个表里面增加一个数据,然后下面有个listview 会自动捕捉到 这个数据源的变化,然后自动更新列表。
我们可以知道 上面那个demo里面 CursorLoader的定义是这样的
public class CursorLoader extends AsyncTaskLoader< Cursor> {

我们现在要实现一个不用contentProvider的Loader 也是基于AsyncTaskLoader来的。
先给出一个抽象类:
package com.example.administrator.activeandroidtest3; import android.content.AsyncTaskLoader; import android.content.Context; import android.database.Cursor; public abstract class SimpleCursorLoader extends AsyncTaskLoader< Cursor> { private Cursor mCursor; public SimpleCursorLoader(Context context) { super(context); }/* 在子线程里运作 */ @Override public abstract Cursor loadInBackground(); /* 在ui 线程里运作 */ @Override public void deliverResult(Cursor cursor) { if (isReset()) { // An async query came in while the loader is stopped if (cursor != null) { cursor.close(); } return; } Cursor oldCursor = mCursor; mCursor = cursor; if (isStarted()) { super.deliverResult(cursor); }if (oldCursor != null & & oldCursor != cursor & & !oldCursor.isClosed()) { oldCursor.close(); } }@Override protected void onStartLoading() { if (mCursor != null) { deliverResult(mCursor); } if (takeContentChanged() || mCursor == null) { forceLoad(); } }@Override protected void onStopLoading() { cancelLoad(); }@Override public void onCanceled(Cursor cursor) { if (cursor != null & & !cursor.isClosed()) { cursor.close(); } }@Override protected void onReset() { super.onReset(); onStopLoading(); if (mCursor != null & & !mCursor.isClosed()) { mCursor.close(); } mCursor = null; } }

然后我们再接着定义我们最终的 不需要provider的loader实现类(注意你如果想写的比较完美的话 cursor记得用抽象类的,抽象类的那个就不要写成private的了,我这里为了图简单 直接用自己构造的)。
package com.example.administrator.activeandroidtest3; import android.content.Context; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; /** * Created by Administrator on 2015/10/7. */ public class SpecialLoader extends SimpleCursorLoader {ForceLoadContentObserver mObserver = new ForceLoadContentObserver(); private Context context; public SpecialLoader(Context context) { super(context); this.context = context; }@Override public Cursor loadInBackground() { DatabaseHelper dh = new DatabaseHelper(context, "Test.db"); SQLiteDatabase database = dh.getReadableDatabase(); String table = "Student"; String[] columns = new String[]{"Name", "No"}; //这个地方因为我用的是activeandroid 的orm 框架,所以默认的自增长主键是Id,但是SimpleCursorAdapter //需要的是_id 否则会报错,所以这里要重命名一下 Cursor cursor = database.rawQuery("SELECT Id AS _id,Name,No FROM Student", null); if (database != null) { if (cursor != null) { //注册一下这个观察者 cursor.registerContentObserver(mObserver); //这边也要注意 一定要监听这个uri的变化。但是如果你这个uri没有对应的provider的话 //记得在你操作数据库的时候 通知一下这个uri cursor.setNotificationUri(context.getContentResolver(), MainActivity.uri); }} return cursor; } }

然后我们在简单看下activity 主类里的代码:
package com.example.administrator.activeandroidtest3; import android.app.Activity; import android.app.LoaderManager; import android.content.Loader; import android.database.Cursor; import android.net.Uri; import android.os.Bundle; import android.util.Log; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.widget.ListView; import android.widget.SimpleCursorAdapter; import android.widget.TextView; import com.activeandroid.query.Select; import java.util.List; import java.util.Random; public class MainActivity extends Activity implements LoaderManager.LoaderCallbacks {public static final Uri uri = Uri.parse("content://com.example.student"); private TextView addTv; private ListView lv; private SimpleCursorAdapter adapter; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); addTv = (TextView) this.findViewById(R.id.add); addTv.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Student student = new Student(); student.name = getRandomString(5); student.no = (int) (Math.random() * 1000) + ""; student.sex = (int) (Math.random() * 1); student.save(); //操作完数据库要notify 不然loader那边收不到哦 getContentResolver().notifyChange(uri, null); } }); lv = (ListView) this.findViewById(R.id.lv); adapter = new SimpleCursorAdapter(MainActivity.this, android.R.layout.simple_list_item_2, null, new String[]{"Name", "No"}, new int[]{android.R.id.text1, android.R.id.text2}, 0); lv.setAdapter(adapter); getLoaderManager().initLoader(0, null, this); }@Override public boolean onCreateOptionsMenu(Menu menu) { // Inflate the menu; this adds items to the action bar if it is present. getMenuInflater().inflate(R.menu.menu_main, menu); return true; }@Override public boolean onOptionsItemSelected(MenuItem item) { // Handle action bar item clicks here. The action bar will // automatically handle clicks on the Home/Up button, so long // as you specify a parent activity in AndroidManifest.xml. int id = item.getItemId(); //noinspection SimplifiableIfStatement if (id == R.id.action_settings) { return true; }return super.onOptionsItemSelected(item); }public static String getRandomString(int length) { //length表示生成字符串的长度 String base = "abcdefghijklmnopqrstuvwxyz0123456789"; Random random = new Random(); StringBuffer sb = new StringBuffer(); for (int i = 0; i < length; i++) { int number = random.nextInt(base.length()); sb.append(base.charAt(number)); } return sb.toString(); }@Override public Loader onCreateLoader(int id, Bundle args) { SpecialLoader loader = new SpecialLoader(MainActivity.this); return loader; }@Override public void onLoadFinished(Loader loader, Object data) { adapter.swapCursor((Cursor) data); }@Override public void onLoaderReset(Loader loader) {} }

最后我们看下运行的效果:
Android 异步加载神器Loader全解析

文章图片

  好,那到这里 又有人要说了,你这个说来说去 还不是只能支持provider或者db类型的数据源吗?好 接着往下,
我们给出另外一个例子,不过这个例子是谷歌官方的例子,我就取其中重要的部分给予注释讲解。
http://developer.android.com/intl/zh-cn/reference/android/content/AsyncTaskLoader.html
首先说一下 这个例子是干嘛的,他主要是监听手机里app list的变化,比如你删除了一个应用
安装了一个应用,马上就能捕捉到你的手机里app list的变化 并显示在界面,大家都知道 监听app list
是通过监听系统广播来完成的。 我就主要讲一下 这个官方demo里 是如何在监听到系统广播以后和loader结合起来
然后自动回调方法的。
/** * Helper class to look for interesting changes to the installed apps * so that the loader can be updated. */ public static class PackageIntentReceiver extends BroadcastReceiver { final AppListLoader mLoader; //这个构造函数是很重要的 他接收的 就是自定义的loader public PackageIntentReceiver(AppListLoader loader) { mLoader = loader; IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED); filter.addAction(Intent.ACTION_PACKAGE_REMOVED); filter.addAction(Intent.ACTION_PACKAGE_CHANGED); filter.addDataScheme("package"); mLoader.getContext().registerReceiver(this, filter); // Register for events related to sdcard installation. IntentFilter sdFilter = new IntentFilter(); sdFilter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE); sdFilter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE); //在这个地方 直接用loader来注册这个广播接收器 mLoader.getContext().registerReceiver(this, sdFilter); }//在收到广播以后 什么事情都没有做,而是调用了loader的onContentChanged方法 @Override public void onReceive(Context context, Intent intent) { // Tell the loader about the change. mLoader.onContentChanged(); } }

你看这里的25-26行 调用了 loader的onContentChanged方法。继续看下面的loader、
/** * A custom Loader that loads all of the installed applications. */ public static class AppListLoader extends AsyncTaskLoader< List< AppEntry> > { final InterestingConfigChanges mLastConfig = new InterestingConfigChanges(); final PackageManager mPm; List< AppEntry> mApps; PackageIntentReceiver mPackageObserver; public AppListLoader(Context context) { super(context); // Retrieve the package manager for later use; note we don\'t // use \'context\' directly but instead the save global application // context returned by getContext(). mPm = getContext().getPackageManager(); }//实际上最重要的就是这个方法了,每当这个回调方法被调用的时候 就去取applist 然后将结果返回到 //onLoadFinished 这个回调方法里面! @Override public List< AppEntry> loadInBackground() { // Retrieve all known applications. List< ApplicationInfo> apps = mPm.getInstalledApplications( PackageManager.GET_UNINSTALLED_PACKAGES | PackageManager.GET_DISABLED_COMPONENTS); if (apps == null) { apps = new ArrayList< ApplicationInfo> (); }final Context context = getContext(); // Create corresponding array of entries and load their labels. List< AppEntry> entries = new ArrayList< AppEntry> (apps.size()); for (int i=0; i< apps.size(); i++) { AppEntry entry = new AppEntry(this, apps.get(i)); entry.loadLabel(context); entries.add(entry); }// Sort the list. Collections.sort(entries, ALPHA_COMPARATOR); // Done! return entries; }/** * Called when there is new data to deliver to the client.The * super class will take care of delivering it; the implementation * here just adds a little more logic. */ @Override public void deliverResult(List< AppEntry> apps) { if (isReset()) { // An async query came in while the loader is stopped.We // don\'t need the result. if (apps != null) { onReleaseResources(apps); } } List< AppEntry> oldApps = mApps; mApps = apps; if (isStarted()) { // If the Loader is currently started, we can immediately // deliver its results. super.deliverResult(apps); }// At this point we can release the resources associated with // \'oldApps\' if needed; now that the new result is delivered we // know that it is no longer in use. if (oldApps != null) { onReleaseResources(oldApps); } }/** * Handles a request to start the Loader. */ @Override protected void onStartLoading() { if (mApps != null) { // If we currently have a result available, deliver it // immediately. deliverResult(mApps); }// Start watching for changes in the app data. if (mPackageObserver == null) { mPackageObserver = new PackageIntentReceiver(this); }// Has something interesting in the configuration changed since we // last built the app list? boolean configChange = mLastConfig.applyNewConfig(getContext().getResources()); if (takeContentChanged() || mApps == null || configChange) { // If the data has changed since the last time it was loaded // or is not currently available, start a load. forceLoad(); } }/** * Handles a request to stop the Loader. */ @Override protected void onStopLoading() { // Attempt to cancel the current load task if possible. cancelLoad(); }/** * Handles a request to cancel a load. */ @Override public void onCanceled(List< AppEntry> apps) { super.onCanceled(apps); // At this point we can release the resources associated with \'apps\' // if needed. onReleaseResources(apps); }/** * Handles a request to completely reset the Loader. */ @Override protected void onReset() { super.onReset(); // Ensure the loader is stopped onStopLoading(); // At this point we can release the resources associated with \'apps\' // if needed. if (mApps != null) { onReleaseResources(mApps); mApps = null; }// Stop monitoring for changes. if (mPackageObserver != null) { getContext().unregisterReceiver(mPackageObserver); mPackageObserver = null; } }/** * Helper function to take care of releasing resources associated * with an actively loaded data set. */ protected void onReleaseResources(List< AppEntry> apps) { // For a simple List< > there is nothing to do.For something // like a Cursor, we would close it here. } }

好,到这里流程就很明显了,在loader里 注册广播接收器,当广播接收器 收到广播以后 就调用loader的onContentChanged方法,
这个方法一调用 AppListLoader里的loadInBackGround就会被调用,然后当loadInBackGround执行完毕以后 就会把结果
传递给onLoadFinished方法了。 搞清楚这个流程 你就真正学会了使用loader这个大杀器了。当然了,我们并不满足于此,loader
还有一个特性就是可以自动管理他自己的生命周期 等等。我们现在就去看看他的源码,是如何完成这一点的。 并且上述几个方法
之间是如何相互调用的,顺序如何。
首先 我们要搞清楚几个类之间的关系:
public class CursorLoader extends AsyncTaskLoader< Cursor> {public abstract class AsyncTaskLoader< D> extends Loader< D> {public class Loader< D> {

这样就很清晰。首先由一个实体类作为最基础的基类,Loader 注意他可以接受一个泛型为参数,然后有一个抽象类:AsyncTaskLoader 也是泛型作为参数。
最后实际调用运作的类就是CursorLoader类了,这里就可以看出来 传进去的泛型是一个Cursor。你在自定义Loader的时候,这个泛型参数 当然是可以自己决定的,
比如官方demo里 传的就是一个List。
搞清楚 他们三者之间的关系,剩下的就简单多了。可以逐步分析了。
在前面的3个demo里,我们分别演示了在fragment和activity里 调用loader的方法。 那我们就看看 这两者之间有什么异同点。先来看fragment。
fragment里 我们是这样调用的:
//这个地方初始化了我们的loader getLoaderManager().initLoader(0, null, this);

直接get了一个manager 然后init他。我们进去看fragment的源码:
//这边就能看出来一个fragment只能有一个loadermanager了。 public LoaderManager getLoaderManager() {if (mLoaderManager != null) { return mLoaderManager; } //mHost很好理解 就是fragment的宿主,也就是跟fragment 相关联的activity。 if (mHost == null) { throw new IllegalStateException("Fragment " + this + " not attached to Activity"); } mCheckedForLoaderManager = true; mLoaderManager = mHost.getLoaderManager(mWho, mLoadersStarted, true); return mLoaderManager; }

既然 我们知道 fragment的getLoaderManager也是通过activity的getLoader去调用的,那我们就去activity里的源码看看 :
//在activty中最终实际上调用的就是他了 是这个方法 LoaderManagerImpl getLoaderManagerImpl() { if (mLoaderManager != null) { return mLoaderManager; } mCheckedForLoaderManager = true; mLoaderManager = getLoaderManager("(root)", mLoadersStarted, true /*create*/); return mLoaderManager; }//这个地方就能看到 主要的第一个参数 who,你到这就能发现 如果是activity自己调用的话,传进去的who的值就是root //也就是说一个actvity 只能有一个loadermanger 但是我们可以发现在fragment里 传进去的值是下面这个: // Internal unique name for this fragment; //String mWho; //也就是说每一个fragment的mWho的值都是唯一的,而在activty中,是维护了一个map,一个key 对应一个loadermanager //key就是fragment的那个唯一的标示,或者是activity自己,activity自己的标示就是(root)了 LoaderManagerImpl getLoaderManager(String who, boolean started, boolean create) { if (mAllLoaderManagers == null) { mAllLoaderManagers = new ArrayMap< String, LoaderManager> (); } LoaderManagerImpl lm = (LoaderManagerImpl) mAllLoaderManagers.get(who); if (lm == null) { if (create) { lm = new LoaderManagerImpl(who, this, started); mAllLoaderManagers.put(who, lm); } } else { lm.updateHostController(this); } return lm; }

好 一直到这里 ,我们就可以下一个结论了,真正的loadermanager都是存储在activity中的,包括fragment的loadermanager也是,通过一个map来保证 get的时候
取的manager是自己对应的,并且全局唯一。继续往下看:
public abstract class LoaderManager { /** * Callback interface for a client to interact with the manager. */ public interface LoaderCallbacks< D> { /** * Instantiate and return a new Loader for the given ID. * * @param id The ID whose loader is to be created. * @param args Any arguments supplied by the caller. * @return Return a new Loader instance that is ready to start loading. */ public Loader< D> onCreateLoader(int id, Bundle args); /** * Called when a previously created loader has finished its load.Note * that normally an application is < em> not< /em> allowed to commit fragment * transactions while in this call, since it can happen after an * activity\'s state is saved.See {@link FragmentManager#beginTransaction() * FragmentManager.openTransaction()} for further discussion on this. * * < p> This function is guaranteed to be called prior to the release of * the last data that was supplied for this Loader.At this point * you should remove all use of the old data (since it will be released * soon), but should not do your own release of the data since its Loader * owns it and will take care of that.The Loader will take care of * management of its data so you don\'t have to.In particular: * * < ul> * < li> < p> The Loader will monitor for changes to the data, and report * them to you through new calls here.You should not monitor the * data yourself.For example, if the data is a {@link android.database.Cursor} * and you place it in a {@link android.widget.CursorAdapter}, use * the {@link android.widget.CursorAdapter#CursorAdapter(android.content.Context, * android.database.Cursor, int)} constructor < em> without< /em> passing * in either {@link android.widget.CursorAdapter#FLAG_AUTO_REQUERY} * or {@link android.widget.CursorAdapter#FLAG_REGISTER_CONTENT_OBSERVER} * (that is, use 0 for the flags argument).This prevents the CursorAdapter * from doing its own observing of the Cursor, which is not needed since * when a change happens you will get a new Cursor throw another call * here. * < li> The Loader will release the data once it knows the application * is no longer using it.For example, if the data is * a {@link android.database.Cursor} from a {@link android.content.CursorLoader}, * you should not call close() on it yourself.If the Cursor is being placed in a * {@link android.widget.CursorAdapter}, you should use the * {@link android.widget.CursorAdapter#swapCursor(android.database.Cursor)} * method so that the old Cursor is not closed. * < /ul> * * @param loader The Loader that has finished. * @param data The data generated by the Loader. */ public void onLoadFinished(Loader< D> loader, D data); /** * Called when a previously created loader is being reset, and thus * making its data unavailable.The application should at this point * remove any references it has to the Loader\'s data. * * @param loader The Loader that is being reset. */ public void onLoaderReset(Loader< D> loader); }

一看就知道 loadermanger 其实是一个抽象类。就是定义了一些 我们需要的接口而已,这些接口方法的含义和用法 在那3个demo里 相信大家都有了解,不多说。
我们去看看这个抽象类的

    推荐阅读