Android如何從資料庫中載入海量資料
在Android3.0之前,很多應用程式響應效能方面有缺陷,其中比較典型的錯誤行為是在UI執行緒中執行了查詢資料操作,尤其是一次性從database查出大量資料並載入到ListView裡,用這種方式載入資料是最差的選擇,硬體偏弱的手機會假死會兒。 其實體驗最好的還屬手機自帶通訊錄App這類應用,滑動絲般順滑。
在Android 3.0版本之前一般的做法是用Activity提供的startManagingCursor()和stopManagingCursor(),已經deprecated的API我們就不談了,3.0之後取而代之的是Loader,想必Loader的使用大家都有所知道:
public class CursorLoaderListFragment extends ListFragment { SimpleCursorAdapter mAdapter; SearchView mSearchView; String mCurFilter; static final String[] CONTACTS_SUMMARY_PROJECTION = new String[] { ContactsContract.Contacts._ID, ContactsContract.Contacts.DISPLAY_NAME, ContactsContract.Contacts.CONTACT_STATUS, }; private LoaderManager.LoaderCallbacks<Cursor> mLoaderCallback = new LoaderManager.LoaderCallbacks<Cursor>() { @Override public Loader<Cursor> onCreateLoader(int id, Bundle args) { // This is called when a new Loader needs to be created. String select = "((" + ContactsContract.Contacts.DISPLAY_NAME + " NOTNULL) AND (" + ContactsContract.Contacts.HAS_PHONE_NUMBER + "=1) AND (" + ContactsContract.Contacts.DISPLAY_NAME + " != '' ))"; return new CursorLoader(getActivity(), ContactsContract.Contacts.CONTENT_URI, CONTACTS_SUMMARY_PROJECTION, select, null, ContactsContract.Contacts.DISPLAY_NAME + " COLLATE LOCALIZED ASC"); } @Override public void onLoadFinished(@NonNull 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); } } @Override public void onLoaderReset(@NonNull 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); } }; @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); setEmptyText("No phone numbers"); setHasOptionsMenu(true); mAdapter = new SimpleCursorAdapter(getActivity(), 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); setListAdapter(mAdapter); LoaderManager.getInstance(this).initLoader(0, null, mLoaderCallback); } public boolean onQueryTextChange(String newText) { String newFilter = !TextUtils.isEmpty(newText) ? newText : null; if (mCurFilter == null && newFilter == null) { return true; } if (mCurFilter != null && mCurFilter.equals(newFilter)) { return true; } mCurFilter = newFilter; LoaderManager.getInstance(this).restartLoader(0, null, mLoaderCallback); return true; } }
不難看出只要實現三個回撥函式就能創建出一個LoaderCallbacks
,並將此丟給LoaderManager
去initLoader或者restartLoader,initLoader是第一次查詢使用的,restartLoader是二次查詢使用的。簡單是簡單不過有個東西不知有沒有發現:在此demo 中onCreateLoader()方法返回值是CursorLoader
物件,它的建構函式必須是Uri,意味著必須是基於Content Provider
實現的資料庫才可以使用,可是現實專案需要ContentProvider
的不多吧,很多是純sqlite的,為了此場景硬生生將sqlite的實現改成ContentProvider
得不償失。
當翻看onCreateLoader()方法定義時候,發現返回值不是CursorLoader
而是Loader
,CursorLoader
只是Loader
的一個子類而已,因此轉機來了:定義一個類繼承Loader
並內部用Cursor實現,最終返回自定義類的物件給onCreateLoader():
public abstract class SQLiteCursorLoader extends AsyncTaskLoader<Cursor> { private Cursor lastCursor; private Cursor queryCursor; public SQLiteCursorLoader(Context context, Cursor cursor) { super(context); queryCursor = cursor; } /** * Runs on a worker thread, loading in our data. Delegates the real work to concrete subclass' * buildCursor() method. */ @Override public Cursor loadInBackground() { final Cursor cursor = queryCursor; if (cursor != null) { // Ensure the cursor window is filled cursor.getCount(); } return cursor; } /** * Runs on the UI thread, routing the results from the background thread to whatever is using * the Cursor (e.g., a CursorAdapter). */ @Override public void deliverResult(final Cursor cursor) { if (isReset()) { // An async query came in while the loader is stopped if (cursor != null) { cursor.close(); } return; } final Cursor oldCursor = lastCursor; lastCursor = cursor; if (isStarted()) { super.deliverResult(cursor); } if (oldCursor != null && oldCursor != cursor && !oldCursor.isClosed()) { oldCursor.close(); } } /** * Starts an asynchronous load of the list data. When the result is ready the callbacks will be * called on the UI thread. If a previous load has been completed and is still valid the result * may be passed to the callbacks immediately. Must be called from the UI thread. */ @Override protected void onStartLoading() { if (lastCursor != null) { deliverResult(lastCursor); } if (takeContentChanged() || lastCursor == null) { forceLoad(); } } /** * Must be called from the UI thread, triggered by a call to stopLoading(). */ @Override protected void onStopLoading() { // Attempt to cancel the current load task if possible. cancelLoad(); } /** * Must be called from the UI thread, triggered by a call to cancel(). Here, we make sure our * Cursor is closed, if it still exists and is not already closed. */ @Override public void onCanceled(final Cursor cursor) { if (cursor != null && !cursor.isClosed()) { cursor.close(); } } /** * Must be called from the UI thread, triggered by a call to reset(). Here, we make sure our * Cursor is closed, if it still exists and is not already closed. */ @Override protected void onReset() { super.onReset(); // Ensure the loader is stopped onStopLoading(); if (lastCursor != null && !lastCursor.isClosed()) { lastCursor.close(); } lastCursor = null; } }
事實上並沒有直接繼承Loader, 而是繼承的AsyncTaskLoader
,從名字看就知道它是類似AsyncTask的原理實現的,SDK的CursorLoader
也是基於AsyncTaskLoader
實現的,當有了SQLiteCursorLoader
我們就可以用它建立LoaderManager.LoaderCallbacks<Cursor>
了:
private LoaderManager.LoaderCallbacks<Cursor> mLoaderCallback = new LoaderManager.LoaderCallbacks<Cursor>() { @Override public Loader<Cursor> onCreateLoader(int id, Bundle args) { // This is called when a new Loader needs to be created. String sql = "SELECT * FROM TABLE_XX"; return new SQLiteCursorLoader(getActivity(), sql); } @Override public void onLoadFinished(@NonNull Loader<Cursor> loader, Cursor data) { // ... } @Override public void onLoaderReset(@NonNull Loader<Cursor> loader) { // ... } };
關於Android資料庫方面的開發用純SQL的確有點累,可以考慮ORM的思路,以前我也寫了一個輕量級的,也一直使用中,SQLiteCursorLoader其實我提供了2個構造方法,一個如上傳Cursor,另外一個是傳BuilderSupport
, 它有2個實現,分別為ConditionBuilder
和MultiTableConditionBuilder
, 通過他們可以以面向物件方式來查詢資料庫,一個是用於單表查詢,一個用於多表查詢,具體可以參考下:Light-DAO