Loader是谷歌在Android 3.0引入的非同步載入機制,能夠對資料非同步載入並顯示到Activity或Fragment上,使用者不需要對資料的生命週期進行管理,而是交給Loader機制來管理。
使用Loader的優點
假如我們需要從網路上獲取資料,通常的做法是使用子執行緒Thread+Handler或者是使用AsyncTask來處理。
Thread+Handler方法實現起來簡單直觀,不過會麻煩點,需要自己實現Handler子類,建立執行緒,還要管理Handler的生命週期。
AsyncTask實現起來會簡單些,無需自己管理執行緒和Handler。但是要管理AsyncTask的生命週期,要對Activity退出時的情況進行處理。否則可能會出現異常或記憶體洩露。
使用Loader無需關心執行緒和Handler的建立和銷燬,也無需自己管理資料整個的生命週期,Loader機制會自動幫我們處理好。我們唯一要處理的就是資料本身。
Loader使用的步驟:
- 建立FragmentActivity或Fragment
- 持有LoaderManager的例項
- 實現Loader,用來載入資料來源返回的資料
- 實現LoaderManager.LoaderCallbacks介面
- 實現資料的展示
- 提供資料的資料來源,如ContentProvider,伺服器下發的資料等
幾個相關的類
LoaderManager
管理Loader例項,並使之和FragmentActiivty或Fragment關聯上
一個Activity或Fragment有一個唯一的LoaderManager例項
一個LoaderManager例項可以管理多個Loader例項
可以在FragmentActivity或Fragmeng中使用getSupportLoaderManager()獲取到LoaderManager例項
可以使用 initLoader() 或 restartLoader() 方法開始進行資料的載入
//第一個引數,為唯一的ID,可以為任意整數,為Loader的唯一標識,這裡為0
//第二個引數,為Bundle型別,可以向Loader傳遞構造引數,這裡為null,表示Loader構造方法不需要引數
//第三個引數,LoaderManager對Loader各事件的呼叫,參考下面講到的 LoaderManager.LoaderCallbacks
getSupportLoaderManager().initLoader(0, null, new LoaderCallbacks<D>());
複製程式碼
LoaderManager.LoaderCallbacks
LoaderManager對Loader各種情況的回撥介面,包含三個回撥方法
- onCreateLoader(int,Bundle) 在這裡需要自己建立Loader物件,int 為Loader的唯一標識,Bundle為Loader的構造引數,可為空
...
new LoaderManager.LoaderCallbacks<String>() {
@Override
public Loader<String> onCreateLoader(int id, Bundle args) {
return new MyLoader();
}
...
}
複製程式碼
- onLoadFinished(Loader,D) 當LoaderManager載入完資料時回撥此方法,在這裡用UI展示資料給使用者。D為泛型,根據實際情況設定為所需的資料型別。和initLoader()LoaderCallbacks引數中的的泛型為同一型別
new LoaderManager.LoaderCallbacks<String>() {
...
@Override
public void onLoadFinished(Loader<String> loader, String data) {
show(data);
}
...
}
複製程式碼
- onLoaderReset(Loader) 當之前建立的Loader例項被重置的時候會回撥此方法,此時需要對所持有的的資料進行清除處理
new LoaderManager.LoaderCallbacks<String>() {
...
@Override
public void onLoaderReset(Loader<String> loader) {
show(null);
}
...
}
複製程式碼
Loader
從資料來源獲取資料,並對資料進行載入,為抽象類,需要自己實現子類
或使用官方已經實現的兩個子類
- AsyncTaskLoader(繼承此類的時候會遇到一個坑,見下面的分析) 處理非同步獲取資料
- CursorLoader 處理ContentProvider返回的資料
實現AsyncTaskLoader遇到的一個坑
首先自定義一個 MyAsyncTaskLoader,繼承AsyncTaskLoader,會發現需要實現引數為Context的構造方法和實現 loadInBackground() 抽象方法
//繼承AsyncTaskLoader類,裡面的泛型為返回的資料的型別,這裡設為String
public class MyAsyncTaskLoader extends AsyncTaskLoader<String>{
public MyAsyncTaskLoader(Context context) {
super(context);
}
@Override
public String loadInBackground() {
//模擬載入
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//返回獲取到的資料
return new String("MyAsyncTaskLoader Test Result");
}
}
複製程式碼
建立FragmentActivity
public class BaseActivity extends AppCompatActivity implements LoaderManager.LoaderCallbacks{
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.base_activity_layout);
// addFragment();
log("onCreate");
loadData();
}
protected void loadData(){
Log.e(getClassName(),"call");
getSupportLoaderManager().initLoader(0, null, this);
}
protected String getClassName(){
return getClass().getSimpleName();
}
@Override
public Loader onCreateLoader(int id, Bundle args) {
Log.e(getClassName(),"onCreateLoader");
return new MyAsyncTaskLoader(BaseActivity.this);
}
@Override
public void onLoadFinished(Loader loader, Object data) {
Log.e(getClassName(),"data:"+data);
}
@Override
public void onLoaderReset(Loader loader) {
}
}
複製程式碼
當執行的時候發現日誌值列印了onCreate,call,onCreateLoader,而預期中的 MyAsyncTaskLoader Test Result 並沒有輸出,也就是說 onLoadFinished 並未被回撥。除錯發現 MyAsyncTaskLoader 中的 loadInBackground() 方法也未執行。
這個是怎麼回事呢?
那麼只好檢視原始碼了,這裡所使用的都是 support-v4 的包。
檢視 AsyncTaskLoader 原始碼發現 loadInBackground() 方法的確為 abstract 型別,其被呼叫的地方是在一個叫做 LoadTask 的內部類中。
//可以把 ModernAsyncTask 看做 AsyncTask
final class LoadTask extends ModernAsyncTask<Void, Void, D> implements Runnable {
....
@Override
protected D doInBackground(Void... params) {
...
D data = AsyncTaskLoader.this.onLoadInBackground();
...
}
.....
}
複製程式碼
並且作為AsyncTaskLoader的一個全域性變數。
public abstract class AsyncTaskLoader<D> extends Loader<D> {
....
volatile LoadTask mTask;
....
}
複製程式碼
mTask 例項化和被執行的地方在 onForceLoad() 方法裡
...
@Override
protected void onForceLoad() {
...
mTask = new LoadTask();
...
executePendingTask();
}
...
void executePendingTask() {
...
if (mUpdateThrottle > 0) {
...
mHandler.postAtTime(mTask, mLastLoadCompleteTime+mUpdateThrottle);
return;
}
}
...
mTask.executeOnExecutor(mExecutor, (Void[]) null);
}
}
複製程式碼
mHandler.postAtTime 或者是 mTask.executeOnExecutor 這兩個地方就是執行 TaskLoader 的地方,並會呼叫到 doInBackground() 方法。
那麼到這裡我們可以猜測我們自定義的 MyAsyncLoader 的 loadInBackground() 未被執行,那麼 onForceLoad() 也應該未被執行。
沿著這條線索查詢看看這個 onForceLoad() 是在哪裡被呼叫的。發現其是在AsyncLoader 的父類 Loader 中的 forceLoad() 中被呼叫
public class Loader{
...
public void forceLoad() {
onForceLoad();
}
...
}
複製程式碼
然後又看到註釋發現,此方法只能在 loader 開始的時候呼叫,還是找不到什麼頭緒。
突然想到好像 CursorLoader 沒有這個問題,那麼看看它是不是有呼叫 forceLoad(),找了下,發現還果然有!是在 onStartLoading() 這個方法裡,並且只有這裡呼叫!
public class CursorLoader extends AsyncTaskLoader<Cursor> {
...
@Override
protected void onStartLoading() {
if (mCursor != null) {
deliverResult(mCursor);
}
if (takeContentChanged() || mCursor == null) {
forceLoad();
}
}
...
}
複製程式碼
那麼我模仿下這個看看是不是真的能行,MyAsyncLoader 的程式碼修改如下:
//繼承AsyncTaskLoader類,裡面的泛型為返回的資料的型別,這裡設為String
public class MyAsyncTaskLoader extends AsyncTaskLoader<String>{
public MyAsyncTaskLoader(Context context) {
super(context);
}
//新增了這段程式碼
@Override
protected void onStartLoading() {
forceLoad();
}
@Override
public String loadInBackground() {
//模擬載入
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//返回獲取到的資料
return new String("MyAsyncTaskLoader Test Result");
}
}
複製程式碼
執行後發現真的能夠輸出了!看來問題是解決了。
問題是解決了,但是還是有一個疑問,這個 onStartLoading()是在哪裡被呼叫的呢?看來還是得看看原始碼。
從 getSupportLoaderManager().initLoader(0, null, this) 開始分析,發現最後是會呼叫到 onStartLoading()。
簡記如下,可自己對照著原始碼檢視:
- LoaderManager的實現類為LoaderManagerImpl
- init()方法裡面建立 LoaderInfo info
- info = createAndInstallLoader(id, args, (LoaderManager.LoaderCallbacks)callback); 進入 createAndInstallLoader 方法
- mCallbacks.onLoadFinished(loader, data); 進入 onLoadFinished 方法
- createLoader(id, args, callback) 進入 createLoader 方法
- installLoader(info); 進入 installLoader 方法
- info.start(); 進入 start 方法
- mLoader.startLoading(); 進入 startLoading 方法
- onStartLoading();
例子
參考上面的AsyncLoader踩坑和官網例子(需要科學上網)
-End-