likes
comments
collection
share

多线程编程之AsyncTask

作者站长头像
站长
· 阅读数 1

讲在最前面的话,AsyncTask 已经被官方废弃,本篇文章是从AsyncTask源码角度来看一些值得学习的地方,怀揣一颗学习的心,我们可以从任何地方汲取知识的营养

官方注释: "AsyncTask 旨在实现对 UI 线程的正确和简单使用。然而,最常见的用例是用于集成到 UI 中,这可能导致上下文泄漏、丢失回调或在配置更改时崩溃。它在平台的不同版本上行为不一致,会吞没来自 doInBackground 的异常,并且与直接使用 Executor 相比并没有提供太多实用性。

AsyncTask 被设计为围绕 Thread 和 Handler 的辅助类,并不构成通用的线程框架。建议将 AsyncTask 用于短期操作(最多几秒钟)。如果需要长时间运行的线程,强烈建议使用 java.util.concurrent 包提供的各种 API,如 Executor、ThreadPoolExecutor 和 FutureTask。

异步任务由在后台线程上运行的计算和在 UI 线程上发布结果组成。异步任务由 3 个泛型类型 Params、Progress 和 Result 定义,并包括 4 个步骤 onPreExecute、doInBackground、onProgressUpdate 和 onPostExecute。"

简介

AsyncTask 是 Android 平台提供的一个简便的类,用于在后台执行异步任务,而不会阻塞主线程,以保持用户界面的响应性。它可以用于处理一些耗时的操作,例如网络请求、数据库查询等。

有趣地说,AsyncTask 就像是一个勤劳的小助手,可以帮助你在后台完成一些繁重的工作,而你可以继续专注于其他的事情,比如喝杯咖啡或者玩手机游戏。

然而,AsyncTask 在 Android 的最新版本中已经被官方标记为过时(deprecated)。这是因为 AsyncTask 存在一些潜在的问题和限制,例如:

  1. 内存泄漏:如果 AsyncTask 没有正确地取消或完成,它可能会导致内存泄漏,因为它持有对外部对象的引用。

  2. 配置更改问题:当屏幕发生旋转或配置更改时,AsyncTask 可能无法正确处理状态的保存和恢复,导致数据丢失或其他异常。

  3. 并发限制AsyncTask 默认使用单个后台线程执行任务,这意味着同时只能执行一个任务。在某些场景下,可能需要并行执行多个任务,而 AsyncTask 无法满足这种需求。

其实上述问题都是可以通过代码设计来解决的,但是这篇文章是为了介绍“过时”的AsyncTask,因此不做过多介绍。常见的方法是及时在onDestroy中释放引用,或者临时保存数据供后续使用。

但是过多的考虑异常case,就违背了选择AsyncTask 的初衷,因此Android 官方推荐使用其他的异步任务处理方式。

所以,尽管 AsyncTask 在过去是开发 Android 应用程序中常用的工具,但现在它已经被官方弃用,开发者应该优先考虑使用更现代的异步任务处理方式。

源码

线程通信的关键:Handler

AsyncTask 的内部,它维护了一个静态的 InternalHandler 类,该类扩展自 Handler。这个 InternalHandler 用于在后台线程中发送消息给主线程。

下面是 AsyncTask 源码中 InternalHandler 的定义:

private static class InternalHandler extends Handler {
    public InternalHandler(Looper looper) {
        super(looper);
    }

    @SuppressWarnings({"unchecked", "RawUseOfParameterizedType"})
    @Override
    public void handleMessage(Message msg) {
        AsyncTaskResult<?> result = (AsyncTaskResult<?>) msg.obj;
        switch (msg.what) {
            case MESSAGE_POST_RESULT:
                // There is only one result
                result.mTask.finish(result.mData[0]);
                break;
            case MESSAGE_POST_PROGRESS:
                result.mTask.onProgressUpdate(result.mData);
                break;
        }
    }
}

AsyncTask 中,当需要发送消息时,它会创建一个 Message 对象并将其发送到 InternalHandler 中。

AsyncTask 源码中有一个名为 postResult() 的方法,用于发送任务结果的消息。下面是 postResult() 方法的代码片段

private Result postResult(Result result) {
    @SuppressWarnings("unchecked")
    Message message = getHandler().obtainMessage(MESSAGE_POST_RESULT,
            new AsyncTaskResult<Result>(this, result));
    message.sendToTarget();
    return result;
}

通过使用 InternalHandlerMessageAsyncTask 实现了在后台线程和主线程之间进行消息传递的机制。后台线程可以通过调用 postResult()postProgress() 方法来向主线程发送消息,而主线程中的 InternalHandler 会接收到这些消息并执行相应的操作,例如调用 onPostExecute()onProgressUpdate() 方法来更新 UI。

AsyncTask 中的两个Executor

AsyncTask存在两个Executor,一个是SERIAL_EXECUTOR,这是一个串行执行的Executor,它可以确保所有任务都按照它们被提交的顺序执行。这对于需要避免多个任务同时访问共享资源的情况非常有用,因为它可以保证一个任务完成后再执行下一个任务。 具体的代码+注释如下:

// SerialExecutor 是一个实现了 Executor 接口的静态内部类。它用于按顺序执行多个任务,以避免多个任务同时运行导致的竞争和冲突。
private static class SerialExecutor implements Executor {
    // 使用 ArrayDeque 存储待执行的任务队列。
    final ArrayDeque<Runnable> mTasks = new ArrayDeque<Runnable>();
    // 当前正在执行的任务。
    Runnable mActive;

    // 实现 Executor 接口的 execute 方法,用于提交任务。
    public synchronized void execute(final Runnable r) {
        // 将任务包装成一个新的 Runnable,以便执行完后可以调用 scheduleNext() 方法继续执行下一个任务。
        mTasks.offer(new Runnable() {
            public void run() {
                try {
                    r.run();
                } finally {
                    scheduleNext();
                }
            }
        });
        // 如果当前没有任务在执行,则立即调度下一个任务。
        if (mActive == null) {
            scheduleNext();
        }
    }

    // 调度下一个任务的方法。
    protected synchronized void scheduleNext() {
        // 从任务队列中获取下一个任务并将其标记为正在执行。
        if ((mActive = mTasks.poll()) != null) {
            // 使用 THREAD_POOL_EXECUTOR线程池 来执行任务。
            THREAD_POOL_EXECUTOR.execute(mActive);
        }
    }
}

另一个是:THREAD_POOL_EXECUTOR,官方解释翻译一下贴下来:

一个可以用于并行执行任务的 Executor。

已经过时了,对于不同的任务,使用单个线程池会导致子优化的行为。小型的-CPU-密集型任务可以从一个有界的池和队列中受益,而长时间阻塞的任务,例如网络操作,则可以从多个线程中受益。请使用或创建一个配置符合您使用情况的 Executor。

也就是说,之前设置了一个串行执行线程池,用来保障任务能够以有序的方式被调用,但是串行的阻塞性太强,因此再配一个并行的线程池来加快线程的工作效率。

public static final Executor THREAD_POOL_EXECUTOR;

static {
    ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
            CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE_SECONDS, TimeUnit.SECONDS,
            new SynchronousQueue<Runnable>(), sThreadFactory);
    threadPoolExecutor.setRejectedExecutionHandler(sRunOnSerialPolicy);
    THREAD_POOL_EXECUTOR = threadPoolExecutor;
}

这里使用了SynchronousQueue作为任务队列,可以通过生产者-消费者1对1的形式快速响应任务。