[Android] Task & Document / Loader

Preface

这个月状态挺差的,各种不顺心和意外,不过还是照例把新学的东西整理一下吧。

  • Task 和 Document 的基本概念
  • Loader 的基本概念和用法

Task & Document

Task

下面的代码将启动照相机:

1
2
Intent lIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
startActivityForResult(lIntent);

效果如图:

Camera Activity in same Task

想把照相机和自己的 App 分开的话,就这样:

1
2
3
Intent lIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
lIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivityForResult(lIntent);

效果如图:

Camera Activity in different task

这张图里看上去有两个 App,在 Android 官方文档 里给每个像上图里的 App 定义了一个术语叫做 “Task”,也就是上图里有2个 Task。容纳许多个 Task 的整个屏幕(也就是上图里的整个屏幕)就叫做 “Recents screen”(其他非正式名称包括但不限于: Overview screen, Recent apps).

Document

使用下面的代码可以让自己的 App 的某个 Activity 以多个 Task 的形式展现在 Recents screen 里:

1
2
3
4
5
6
7
Intent lIntent = new Intent(this, NewActivity.class);
lIntent.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
lIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT);
startActivity(lIntent);

// 在新的 Activity 里使用下面的代码可以修改 Recents screen 里每个 Task 的标题
setTaskDescription(new ActivityManager.TaskDescription("The title"));

Multiple documents

Task 是相对于 Recents screen 而言的,也就是上面的图片里一共有 3 个 Task。Android 官方文档 创造了一个新的概念称之为 “Document”,Document 是相对于单个 App 而言的,以上面的图片为例的话,Hello Android 这个 App 就有 2 个 Document,照相机 App 只有1个 Document。
不过我觉得还有另一种说法, 上面的 Hello Android 创建了一个新的 Task 名为 The title -4771557, 这个新的 Task 才算做是 Document, 原来已经存在的叫做 Hello Android 的 Task 不能称之为 Document, 因此 Hello Android 这个 App 一共有1个 Document。
至于哪种说法是对的 我也没能在官方文档中找到。

Loader

以我目前的经验来看,Loader 适合那种设计上耗时不太长的操作(1s 或更少,比如使用 ContentProvider 请求数据,或者是 从设备存储里读取某个文件,并且不指望这个异步操作能完成),其最终目的是让 App 尽快响应用户的操作(Responsive)。目前来看,相比 AsyncTaskLoaderManager 会帮我 handle 一些 Activity/Fragment 的生命周期事件,并在适当的时候通知我分配/回收资源。在配置发生变动时(比如屏幕旋转)会导致 Activity/Fragment 被重新创建,这种情况下用 LoaderManager 就比手动管理 AsyncTask 来得省事。

实现 AsyncTaskLoader

鉴于我还没有学到 ContentProvider 那一章,所以目前先用 AsyncTaskLoader 来做演示。

  1. 新建一个类并继承 android.content.AsyncTaskLoader
  2. Override 下列方法
    • onStartLoading()
    • loadInBackground()
    • onStopLoading()
    • deliverResult(D data)
    • onReset()

下面展示一个 AsyncTaskLoader 的一种实现。

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
private static final class LoaderImpl extends AsyncTaskLoader<String> {
public static final int LOADER_ID = 0;

private String mStrUrl;
private String mData;

public LoaderImpl(Context context, Bundle parameters) {
super(context);
mStrUrl = parameters.getString("url");
}

@Override
protected void onStartLoading() {
/* Loader 被初始化后会立即自动调用这个方法.
* 如果这个 Loader 在 Activity/Fragment 被重建之前曾经载入过数据(也就是 loadInBackgroun())
* 被调用过, 这里就可以直接把之前载入过的数据直接返回.
* 载入过的数据需要在 deliverResult(D data) 里进行缓存.
* 如果有其他的需要在载入数据之前做的事情, 也需要在这里做, 等同于 AsyncTask.onPreExecute().
*/
if (mData != null) {
// 如果有缓存的数据 则直接返回.
deliverResult(mData);
} else {
// 如果缓存是空的 则载入新的数据.
forceLoad();
// 调用 forceLoad() 后 loadInBackground() 就会被调用.
}
}

@Override
public String loadInBackground() {
/* 等同于 AsyncTask.doInBackground(Object... params). */
String lStrResult = downloadHtmlFromSomewhere(mStrUrl);
return lStrResult;
}

@Override
protected void onStopLoading() {
/* 这个 onStopLoading() 的设计实在是无法理解, 稍后再具体解释. */
cancelLoad();
}

@Override
public void deliverResult(List<PackageInfo> data) {
// 可以在这里缓存 loadInBackground() 的结果, 然后再交由父类将结果递交出去.
mData = data;
super.deliverResult(data);
}

@Override
protected void onReset() {
// 在这里释放当前 Loader 使用的所有资源.
super.onReset();
onStopLoading();
this.mFilterAndSortOption = null;
}
}

实现 LoaderManager.LoaderCallbacks

该接口的实现负责处理下面这几件事:

  1. onCreateLoader(int id, Bundle args): 负责根据 ID 为 LoaderManager 提供相应的 Loader 实例
  2. onLoadFinished(Loader loader, D data): Loader 运行出结果后会把结果递送到这里, data 就是结果
  3. onLoaderReset(Loader loader): 当 Loader 需要被回收的时候调用该回调函数

下面是一个 LoaderManager.LoaderCallbacks 的实现的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
private class LoaderCallbacksImpl implements LoaderManager.LoaderCallbacks {
@Override
public Loader onCreateLoader(int id, Bundle args) {
if (args == null) args = new Bundle();
if (id == LoaderImpl.LOADER_ID) {
return new LoaderPackageList(getContext(), args);
}
throw new IllegalArgumentException("LoaderId not found: " + id);
}

@Override
public void onLoadFinished(Loader loader, Object data) {
if (loader.getId() == LoaderImpl.LOADER_ID) {
processResult((String) data);
}
}

@Override
public void onLoaderReset(Loader loader) {
if (loader.getId() == LoaderPackageList.LOADER_ID) {
releaseResources(loader);
}
}
}

AsyncTaskLoader 的优缺点

优点:

  1. LoaderManager 会在 Activity/Fragment 发生配置变化的时候 管理好它所管理的 Loader 实例

缺点:

  1. AsyncTaskLoader 没有 AsyncTaskpublishProgress(), 想在界面上展示进度的话 还得另想办法.
    因为 Loader 的性质导致它不能强引用某个 Activity/Fragment, 所以还得考虑用 WeakReference 或者 Handler 来实现.
  2. 想中断正在运行的 Loader 需要调用 cancelLoad(), 但 cancelLoad() 运行在主线程而不是 loadInBackground() 所在的 Worker Thread, 因此没法使用标准的 Thread API Thread.currentThread().interrupt()Thread.currentThread().isInterrupted() 来中断正在运行的 Loader. 由此带来的问题就是: 如果 loadInBackground() 里的代码想知道当前 Loader 是否想中断运行的话, 还得使用 Loader.isLoadInBackgroundCancelled() 来判断, 这样就将所有代码限制到只能在 loadInBackground(), 不能分开到其它的类里.

综合上面的2个缺点, 我认为 AsyncTaskLoader 不适合用来做过长时间的、需要用户感知到进度的操作。也许以后得自己实现一个类似 AsyncTaskLoader 的类来弥补上面的缺点。

结语

可以说 这篇博文写得非常不走心,Android 官方文档有些晦涩不清,搞得我光是 Task/Document 的概念就查了好多资料才理解它。后来又看 Loader,它的优点在缺点的掩盖下分文不值。

References

  1. Tasks and Back Stack | Android Developers - developer.android.com
  2. Recents Screen | Android Developers - developer.android.com
  3. Dominating the Overview Screen – Google Developers – medium.com