最近因爲項目需要在研究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架構。