Loader是一個異步加載數據的類,它和AsyncTask有類似也有不同,今天我們就先來學習下它。由於是對比學習,所以我們先來復習下AsyncTask的使用和特點。
一、AsyncTask
參考自:http://www.open-open.com/lib/view/open1417955629527.html
package com.example.jacktony.myapplication03; import android.os.AsyncTask; /** * Created by Jack Tony on 2014/12/15. */ public class Task extends AsyncTask<String, Integer, Long>{ @Override protected void onPreExecute() { super.onPreExecute(); } @Override protected Long doInBackground(String... params) { return null; } @Override protected void onProgressUpdate(Integer... values) { super.onProgressUpdate(values); } @Override protected void onPostExecute(Long aLong) { super.onPostExecute(aLong); } @Override protected void onCancelled(Long aLong) { super.onCancelled(aLong); } }
1.1 它什么時候會被結束
關於AsyncTask存在一個這樣廣泛的誤解,很多人認為一個在Activity中的AsyncTask會隨着Activity的銷毀而銷毀。然后事實並非如此。AsyncTask會一直執行doInBackground()方法直到方法執行結束。一旦上述方法結束,會依據情況進行不同的操作。
- 如果cancel(boolean)被調用了,則執行onCancelled(Result)方法
- 如果cancel(boolean)沒有被調用,則執行onPostExecute(Result)方法
AsyncTask的cancel方法需要一個布爾值的參數,參數名為mayInterruptIfRunning,意思是如果正在執行是否可以打斷
, 如果這個值設置為true,表示這個任務可以被打斷,否則,正在執行的程序會繼續執行直到完成。如果在doInBackground()方法中有一個循環操作,我們應該在循環中使用isCancelled()來判斷,如果返回為true,我們應該避免執行后續無用的循環操作。
總之,我們使用AsyncTask需要確保AsyncTask正確地取消。
1.2 奇怪的cancel()
如果你調用了AsyncTask的cancel(false),doInBackground()仍然會執行到方法結束,只是不會去調用onPostExecute()方法。但是實際上這是讓應用程序執行了沒有意義的操作。那么是不是我們調用cancel(true)前面的問題就能解決呢?並非如此。如果mayInterruptIfRunning設置為true,會使任務盡早結束,但是如果的doInBackground()有不可打斷的方法,則它就會失效,比如這個BitmapFactory.decodeStream() IO操作。但是你可以提前關閉IO流並捕獲這樣操作拋出的異常。但是這樣會使得cancel()方法沒有任何意義。
1.3 內存泄露
還有一種常見的情況就是,在Activity中使用非靜態匿名內部AsyncTask類,由於Java內部類的特點,AsyncTask內部類會持有外部類的隱式引用。詳細請參考細話Java:”失效”的private修飾符,由於AsyncTask的生命周期可能比Activity的長,當Activity進行銷毀AsyncTask還在執行時,由於AsyncTask持有Activity的引用,導致Activity對象無法回收,進而產生內存泄露。所以很不建議用內部類來做異步處理。
1.4 結果丟失
另一個問題就是在屏幕旋轉等造成Activity重新創建時AsyncTask數據丟失的問題。當Activity銷毀並創新創建后,還在運行的 AsyncTask會持有一個Activity的非法引用即之前的Activity實例。導致onPostExecute()沒有任何作用。
1.5 串行 or 並行
- 在1.6之前,所有的AsyncTask在一個單獨的線程中有序的執行。
- 從1.6到2.3,這些AsyncTask在一個線程池中執行,但是有上限。
- 從3.0開始,又使用最早的方案!他們在一個單獨的線程中有序的執行,除非你調用executeOnExecutor,並且傳入一個ThreadPoolExecutor。
Android團隊從3.0開始認為,開發者可能並不喜歡讓AsyncTask並行,於是Android團隊又把AsyncTask改成了串行。當然這一次的修改並沒有完全禁止 AsyncTask並行。你可以通過設置executeOnExecutor(Executor)來實現多個AsyncTask並行。關於API文檔的描述如下
If we want to make sure we have control over the execution, whether it will run serially or parallel, we can check at runtime with this code to make sure it runs parallel:
public static void execute(AsyncTask as) { if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.HONEYCOMB_MR1) { as.execute(); } else { as.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } } //(This code does not work for API lvl 1 to 3)
1.6 真的需要AsyncTask么
並非如此,使用AsyncTask雖然可以以簡短的代碼實現異步操作,但是正如本文提到的,你需要讓AsyncTask正常工作的話,需要注意很多條條框框。推薦的一種進行異步操作的技術就是使用Loaders。下面就來談談這個從3.0引入的類,support包中也對低版本添加了支持。
二、Loader的特點
Loaders使得在一個Activity或fragment中異步加載數據變得容易。loaders有這些特性:
- 提供異步加載數據的功能。
- 監視它們的數據資源,並在這些資源發生變化時發送新的結果。
- 當配置信息發生改變后重新被創建時,它會重連到上一次裝載機的指示位置。而且,它們不需要重新查詢數據。
- 允許一個Activity或者Fragment連接到Activity或者Loader被重新創建之前的Loader,並且重新取出里面的result
- 如果result在Loader從Activity/Fragmentdisconnected之后才得到,那么它會被保存在cache中,並且在Activity/Fragemtneon被重新創建之后傳送回來。
- Loader可以監控他得數據源,在數據源數據變化后將新的數據deliver(傳遞)出來。
- Loader處理了result相關的資源的allocation/disallocation(比如Cursors)。
如果你想在一個Activity或者Fragment里面進行異步數據的加載,不要再使用AsyncTask了。也不要認為Loader只是用來做數據庫相關的事情(CursorLoaders),他可以做很多事情。
三、Loader的管理者——LoaderManager
Loader是由一個loaderManager來管理的,每一個Activity或Fragment都只有一個LoaderManager,但是一個LoaderManager可以包含多個加載器。
3.1 建立loaderManager
在activity或者fragment中用getSupportLoaderManager()或getLoaderManager()得到這個對象就行了。
getSupportLoaderManager()
getLoaderManager()
3.2 初始化loader的回調函數
getSupportLoaderManager().initLoader(0, null, new MyLoaderCallback());
通過initLoader就可以初始化一個loader了,這個初始化很不徹底,其實就是通過它能觸發回調方法中的onCreateLoader()方法,而onCreateLoader()方法會返回一個loader,所以這里的初始化並沒有真正new一個對象,而是調用了onCreateLoader()來初始化,所以你需要在回調函數中的onCreateLoader()進行處理。因為我們現在是學習loaderManager,還沒涉及到loader,所以在onCreatLoader中我們沒建立一個loader,返回一個null對象。這也側面證明了initloader()並沒有真正初始化一個真正的loader,真正的初始化是在onCreatLoader()中進行的。
private class MyLoaderCallback implements LoaderManager.LoaderCallbacks { final private String TAG = getClass().getSimpleName(); /** * @param id * @param args * @return Object returned from onCreateLoader must not be a non-static inner member class */ @Override public Loader onCreateLoader(int id, Bundle args) { Log.i(TAG, "onCreateLoader"); //return new MyAsyncLoader(getApplicationContext());
return null; } @Override public void onLoadFinished(Loader loader, Object data) { Log.i(TAG, "onLoadFinished"); } @Override public void onLoaderReset(Loader loader) { Log.i(TAG, "onLoaderReset"); } }
3.3 參數
- 標識加載器的唯一Id,在這個例子中Id是0。在Activity或者Fragment中創建一個私有的常量ID就可以了。
- 提供給加載器的可選參數(Bundle),在這個例子中是null。
- 一個LoaderManager.loaderCallBacks實現,這里是MyLoaderCallback()
1.第一個參數需要在當前Activity范圍內找一個唯一的id進行傳入,方便控制loader。
2.第二個參數可以傳入bundle,很多時候你不需要傳入任何參數,置null即可。
Bundle args = new Bundle(); args.putString("query",query); getLoaderManager().restartLoader(LOADER_ID,args, loaderCallbacks);
在Callback中得到這個bundle
@Override public Loader onCreateLoader(int id, Bundle args) { Log.i(TAG, "onCreateLoader"); return new MyAsyncLoader(getApplicationContext()); }
3.4 結果
initLoader()的調用確保加載器被初始化和激活。它有兩個可能的結果:
① 如果這個加載器指定的id已經存在,上一次被創建的加載器就被重用。
② 如果這個加載器指定的id不存在,initLoader()方法觸發LoaderManager.LoaderCallbacks
方法onCreateLoader()。這是你實現實例化和返回一個新的加載器代碼的地方。
無論在任何情況下,提供的LoaderManager.LoaderCallBacks實現都和加載器有關,而且將會在加載器狀態改變時被調用。如果調用調用者是在它的開始狀態,而且請求的加載器已經存在還產生了數據,接着系統就會直接調用onLoadFinished()方法(在initLoader()期間),所以你必須准備好這種情況的發生。也就是說,這個加載器已經被初始化並且啟動了,那么再initLoader的時候就會直接調用onLoadFinished()方法。(這中文翻譯的好蛋疼T_T)
3.5 初始化Loader后怎么要用它么?
initLoader()方法返回的是已經被創建的加載器(前提是你在callback中確實初始化了一個loader),但是你不需要獲取對它的引用。LoaderManager自動管理加載器的生命。Loadermanager在必要的時候啟動和停止加載,而且保持加載器的狀態和它關聯的內容。這意味着,你很少直接與加載器交互。當一個特別的事件發生時你通常使用LoaderManager.LoaderCallbacks的方法進行干預。總之,你現在進行的僅僅是一個初始化的動作,而不需要對這個初始化的loader做任何處理。
3.6 復用loader和重新初始化loader
當你像上面展示的那樣使用initLoader()時,如果對於指定的id的加載器已經存在了一個那將使用時這個存在的。如果沒有,將創建一個。但是有時你希望拋棄原來的老數據重新開始。
為了清除你的老數據,你需要使用restartLoader()方法。
getLoaderManager().restartLoader(0, null, this);
3.7 LoaderManager的一個Bug
當一個Fragment因為configuration變化被重新創建的時候(比如旋轉屏幕,改變語言等),你在onActivityCreated()里面調用了initLoader()之后,他的LoaderManager調用了兩次onLoadFinished。
解決方案:
1. 如果代碼邏輯允許,可以不用處理。
2. 將之前的result保存起來,檢查結果是否有變化。
3. 在onCreate()里面調用setRetainInstance(true)。
3.8 一次性的loader
有時候我們僅僅希望laoder啟動一次,比如:我們點擊一個按鈕然后提交一些信息到服務器。在用戶點擊了一個按鈕后,我們調用initLoader,這個時候用戶旋轉屏幕,在旋轉完屏幕之后,我們想要使用之前loader得到的結果。
注意:在旋轉屏幕的時候,我們的Loader還沒有提交完數據。像我們之前用AsyncTask的話,是沒有辦法在Activity/Fragment重新創建之后拿到之前任務返回的result的。但是使用Loader就簡單多了。
解決方案:
在loaderManager中刪除掉這個id的loader,因為loaderManager可能管理多個loader,所以要用id來判斷
@Override public void onLoadFinished(Loader loader, Object data) { Log.i(TAG, "onLoadFinished"); getLoaderManager().destroyLoader(LOADER_ID); }
在Activity / Fragment創建時進行邏輯判斷,如果當前這個id的loader真正運行,那么就重新初始化一下,如果沒有這個id的loader,那么就不做任何處理(沒實際測試過效果)。
這里用fragment舉例:
@Override public void onActivityCreated(Bundle savedInstanceState) { super.onCreate(savedInstanceState); ... //Reconnect to the loader only if present if(getLoaderManager().getLoader(LOADER_ID) != null) { getLoaderManager().initLoader(LOADER_ID,null, this); } }
3.9 避免出現FragmentManager exceptions
你不能在LoaderCallback這些回調里面直接創建FragmentTransaction(包括DialogFragment這種dialog)。解決方法就是使用Handler來處理FragmentTransaction。
四、Loader
終於來到了我們的重點了,話說前面說了好多好多啊,終於鋪墊完畢了。
Loader一個執行異步加載數據的抽象類,這是一個加載器的基類。你一般可以使用AsyncTaskLoader或CursorLoader,當然你也可以實現自己的子類。當加載器被激活時,它們可以被用來監視它們的數據資源,和在內容改變時發送新的結果。
注意:Loader只能是static的,不然的話他們就會保持一個對outer class的引用。
4.1 常用類
AsyncTaskLoader
一個提供異步任務的加載器
CursorLoader
一個用來查詢數據庫相關的加載器,是AsyncTaskLoader的子類。
4.2 回調方法
重要的回調
onStartLoading() onStopLoading() onReset() onForceLoad() // from Loader ORloadInBackground() // from AsyncTaskLoader
可選的回調
deliverResult() [override]
4.3 建立一個Loader類(必須是static class)
private class MyLoaderCallback implements LoaderManager.LoaderCallbacks { final private String TAG = getClass().getSimpleName(); /** * @param id * @param args * @return Object returned from onCreateLoader must not be a non-static inner member class */ @Override public Loader onCreateLoader(int id, Bundle args) { Log.i(TAG, "onCreateLoader"); return new MyAsyncLoader(getApplicationContext()); } @Override public void onLoadFinished(Loader loader, Object data) { Log.i(TAG, "onLoadFinished " + data); } @Override public void onLoaderReset(Loader loader) { Log.i(TAG, "onLoaderReset"); } }
public static class MyAsyncLoader extends AsyncTaskLoader<String> { final private String TAG = "MyAsyncLoader"; public MyAsyncLoader(Context context) { super(context); } @Override protected void onStartLoading() { super.onStartLoading(); Log.i(TAG, "onStartLoading");
forceLoad(); } @Override public String loadInBackground() { Log.i(TAG, "loadInBackground"); return "kale"; } @Override public void deliverResult(String data) { Log.i(TAG, "deliverResult"); } @Override protected void onStopLoading() { super.onStopLoading(); Log.i(TAG, "onStopLoading"); cancelLoad(); // Attempt to cancel the current load task if possible } @Override protected void onReset() { Log.i(TAG, "onReset"); super.onReset(); } }
輸出日志:
initLoader時:
退出activity時
分析:
initLoader時首先調用callback中的onCreateLoader方法,返回一個loader,然后loaderManager開始管理這個loader,直接執行loader的onStartLoading方法,我們可以在這里做點准備工作,准備工作做完了后就通過forceLoad()來執行loadInBackground()方法,在這里進行加載各種數據,這個方法執行完畢后在callback中就會自動調用onLoadFinished()方法,告訴activity已經加載結束了,並且在public void onLoadFinished(Loader loader, Object data) 中,我們可以得到結果的data對象。
分析:回調方法所在的線程
這個完全可以類比到AsyncTask,之前也有預料到這個結果,loadInBackground()是在主線程之外的線程運行的,其余的回調方法都是在主線程運行的。所以可以這么理解onStartLoading()做一些初始化的工作,真正做處理的代碼要放在loadInBackground()中,loadInBackground()中的代碼執行完畢后會自動傳輸數據,我們在callback回調中接收就好了。至於怎么結束,我們可以不怎么管他,因為在當前activity退出時,它會自動調用onStop(),onRest(),咱們在這里面可以做點收尾工作。
4.4 一個較為完整的例子
一般我們不可能就這么簡單的用loader,里面應該有一些邏輯處理,比如做點開始的准備和收尾工作之類的。
public static class MyAsyncLoader extends AsyncTaskLoader<String> { final private String TAG = "MyAsyncLoader"; private String mResult; public MyAsyncLoader(Context context) { super(context); } @Override protected void onStartLoading() { super.onStartLoading(); Log.i(TAG, "onStartLoading"); if (mResult != null) { //If we currently have a result available, deliver it immediately. deliverResult(mResult); } if (takeContentChanged() || mResult == null) { //If the data has changed since the last time it was loaded //or is not currently available, start a load. forceLoad(); // it'll call loadInBackground task } } @Override public String loadInBackground() { Log.i(TAG, "loadInBackground"); return "kale"; } @Override public void deliverResult(String data) { Log.i(TAG, "deliverResult"); if (isStarted()) { //If the Loader is currently started, we can immediately deliver its results. super.deliverResult(data); } } @Override protected void onStopLoading() { super.onStopLoading(); Log.i(TAG, "onStopLoading"); cancelLoad(); // Attempt to cancel the current load task if possible } @Override protected void onReset() { Log.i(TAG, "onReset"); super.onReset(); mResult = null; } }
4.5 CursorLoader
CursorLoader繼承自AsyncLoader,所以就不在多說了,它主要處理數據查詢方面的工作。
public static class KaleCursorLoader extends CursorLoader{public KaleCursorLoader(Context context) { super(context); } }
五、LoaderManager.LoaderCallbacks回調的方法
之前說了很多asyncLoader的東西,這里正好用CursorLoader做例子。
5.1 onCreateLoader
當你試圖訪問一個裝載器時(例如,通過initLoader()),它會檢查那個裝載器是否被已存在的id所指定。如果它沒有,它將觸發LoaderManager.LoaderCallbacks
的onCreateLoader()方法。
在下面例子中,onCreateLoader()回調方法創建了一個CursorLoader。你必須使用它的構造方法來創建一個CursorLoader,它需要對ContentProvider執行一個查詢所需要的全套的信息。它可能需要:
- uri--要檢索內容的URI。
- projection--要返回的某一列元素的列表。傳遞空將返回所有列的元素的集合,這是不高效的。
- selection--聲明要返回哪些行的過濾器,按照SQL的where語句格式化(包含where本身)。傳遞null將會返回給定URI的所有行。
- selectionArgs--在Selection中可以包含多個?,它們將會被從selectionArgs中得到的值所替代,使它們顯示在selection中。這些值將會被綁定為字符串。
- sortOrder--如何對行進行排序,被格式化成SQL ORDER BY 子句(包含ORDER BY 本身)。傳遞空值將會使用默認排序,那也許是無序的。
// If non-null, this is the current filter the user has provided. String mCurFilter; ... 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"); }
這里怎么去理解呢?就是說這個loader是查詢數據庫的,我給交給這個loader進行處理的前先把數據的URI,查詢的語句(select),排序方式(order by)等等作為構造函數的參數傳遞到loader中,這個loader用這些東西去查究好了。
5.2 onLoadFinished
當先前創建的裝載器已經完成它的裝載工作時此方法將會被調用。這個方法要保證在為這個裝載器提供的最終數據釋放之前被調用。在此刻你應該清除掉所有使用的舊數據(由於它將很快被釋放),但是不要清除你自己傳遞(發布)的數據,因為它的裝載機擁有它並將管理它。
一旦裝載機知道應用程序不再使用那些數據就會釋放它們。例如,如果數據是從一個CursorLoader返回的cursor,你自己就應該調用它的close()方法。如果游標被放置在一個CursorAdapter中,你應該使用swapCursor()方法以便舊的Cursor也被關掉。例如:
@Override public void onLoadFinished(Loader<Cursor> loader, Cursor data) { // do something data.close(); }
// This is the Adapter being used to display the list's data. SimpleCursorAdapter mAdapter; ...
@Override 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); }
5.3 onLoaderReset
當先前被創建的裝載機被重置的時候這個方法就被調用,從而使它的數據無效。這個回調可以讓你找到數據什么時候將要被釋放,這樣你就可以釋放掉對它的引用。如果游標被放置在一個CursorAdapter中,你應該實現調用swapCursor同時傳遞一個空值。
// This is the Adapter being used to display the list's data. SimpleCursorAdapter mAdapter; ... 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添加進度 & 錯誤處理機制
6.1 添加進度
Loader不像AsyncTask那樣可以有一個進度的回調方法,所以這里要通過LocalBroadcastManager來進行處理。通過廣播的形式來傳遞進度,下面僅僅是一個舉例,和實際使用無關。
@Override protected void onStart() { //Receive loading status broadcasts in order to update the progress bar LocalBroadcastManager.getInstance(this).registerReceiver(loadingStatusReceiver, new IntentFilter(MyLoader.LOADING_ACTION)); super.onStart(); } @Override protected void onStop() { super.onStop(); LocalBroadcastManager.getInstance(this).unregisterReceiver(loadingStatusReceiver); } @Override public Result loadInBackground() { // Show progress bar Intent intent = new Intent(LOADING_ACTION).putExtra(LOADING_EXTRA,true); LocalBroadcastManager.getInstance(getContext()).sendBroadcast(intent); try { return doStuff(); } finally { // Hide progress bar intent = newIntent(LOADING_ACTION).putExtra(LOADING_EXTRA, false); LocalBroadcastManager.getInstance(getContext()).sendBroadcast(intent); } }
開始的時候建立廣播對象,結束的時候注銷廣播,在運行的時候可以在某個時期發送廣播。
6.2 添加錯誤處理
你遇到error的時候通常只能簡單的返回null。
解決方法:
① 封裝一個結果和exception。比如Pair<T,Exception>。你的Loader的cache需要更智能一些,他可以檢查result是否為null或者是否有error。
② 在Loader中加入一個exception的變量。如果在出錯的時候給這個變量賦值,在finish時傳遞出去這個值,然后我們就可以在callback中進行檢查處理了。
public abstract class ExceptionSupportLoader<T>extends AsyncTaskLoader<T> {
private Exception lastException;
public ExceptionSupportLoader(Context context) { super(context); }
public Exception getLastException() { return lastException; }
@Override public T loadInBackground() { try{ return tryLoadInBackground(); } catch(Exception e) { this.lastException= e; return null; } } protected abstract T tryLoadInBackground() throws Exception; }
@Override public void onLoadFinished(Loader<Result>loader, Result result) { if(result == null) { Exception exception = ((ExceptionSupportLoader<Result>) loader).getLastException(); //Error handling } else { //Result handling } }
英文資源參考(PPT):http://download.csdn.net/detail/shark0017/8265393
參考自:
http://blog.csdn.net/dxj007/article/details/7880417
http://www.open-open.com/lib/view/open1417955629527.html
http://blog.csdn.net/liaoqianchuan00/article/details/24094913