最近因为项目需要在研究mms短信代码,本来以为现在短信已经是一个很鸡肋的功能,没有什么价值了。但在看代码过程中,却在技术上收获不少,尤其是关于处理N多后台任务的架构设计很不错,因为mms大致上可以看成是一个IM应用,所以这些优秀设计都可以应用到代码中。下面会从以下几个方面分析:处理数据加载的LoadManager框架、处理后台任务的Action+IntentService框架、还有其它一些细小的方面。
我们首先来看LoadManager框架。
LoaderManager是什么?
正常情况下,一个Activity界面的启动是一个加载数据、展示数据的过程,比如微信的主界面。我们一般会封装一个DataBaseUtils工具类加载数据库cursor,再将cursor数据写到UI上。但是这一方面可能造成应性能方面有缺陷,比如UI切换之间的小故障、activity切换延迟、ANR问题(在UI线程执行query),另一方面由于cursor的生命周期管理不当,造成内存泄漏,同时在某些情况下由于没有保存数据能会重复查询导致效率浪费。
所以为了统一解决Activity/Fragment加载数据问题,android从3.0开始提出了LoaderManager框架,很好的解决了这个问题,尤其以Cursor查询为代表(CursorLoader)。
LoaderManager的使用
启动一个Loader
LoaderManager mLoaderManager = getLoaderManager();
mLoaderManager.initLoader(CONVERSATION_LIST_LOADER, mArgs, this);
第一个参数是id,是该Loader任务的key
第二个参数是Bundle,用于传递一些数据给Loader
第三个参数是LoaderCallbacks,需要我们实现一个实例供LoaderManager调用,其中有三个方法如下。
public interface LoaderCallbacks<D> {
public Loader<D> onCreateLoader(int id, Bundle args);
public void onLoadFinished(Loader<D> loader, D data);
public void onLoaderReset(Loader<D> loader);
}
onCreateLoader是返回一个具体处理后台任务的Loader给LoaderManager框架, 这里我们以系统帮我们实现的CursorLoader为例来说明
public class CursorLoader extends AsyncTaskLoader<Cursor> {
@Override
public Cursor loadInBackground() {
....................................
try {
Cursor cursor = getContext().getContentResolver().query(mUri, mProjection, mSelection,
mSelectionArgs, mSortOrder, mCancellationSignal);
if (cursor != null) {
try {
// Ensure the cursor window is filled.
cursor.getCount();
cursor.registerContentObserver(mObserver);
} catch (RuntimeException ex) {
cursor.close();
throw ex;
}
}
return cursor;
} finally {
synchronized (this) {
mCancellationSignal = null;
}
}
}
/* Runs on the UI thread */
@Override
public void deliverResult(Cursor cursor) {
....................................
Cursor oldCursor = mCursor;
mCursor = cursor;
if (isStarted()) {
super.deliverResult(cursor);
}
....................................
}
}
我们可以看到CursorLoader继承于AsyncTaskLoader,AsyncTaskLoader见名知意里面通过AsyncTask来开一个后台线程处理后台任务。而AsyncTask最终在doInBackground中调用CursorLoader的loadInBackground方法处理具体加载数据的逻辑。最终deliverResult将结果返回给我们,即将结果回调给LoaderCallbacks的onLoadFinished方法,我们在onLoadFinished这个方法里面加上我们的逻辑,将data展示到UI上。
当然这里可能有人会问,我们是不是应该对数据库做一个监听发现如果变化的话再次启动一个Loader去update数据。其实这是完全不必的,因为监听的工作Loader也帮我们做了,仔细看一下CursorLoader的loadInBackground方法,会发现在加载完数据之后有这样cursor.registerContentObserver(mObserver);的一个逻辑,其实就是对数据库的监听,当然一旦发现数据变化,Loader内部就会再次start,当加载完数据之后会再次回调给LoaderCallbacks的onLoadFinished方法。
LoaderManager原理(该部分可以略过)
这里说一下LoaderManager的实现原理。每个Activity/Fragment都有一个LoaderManager用于维护与其相关的所有LoaderInfo任务的执行销毁等,所有的LoaderInfo存在SparseArray<LoaderInfo> mLoaders里面,key是id。当initLoader的时候,首先查看LoaderInfo是否已经存在,如果存在就直接拿数据,否则就启动该Loader.,逻辑如下:
public <D> Loader<D> initLoader(int id, Bundle args, LoaderManager.LoaderCallbacks<D> callback) {
if (mCreatingLoader) {
throw new IllegalStateException("Called while creating a loader");
}
LoaderInfo info = mLoaders.get(id);
if (DEBUG) Log.v(TAG, "initLoader in " + this + ": args=" + args);
if (info == null) {
// Loader doesn't already exist; create.
info = createAndInstallLoader(id, args, (LoaderManager.LoaderCallbacks<Object>)callback);
if (DEBUG) Log.v(TAG, " Created new loader " + info);
} else {
if (DEBUG) Log.v(TAG, " Re-using existing loader " + info);
info.mCallbacks = (LoaderManager.LoaderCallbacks<Object>)callback;
}
if (info.mHaveData && mStarted) {
// If the loader has already generated its data, report it now.
info.callOnLoadFinished(info.mLoader, info.mData);
}
return (Loader<D>)info.mLoader;
}
这里面还要说一下,在我们第一次Loader加载完数据之后,Activity在每次onStart的时候会再次启动Loader,在onStop的时候会停止Loader. 这说明,Loader的生命周期完全被Activity/Fragment管控,不用我们再操心(哼哼哈嘿。。)。
Loader的自定义
上面通过android默认CursorLoader的例子说明了Loader的使用,那我们有没有在其它情况下可以自己实现一个Loader呢?答案当然是肯定的,一般情况下我们只需要写一个类继承AsyncTaskLoader,实现loadInBackground、deliverResult、onStartLoading、onStopLoading、onReset等方法即可,不算很难。下面贴一个网上的简单例子,具体是实现了加载应用列表的功能
package com.adp.loadercustom.loader;
import java.text.Collator;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.support.v4.content.AsyncTaskLoader;
import android.util.Log;
import com.adp.loadercustom.observer.InstalledAppsObserver;
import com.adp.loadercustom.observer.SystemLocaleObserver;
/**
* An implementation of AsyncTaskLoader which loads a {@code List<AppEntry>}
* containing all installed applications on the device.
*/
public class AppListLoader extends AsyncTaskLoader<List<AppEntry>> {
private static final String TAG = "ADP_AppListLoader";
private static final boolean DEBUG = true;
final PackageManager mPm;
// We hold a reference to the Loader's data here.
private List<AppEntry> mApps;
public AppListLoader(Context ctx) {
// Loaders may be used across multiple Activitys (assuming they aren't
// bound to the LoaderManager), so NEVER hold a reference to the context
// directly. Doing so will cause you to leak an entire Activity's context.
// The superclass constructor will store a reference to the Application
// Context instead, and can be retrieved with a call to getContext().
super(ctx);
mPm = getContext().getPackageManager();
}
/****************************************************/
/** (1) A task that performs the asynchronous load **/
/****************************************************/
/**
* This method is called on a background thread and generates a List of
* {@link AppEntry} objects. Each entry corresponds to a single installed
* application on the device.
*/
@Override
public List<AppEntry> loadInBackground() {
if (DEBUG) Log.i(TAG, "+++ loadInBackground() called! +++");
// Retrieve all installed applications.
List<ApplicationInfo> apps = mPm.getInstalledApplications(0);
if (apps == null) {
apps = new ArrayList<ApplicationInfo>();
}
// 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(getContext());
entries.add(entry);
}
// Sort the list.
Collections.sort(entries, ALPHA_COMPARATOR);
return entries;
}
/*******************************************/
/** (2) Deliver the results to the client **/
/*******************************************/
/**
* Called when there is new data to deliver to the client. The superclass will
* deliver it to the registered listener (i.e. the LoaderManager), which will
* forward the results to the client through a call to onLoadFinished.
*/
@Override
public void deliverResult(List<AppEntry> apps) {
if (isReset()) {
if (DEBUG) Log.w(TAG, "+++ Warning! An async query came in while the Loader was reset! +++");
// The Loader has been reset; ignore the result and invalidate the data.
// This can happen when the Loader is reset while an asynchronous query
// is working in the background. That is, when the background thread
// finishes its work and attempts to deliver the results to the client,
// it will see here that the Loader has been reset and discard any
// resources associated with the new data as necessary.
if (apps != null) {
releaseResources(apps);
return;
}
}
// Hold a reference to the old data so it doesn't get garbage collected.
// We must protect it until the new data has been delivered.
List<AppEntry> oldApps = mApps;
mApps = apps;
if (isStarted()) {
if (DEBUG) Log.i(TAG, "+++ Delivering results to the LoaderManager for" +
" the ListFragment to display! +++");
// If the Loader is in a started state, have the superclass deliver the
// results to the client.
super.deliverResult(apps);
}
// Invalidate the old data as we don't need it any more.
if (oldApps != null && oldApps != apps) {
if (DEBUG) Log.i(TAG, "+++ Releasing any old data associated with this Loader. +++");
releaseResources(oldApps);
}
}
/*********************************************************/
/** (3) Implement the Loader锟絪 state-dependent behavior **/
/*********************************************************/
@Override
protected void onStartLoading() {
if (DEBUG) Log.i(TAG, "+++ onStartLoading() called! +++");
if (mApps != null) {
// Deliver any previously loaded data immediately.
if (DEBUG) Log.i(TAG, "+++ Delivering previously loaded data to the client...");
deliverResult(mApps);
}
// Register the observers that will notify the Loader when changes are made.
if (mAppsObserver == null) {
mAppsObserver = new InstalledAppsObserver(this);
}
if (mLocaleObserver == null) {
mLocaleObserver = new SystemLocaleObserver(this);
}
if (takeContentChanged()) {
// When the observer detects a new installed application, it will call
// onContentChanged() on the Loader, which will cause the next call to
// takeContentChanged() to return true. If this is ever the case (or if
// the current data is null), we force a new load.
if (DEBUG) Log.i(TAG, "+++ A content change has been detected... so force load! +++");
forceLoad();
} else if (mApps == null) {
// If the current data is null... then we should make it non-null! :)
if (DEBUG) Log.i(TAG, "+++ The current data is data is null... so force load! +++");
forceLoad();
}
}
@Override
protected void onStopLoading() {
if (DEBUG) Log.i(TAG, "+++ onStopLoading() called! +++");
// The Loader has been put in a stopped state, so we should attempt to
// cancel the current load (if there is one).
cancelLoad();
// Note that we leave the observer as is; Loaders in a stopped state
// should still monitor the data source for changes so that the Loader
// will know to force a new load if it is ever started again.
}
@Override
protected void onReset() {
if (DEBUG) Log.i(TAG, "+++ onReset() called! +++");
// Ensure the loader is stopped.
onStopLoading();
// At this point we can release the resources associated with 'apps'.
if (mApps != null) {
releaseResources(mApps);
mApps = null;
}
// The Loader is being reset, so we should stop monitoring for changes.
if (mAppsObserver != null) {
getContext().unregisterReceiver(mAppsObserver);
mAppsObserver = null;
}
if (mLocaleObserver != null) {
getContext().unregisterReceiver(mLocaleObserver);
mLocaleObserver = null;
}
}
@Override
public void onCanceled(List<AppEntry> apps) {
if (DEBUG) Log.i(TAG, "+++ onCanceled() called! +++");
// Attempt to cancel the current asynchronous load.
super.onCanceled(apps);
// The load has been canceled, so we should release the resources
// associated with 'mApps'.
releaseResources(apps);
}
@Override
public void forceLoad() {
if (DEBUG) Log.i(TAG, "+++ forceLoad() called! +++");
super.forceLoad();
}
/**
* Helper method to take care of releasing resources associated with an
* actively loaded data set.
*/
private void releaseResources(List<AppEntry> apps) {
// For a simple List, there is nothing to do. For something like a Cursor,
// we would close it in this method. All resources associated with the
// Loader should be released here.
}
/*********************************************************************/
/** (4) Observer which receives notifications when the data changes **/
/*********************************************************************/
// An observer to notify the Loader when new apps are installed/updated.
private InstalledAppsObserver mAppsObserver;
// The observer to notify the Loader when the system Locale has been changed.
private SystemLocaleObserver mLocaleObserver;
/**************************/
/** (5) Everything else! **/
/**************************/
/**
* Performs alphabetical comparison of {@link AppEntry} objects. This is
* used to sort queried data in {@link loadInBackground}.
*/
private static final Comparator<AppEntry> ALPHA_COMPARATOR = new Comparator<AppEntry>() {
Collator sCollator = Collator.getInstance();
@Override
public int compare(AppEntry object1, AppEntry object2) {
return sCollator.compare(object1.getLabel(), object2.getLabel());
}
};
}
我们注意一下onStartLoading方法,发现有
mAppsObserver = new InstalledAppsObserver(this);
这样的逻辑,说明这里是监听数据变化的地方,这是与CursorLoader不同的地方。类似的我们可以实现自己的Loader.
好了,这是mms应用中关于LoaderManager的总结,下一篇我们看一下处理n多后台任务的Action+IntentService架构。