Loader框架

Loader Framework

如果在应用里通过ContentProvider取得数据,最糟的情况可能有一下几种:

  1. 你完全不知道android应用该怎么写,在主线程里去执行耗时操作
  2. 使用AsyncTask:
    你的app在屏幕方向改变的时候崩溃了,于是你google到stackoverflow上的答案并锁定了屏幕方向,接着你加上了容易出错的代码,在Activity中detach,attach AsyncTask.
  3. 使用CursorLoader
    从android HoneyComb到Kitkat,Loader Framework中的Loader子类都只有孤零零的CusorLoader.很可能你会使用其他的异步操作,而不仅仅是通过ContentProvider.你也许想要访问SharedPreferences,读文件或者请求访问web API.那样的话,你需要Loader,但正确实现它会有点复杂。
    我将带你完整过一遍整个流程,理解Loaders是如何工作的,为你的Loaders实现一个正确的基类,实现一个修复所有问题的CursorLoader,并扩展使其具备通知多个Uri的能力。这将是一篇长博客,所以拿一杯你喜欢的饮料边看边喝吧。

Loaders

Loader应该要做到三件事:

  1. 在后台线程里加载数据
  2. 缓存已经加载的数据,这样就不会在屏幕方向变化的时候重新加载一遍了。
  3. 如果可行,监控数据源并在适当的时候重新加载最新的数据。

Loader类自身并没有在后台线程里加载数据的机制,或者你自己实现,或者你继承AsyncTaskLoader.这部分内容就涵盖了我们需求列表第一点。

AsyncTaskLoader并没有处理好第二点。事实上AsyncTaskLoader远远没有到达功能完全,比如这种看上去非常合理的实现并不能起作用。

1
2
3
4
5
6
public class DasLoader extends AsyncTaskLoader<String> {
public DasLoader(Context context) {
super(context);
}
@Override public String loadInBackground() { return "Das"; }
}

##AbstractLoader v1##

下面这种实现提供了所有的加载和缓存数据的模板代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
public abstract class AbstractLoader<T> extends AsyncTaskLoader<T> { 
T mResult;
public AbstractLoader(Context context) {
super(context);
}

@Override
public void deliverResult(T result) {
if (isReset()) {
releaseResources(result);
return;
}
T oldResult = mResult;
mResult = result;
if (isStarted()) {
super.deliverResult(result);
}
if (oldResult != result && oldResult != null) {
releaseResources(oldResult);
}
}

@Override
public void onCanceled(T result) {
super.onCanceled(result);
releaseResources(result);
}

@Override
protected void onReset() {
super.onReset(); // Ensure the loader is stopped onStopLoading();
releaseResources(mResult);
mResult = null;
}

@Override
protected void onStartLoading() {
if (mResult != null) {
deliverResult(mResult);
}
if (takeContentChanged() || mResult == null) {
forceLoad();
}
}

@Override
protected void onStopLoading() {
cancelLoad();
}
protected void releaseResources(T result) { }
}

奇怪的是framework没有提供这样的类。而现在你可以像这样写你的自定义Loader.

1
2
3
4
5
6
7
8
9
public class DasLoader extends AbstractLoader<String> {
public DasLoader(Context context) {
super(context);
}
@Override
public String loadInBackground() {
return "Das";
}
}

但为什么需要这些代码?理解Loaders的关键是理解loader行为的不同状态:started,stopped,abandoned和reset。当进入每种状态时,相应的callback就会执行。

onStartLoading:已经创建Loader,要么加载数据,或者返回已缓存的数据。

onStopLoading:Loader应该保存缓存的数据,监控数据源变化,但不能加载数据。比如用户回到屏幕桌面,就会发生这种情况。

onAbandoned:重新启动Loader.在onCreateLoader回调方法里创建了Loader的新实例,并在Fragment或者Activity里加载新数据。在被废弃的Loader里监控数据源或者重新加载数据没有意义–数据会在新Loader里加载。当新Loader发送数据后,这个被废弃的Loader会被重置。

onReset:Loader加载的之前的数据不会再使用,并会清空。该Loader可能会重新启动,所以在你的Loader实现里清空状态。

AsyncTaskLoader提供了额外的回调:

onCancelled:当加载时发现不会再使用该数据会调用,比如当AsyncTask执行onLoadInBackgroud时被取消。在该回调中你应该处理好释放资源。

因为onRest callback和deliverResults实现中都需要释放资源,我们的AbstractLoader提供了方便的realeaseResources()方法用来关闭Cursor或者文件处理器等等。

现在梳理一遍AbstractLoader的实现。当使用 LoaderManager.initLoader()启动了Loader,onStartLoading被调用:

1
2
3
4
5
6
7
8
9
10
11
T mResult; 
// ...
@Override
protected void onStartLoading() {
if (mResult != null) {
deliverResult(mResult);
}
if (takeContentChanged() || mResult == null) {
forceLoad();
}
}

在AbstractLoader中的mResult成员保存已加载的数据.如果已经加载完数据,只需要将结果传递到Loader 客户端中。如果缓存数据为空,或者Loader被通知有新数据可以获取,就会调用forceLoad()方法强制加载数据。该方法启动AsyncTask在后台线程中调用loadInBackground,最后结果会传给deliverResults方法中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Override
public void deliverResult(T result) {
if (isReset()) {
releaseResources(result);
return;
}
T oldResult = mResult;
mResult = result;
if (isStarted()) {
super.deliverResult(result);
}
if (oldResult != result && oldResult != null) {
releaseResources(oldResult);
}
}

这块有些有趣的事情。首先,检查loader是否进入reset状态。该状态中之前所有的资源都要是否,所以只需要处理新加载的数据。然后将数据交换到缓存中,调用deliverResults,释放之前缓存的资源。

当Fragment或者Activity中活动的Loader停止了,Loaders仍然要进入到stop状态。这意味着Loaders需要保持缓存数据,监控数据是否有效,但是不能加载数据或者将数据传递到UI线程中。按照AsyncTaskLoader,意味着取消所有运行的AsyncTasks.

1
2
3
4
@Override 
protected void onStopLoading() {
cancelLoad();
}

目前AsyncTaskLoader的实现并没有打断当前的任务,只保证了这些任务的结果不会传递到UI线程中。然而,这些任务执行的结果需要释放资源,所以onCancelled回调将执行。

1
2
3
4
5
@Override
public void onCanceled(T result) {
super.onCanceled(result);
releaseResources(result);
}

最后需要实现的都回调是onReset:

1
2
3
4
5
6
7
@Override 
protected void onReset() {
super.onReset(); // Ensure the loader is stopped
onStopLoading();
releaseResources(mResult);
mResult = null;
}

这儿有两件重要的事。第一,Loader可以从started状态转换到reset状态,意味着它仍然可有活动的AsyncTasks执行loadInBackgroud.我们需要先将其停止。然后,根据约定,需要释放资源并清空缓存。

onAbandoned 回调呢?AbstractLoader 自己没有监控数据源,所以这个回调不必实现。

##CursorLoader##

那么我们如何能实现观测数据源并自动重新加载的Loader呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class MyCursorLoader extends AbstractLoader<Cursor> { 
private final ForceLoadContentObserver mObserver;

public MyCursorLoader(Context context) {
super(context);
mObserver = new ForceLoadContentObserver();
}
// bunch of setters for uri, projection, selection, etc. Omitted for brevity

@Override
public Cursor loadInBackground() {
Cursor cursor = getContext().getContentResolver().query(mUri, mProjection, mSelection, mSelectionArgs, mSortOrder);
if (cursor != null) {
// Ensure the cursor window is filled
cursor.getCount();
cursor.registerContentObserver(mObserver);
}
return cursor;
}
}

该实现有两个bug,其中一个是功能性bug.让我们着手后一个吧。

onStartLoading 当进入到该状态时,监控数据源。但想一想,如果查询需要200ms,而数据每150ms发生变化,那将会怎样?Loader将永远不能将数据发送出去,因为每个load请求将会在loadInBackgroud执行的过程中被content observer取消。

我猜大概就是这个原因,CursorLoader的Android实现在数据加载完成时注册observer.采用这种方法,第一次加载的数据将会尽快发送出去,但接下来加载的数据只会在加载过程中数据源不发生变化时才能发送出去。

##Fixing CursorLoader##

  1. 当第一次加载的数据发送完毕,注册ContentObserver.
  2. 在onAbandon方法里取消注册ContentObserver.

第一点要求我们改变deliverResult方法,因此我们这样来修改AbstractLoader:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Override 
public void deliverResult(T result) {
if (isReset()) {
releaseResources(result);
return;
}
T oldResult = mResult;
mResult = result;
if (isStarted()) {
if (oldResult != result) {
onNewDataDelivered(result);
}
super.deliverResult(result);
}
if (oldResult != result && oldResult != null) {
releaseResources(oldResult);
}
}
protected void onNewDataDelivered(T data) { }

CursorLoader看起来应该像这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class MyCursorLoader extends AbstractLoader<Cursor> {  

private final ForceLoadContentObserver mObserver;

public MyCursorLoader(Context context) {
super(context);
mObserver = new ForceLoadContentObserver();
}
// bunch of setters for uri, projection, selection, etc. Omitted for brevity
@Override
public Cursor loadInBackground() {
Cursor cursor = getContext().getContentResolver().query(mUri, mProjection, mSelection, mSelectionArgs, mSortOrder);
if (cursor != null) {
// Ensure the cursor window is filled
cursor.getCount();
}
return cursor;
}
@Override
protected void onNewDataDelivered(Cursor data) {
super.onNewDataDelivered(data);
data.registerContentObserver(mObserver);
}
}

第二部分–在onAbandon里取消注册observer会有点困难。如果observer没有注册,调用Cursor.unregisterContentObserver是非法的,而且当deliverResults没被调用的时候onAbandon可以被调用。于是,我们创建一个代理的ContentObserver。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
public class DisableableContentObserver extends ContentObserver {
private final ContentObserver mWrappedObserver;
private boolean mIsEnabled = true;
public DisableableContentObserver(ContentObserver wrappedObserver) {

super(new Handler());
mWrappedObserver = wrappedObserver;
}

@Override
public void onChange(boolean selfChange) {
if (mIsEnabled) {
mWrappedObserver.onChange(selfChange);
}
}

public void setEnabled(boolean isEnabled) {
mIsEnabled = isEnabled;
}
}


public class MyCursorLoader extends AbstractLoader<Cursor> {

private final DisableableContentObserver mObserver;
public MyCursorLoader(Context context) {
super(context);
mObserver = new DisableableContentObserver(new ForceLoadContentObserver());
}
// bunch of setters for uri, projection, selection, etc. Omitted for brevity
@Override
protected void onStartLoading() {
mObserver.setEnabled(true);
super.onStartLoading();
}

@Override
protected void onAbandon() {
mObserver.setEnabled(false);
}

@Override
protected void onReset() {
mObserver.setEnabled(false);
super.onReset();
}

@Override
public Cursor loadInBackground() {
Cursor cursor = getContext().getContentResolver().query(mUri, mProjection, mSelection,
mSelectionArgs, mSortOrder);
if (cursor != null) {
// Ensure the cursor window is filled
cursor.getCount();
}
return cursor;
}

@Override
protected void onNewDataDelivered(Cursor data) {
super.onNewDataDelivered(data);
data.registerContentObserver(mObserver);
}
}

##AbstractObservingLoader##
The CursorLoader is a bit special case, because the Cursor itself contains ContentObservable. In most cases however the content observers and loaded data would be completely separated. For these cases it would be useful to have a base class for Loader which registers some ContentObservers:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public abstract class AbstractObservingLoader<T> extends AbstractLoader<T> {
protected final DisableableContentObserver mObserver;
private boolean mIsRegistered;

public AbstractObservingLoader(Context context) {
super(context);
mObserver = new DisableableContentObserver(new ForceLoadContentObserver());
}

@Override
protected void onStartLoading() {
mObserver.setEnabled(true);
super.onStartLoading();
}

@Override
protected void onAbandon() {
mObserver.setEnabled(false);
unregisterObserver(mObserver);
mIsRegistered = false;
}

@Override
protected void onReset() {
mObserver.setEnabled(false);
unregisterObserver(mObserver);
mIsRegistered = false;z
super.onReset();
}

@Override
protected void onNewDataDelivered(T data) {
if (!mIsRegistered) {
mIsRegistered = true;
registerObserver(mObserver);
}
}

protected abstract void registerObserver(ContentObserver observer);
protected abstract void unregisterObserver(ContentObserver observer);
}

We need to keep the registered state in our Loader, because the default Observable implementation doesn’t like registering the same observer twice or unregistering not registered observer.
Now we can use this class as a base for a Loader which should be reloaded when one of specified Uris is triggered:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class MyCursorLoader extends AbstractObservingLoader<Cursor> {

public MyCursorLoader(Context context) {
super(context);
}

// bunch of setters for uri, projection, selection, etc. Omitted for brevity

@Override
public Cursor loadInBackground() {
Cursor cursor = getContext().getContentResolver().query(mUri, mProjection, mSelection,
mSelectionArgs, mSortOrder);
if (cursor != null) {
// Ensure the cursor window is filled
cursor.getCount();
}
return cursor;
}

@Override
protected void onNewDataDelivered(Cursor data) {
super.onNewDataDelivered(data);
data.registerContentObserver(mObserver);
}

@Override
protected void registerObserver(ContentObserver observer) {
for (Uri uri : mObservedUris) {
getContext().getContentResolver().registerContentObserver(uri, true, observer);
}
}

@Override
protected void unregisterObserver(ContentObserver observer) {
getContext().getContentResolver().unregisterContentObserver(observer);
}
}

以下是原文链接:
android loaders