在Android 3.0中提供了一个新概念Loaders,通过LoaderManager类可以很轻松的异步加载数据从Fragment或Activity中,Loaders提供了回调机制通知最终的运行结果,有点类似AsyncTask类,但由于Loader对于并发可以用过Loader管理器统一管理,所以更适合批量处理多个异步任务的处理(当然内部仍然是多线程)。下面和大家看下honeycomb中的新特性吧,对于解决多重异步I/O加快Android平板应用的运行是十分有效的。
一、LoaderManager
LoaderManager类位于android.app.LoaderManager,提供了以下几个方法
abstract void destroyLoader(int id) //停止并移除loader通过ID abstract void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) //打印LoaderManager的状态到一个流中 static void enableDebugLogging(boolean enabled) //启用debug记录 abstract <D> Loader<D> getLoader(int id) //返回找到的ID或没有匹配的在Loader中 abstract <D> Loader<D> initLoader(int id, Bundle args, LoaderCallbacks<D> callback) //初始化Loader使其成为活动状态 abstract <D> Loader<D> restartLoader(int id, Bundle args, LoaderCallbacks<D> callback) //启动一个新的或重启一个存在的Loader在管理器中
同时LoaderManager还有一个回调接口android.app.LoaderManager.LoaderCallbacks<D> 用于和LoaderManager交互
abstract Loader<D> onCreateLoader(int id, Bundle args) //举例并返回一个新Loader通过ID abstract void onLoadFinished(Loader<D> loader, D data) //当前面一个Loader已经完成时回调 abstract void onLoaderReset(Loader<D> loader) //当一个新的loader或存在的loader重启时回调
二、Loader
Loader类位于android.content.Loader<D>,整体比较复杂,主要成员有
1. 构造方法 Loader(Context context) //作为唯一实例化方法参数只有一个Context
2. Public Methods
void abandon() //高速Loader他在绑定 String dataToString(D data) //用于调试,转换一个Loader数据类的实例为字符串用于打印 void deliverResult(D data) //发送一个load注册的listener结果 void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) //打印loader状态通过给定的流 void forceLoad() //强制一个异步载入 Context getContext() //返回Context实例 int getId() boolean isAbandoned() //判断是否已经绑定 boolean isReset() //判断是否已经重启 boolean isStarted() //判断是否已经执行 void onContentChanged() //内容变化回调 registerListener(int id, OnLoadCompleteListener<D> listener) void reset() //重置一个Loader的状态 final void startLoading() //启动一个异步的载入从Loader的数据 void stopLoading() //停止载入 boolean takeContentChanged() String toString() void unregisterListener(OnLoadCompleteListener<D> listener)
提供的子类 android.content.Loader.ForceLoadContentObserver 和 接口 android.content.Loader.OnLoadCompleteListener<D>
为了更清晰的表达,给出一个SDK例子完整代码,来作分析
public class LoaderThrottle extends Activity { static final String TAG = "LoaderThrottle"; public static final String AUTHORITY = "com.example.android.apis.app.LoaderThrottle"; public static final class MainTable implements BaseColumns { // This class cannot be instantiated private MainTable() {} public static final String TABLE_NAME = "main"; public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/main"); public static final Uri CONTENT_ID_URI_BASE = Uri.parse("content://" + AUTHORITY + "/main/"); public static final String CONTENT_TYPE = "vnd.android.cursor.dir/vnd.example.api-demos-throttle"; public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/vnd.example.api-demos-throttle"; public static final String DEFAULT_SORT_ORDER = "data COLLATE LOCALIZED ASC"; public static final String COLUMN_NAME_DATA = "data"; } static class DatabaseHelper extends SQLiteOpenHelper { private static final String DATABASE_NAME = "loader_throttle.db"; private static final int DATABASE_VERSION = 2; DatabaseHelper(Context context) { // calls the super constructor, requesting the default cursor factory. super(context, DATABASE_NAME, null, DATABASE_VERSION); } @Override public void onCreate(SQLiteDatabase db) { db.execSQL("CREATE TABLE " + MainTable.TABLE_NAME + " (" + MainTable._ID + " INTEGER PRIMARY KEY," + MainTable.COLUMN_NAME_DATA + " TEXT" + ");"); } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { // Logs that the database is being upgraded Log.w(TAG, "Upgrading database from version " + oldVersion + " to " + newVersion + ", which will destroy all old data"); // Kills the table and existing data db.execSQL("DROP TABLE IF EXISTS notes"); // Recreates the database with a new version onCreate(db); } } public static class SimpleProvider extends ContentProvider { // A projection map used to select columns from the database private final HashMap<String, String> mNotesProjectionMap; // Uri matcher to decode incoming URIs. private final UriMatcher mUriMatcher; // The incoming URI matches the main table URI pattern private static final int MAIN = 1; // The incoming URI matches the main table row ID URI pattern private static final int MAIN_ID = 2; // Handle to a new DatabaseHelper. private DatabaseHelper mOpenHelper; public SimpleProvider() { // Create and initialize URI matcher. mUriMatcher = new UriMatcher(UriMatcher.NO_MATCH); mUriMatcher.addURI(AUTHORITY, MainTable.TABLE_NAME, MAIN); mUriMatcher.addURI(AUTHORITY, MainTable.TABLE_NAME + "/#", MAIN_ID); // Create and initialize projection map for all columns. This is // simply an identity mapping. mNotesProjectionMap = new HashMap<String, String>(); mNotesProjectionMap.put(MainTable._ID, MainTable._ID); mNotesProjectionMap.put(MainTable.COLUMN_NAME_DATA, MainTable.COLUMN_NAME_DATA); } @Override public boolean onCreate() { mOpenHelper = new DatabaseHelper(getContext()); // Assumes that any failures will be reported by a thrown exception. return true; } @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { // Constructs a new query builder and sets its table name SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); qb.setTables(MainTable.TABLE_NAME); switch (mUriMatcher.match(uri)) { case MAIN: // If the incoming URI is for main table. qb.setProjectionMap(mNotesProjectionMap); break; case MAIN_ID: // The incoming URI is for a single row. qb.setProjectionMap(mNotesProjectionMap); qb.appendWhere(MainTable._ID + "=?"); selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs, new String[] { uri.getLastPathSegment() }); break; default: throw new IllegalArgumentException("Unknown URI " + uri); } if (TextUtils.isEmpty(sortOrder)) { sortOrder = MainTable.DEFAULT_SORT_ORDER; } SQLiteDatabase db = mOpenHelper.getReadableDatabase(); Cursor c = qb.query(db, projection, selection, selectionArgs, null /* no group */, null /* no filter */, sortOrder); c.setNotificationUri(getContext().getContentResolver(), uri); return c; } @Override public String getType(Uri uri) { switch (mUriMatcher.match(uri)) { case MAIN: return MainTable.CONTENT_TYPE; case MAIN_ID: return MainTable.CONTENT_ITEM_TYPE; default: throw new IllegalArgumentException("Unknown URI " + uri); } } @Override public Uri insert(Uri uri, ContentValues initialValues) { if (mUriMatcher.match(uri) != MAIN) { // Can only insert into to main URI. throw new IllegalArgumentException("Unknown URI " + uri); } ContentValues values; if (initialValues != null) { values = new ContentValues(initialValues); } else { values = new ContentValues(); } if (values.containsKey(MainTable.COLUMN_NAME_DATA) == false) { values.put(MainTable.COLUMN_NAME_DATA, ""); } SQLiteDatabase db = mOpenHelper.getWritableDatabase(); long rowId = db.insert(MainTable.TABLE_NAME, null, values); // If the insert succeeded, the row ID exists. if (rowId > 0) { Uri noteUri = ContentUris.withAppendedId(MainTable.CONTENT_ID_URI_BASE, rowId); getContext().getContentResolver().notifyChange(noteUri, null); return noteUri; } throw new SQLException("Failed to insert row into " + uri); } @Override public int delete(Uri uri, String where, String[] whereArgs) { SQLiteDatabase db = mOpenHelper.getWritableDatabase(); String finalWhere; int count; switch (mUriMatcher.match(uri)) { case MAIN: count = db.delete(MainTable.TABLE_NAME, where, whereArgs); break; case MAIN_ID: finalWhere = DatabaseUtils.concatenateWhere( MainTable._ID + " = " + ContentUris.parseId(uri), where); count = db.delete(MainTable.TABLE_NAME, finalWhere, whereArgs); break; default: throw new IllegalArgumentException("Unknown URI " + uri); } getContext().getContentResolver().notifyChange(uri, null); return count; } @Override public int update(Uri uri, ContentValues values, String where, String[] whereArgs) { SQLiteDatabase db = mOpenHelper.getWritableDatabase(); int count; String finalWhere; switch (mUriMatcher.match(uri)) { case MAIN: // If URI is main table, update uses incoming where clause and args. count = db.update(MainTable.TABLE_NAME, values, where, whereArgs); break; case MAIN_ID: // If URI is for a particular row ID, update is based on incoming // data but modified to restrict to the given ID. finalWhere = DatabaseUtils.concatenateWhere( MainTable._ID + " = " + ContentUris.parseId(uri), where); count = db.update(MainTable.TABLE_NAME, values, finalWhere, whereArgs); break; default: throw new IllegalArgumentException("Unknown URI " + uri); } getContext().getContentResolver().notifyChange(uri, null); return count; } } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); FragmentManager fm = getFragmentManager(); if (fm.findFragmentById(android.R.id.content) == null) { ThrottledLoaderListFragment list = new ThrottledLoaderListFragment(); fm.beginTransaction().add(android.R.id.content, list).commit(); } } public static class ThrottledLoaderListFragment extends ListFragment implements LoaderManager.LoaderCallbacks<Cursor> { static final int POPULATE_ID = Menu.FIRST; static final int CLEAR_ID = Menu.FIRST+1; SimpleCursorAdapter mAdapter; String mCurFilter; AsyncTask<Void, Void, Void> mPopulatingTask; @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); setEmptyText("No data. Select 'Populate' to fill with data from Z to A at a rate of 4 per second."); setHasOptionsMenu(true); // Create an empty adapter we will use to display the loaded data. mAdapter = new SimpleCursorAdapter(getActivity(), android.R.layout.simple_list_item_1, null, new String[] { MainTable.COLUMN_NAME_DATA }, new int[] { android.R.id.text1 }, 0); setListAdapter(mAdapter); // Prepare the loader. Either re-connect with an existing one, // or start a new one. getLoaderManager().initLoader(0, null, this); } @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { menu.add(Menu.NONE, POPULATE_ID, 0, "Populate") .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM); menu.add(Menu.NONE, CLEAR_ID, 0, "Clear") .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM); } @Override public boolean onOptionsItemSelected(MenuItem item) { final ContentResolver cr = getActivity().getContentResolver(); switch (item.getItemId()) { case POPULATE_ID: if (mPopulatingTask != null) { mPopulatingTask.cancel(false); } mPopulatingTask = new AsyncTask<Void, Void, Void>() { @Override protected Void doInBackground(Void... params) { for (char c='Z'; c>='A'; c--) { if (isCancelled()) { break; } StringBuilder builder = new StringBuilder("Data "); builder.append(c); ContentValues values = new ContentValues(); values.put(MainTable.COLUMN_NAME_DATA, builder.toString()); cr.insert(MainTable.CONTENT_URI, values); // Wait a bit between each insert. try { Thread.sleep(250); } catch (InterruptedException e) { } } return null; } }; mPopulatingTask.executeOnExecutor( AsyncTask.THREAD_POOL_EXECUTOR, (Void[])null); return true; case CLEAR_ID: if (mPopulatingTask != null) { mPopulatingTask.cancel(false); mPopulatingTask = null; } AsyncTask<Void, Void, Void> task = new AsyncTask<Void, Void, Void>() { @Override protected Void doInBackground(Void... params) { cr.delete(MainTable.CONTENT_URI, null, null); return null; } }; task.execute((Void[])null); return true; default: return super.onOptionsItemSelected(item); } } @Override public void onListItemClick(ListView l, View v, int position, long id) { // Insert desired behavior here. Log.i(TAG, "Item clicked: " + id); } // These are the rows that we will retrieve. static final String[] PROJECTION = new String[] { MainTable._ID, MainTable.COLUMN_NAME_DATA, }; public Loader<Cursor> onCreateLoader(int id, Bundle args) { CursorLoader cl = new CursorLoader(getActivity(), MainTable.CONTENT_URI, PROJECTION, null, null, null); cl.setUpdateThrottle(2000); // update at most every 2 seconds. return cl; } public void onLoadFinished(Loader<Cursor> loader, Cursor data) { mAdapter.swapCursor(data); } public void onLoaderReset(Loader<Cursor> loader) { mAdapter.swapCursor(null); } } }