Hotaru's Notebook

[Android] Task & Document / Loader

Preface

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

Task & Document

Task

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

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

效果如图:

Camera Activity in same Task

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

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 里:

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 的一种实现。

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 的实现的例子:

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

#Android #Task #Document #Loader