Skip to content

AsyncTask 任务生命周期

从 executeOnExecutor 追踪 AsyncTask 的状态、线程切换、进度消息和取消竞态,并解释它为何退回到通用 Executor。

基于android-17.0.0_r1
AndroidAsyncTaskHandlerExecutor源码阅读

AsyncTask 任务生命周期 ​

AsyncTask 看起来像四个回调:onPreExecute()、doInBackground()、onProgressUpdate() 和 onPostExecute()。真正决定行为的却是另一条链:execute() 先改变一次性状态,再把 FutureTask 交给执行器;后台 Callable 完成后把结果封装成 Message,由一个绑定回调 Looper 的 Handler 把控制权送回目标线程。

本文面向已经读过 Handler 消息发送、 Handler 消息处理 和 HandlerExecutor 提交语义 的读者。重点不是回顾历史版本,而是回答一个可以 顺着源码复述的问题:一个 AsyncTask 实例从 execute() 到最终回调时,谁拥有任务、哪些字段改变、 取消和异常在哪个线程生效。文章只讨论 frameworks/base 中这份实现,不把 AndroidX 的 AsyncTask 替代品当作同一实现。

读完后,读者应能定位默认执行器和回调 Handler,解释为什么 execute() 只能调用一次,判断 cancel(true) 是否真的停止了后台代码,并区分“结果消息已经入队”和“回调已经完成”。

1. 入口契约 ​

源码文件:frameworks/base/core/java/android/os/AsyncTask.java

AsyncTask 的公开入口最终都汇聚到 executeOnExecutor()。execute() 只是选择当前的 sDefaultExecutor;Android 17 的默认值是全局的 SERIAL_EXECUTOR。

java
@MainThread
public final AsyncTask<Params, Progress, Result> execute(Params... params) {
    return executeOnExecutor(sDefaultExecutor, params);
}

@MainThread
public final AsyncTask<Params, Progress, Result> executeOnExecutor(Executor exec,
        Params... params) {
    if (mStatus != Status.PENDING) {
        switch (mStatus) {
            case RUNNING:
                throw new IllegalStateException("Cannot execute task:"
                        + " the task is already running.");
            case FINISHED:
                throw new IllegalStateException("Cannot execute task:"
                        + " the task has already been executed "
                        + "(a task can be executed only once)");
        }
    }

    mStatus = Status.RUNNING;
    onPreExecute();
    mWorker.mParams = params;
    exec.execute(mFuture);
    return this;
}

这里有三个容易被回调名称掩盖的事实:

时点发生的事情所在线程
进入 executeOnExecutor检查 mStatus,写入 RUNNING调用者;API 契约要求 UI 线程
onPreExecute()同步直接调用,不经过 Handler调用者线程
exec.execute(mFuture)只提交后台执行单元由传入 Executor 决定

@MainThread 是调用约定,不是运行时线程检查。若 onPreExecute() 抛异常,状态已经写成 RUNNING,后续 exec.execute() 不会发生;该实例也不会自动回滚到 PENDING。这解释了为什么 AsyncTask 的一次性限制不仅是 API 设计,也是字段写入顺序产生的结果。

2. 对象组成 ​

源码文件:frameworks/base/core/java/android/os/AsyncTask.java

一个实例把业务输入、可取消的执行单元和回调通道拆成几个字段。mWorker 是真正实现 Callable<Result> 的对象,mFuture 则负责把它接到 Executor 的提交和取消协议。

java
private final WorkerRunnable<Params, Result> mWorker;
private final FutureTask<Result> mFuture;
private volatile Status mStatus = Status.PENDING;
private final AtomicBoolean mCancelled = new AtomicBoolean();
private final AtomicBoolean mTaskInvoked = new AtomicBoolean();
private final Handler mHandler;

private static abstract class WorkerRunnable<Params, Result> implements Callable<Result> {
    Params[] mParams;
}

private static class AsyncTaskResult<Data> {
    final AsyncTask mTask;
    final Data[] mData;

    AsyncTaskResult(AsyncTask task, Data... data) {
        mTask = task;
        mData = data;
    }
}

字段的所有者和消费者可以这样分开看:

字段写入者消费者生效时机
mWorker.mParamsexecuteOnExecutorWorkerRunnable.call后台单元开始运行时
mStatus入口与 finish再次 execute、getStatus入口或结果消息分发时
mCancelledcancel 或后台异常isCancelled、finish、publishProgress原子写入后立即可见
mTaskInvokedWorkerRunnable.calldone后台 call 是否真正开始
mHandler构造函数postResult、publishProgress构造时固定回调 Looper

mTaskInvoked 不是“任务完成”标志。它只表示 WorkerRunnable.call() 是否进入过;任务可能 已经开始但仍在运行,也可能在 call() 抛异常后被 done() 观察到。

3. 执行分流 ​

源码文件:frameworks/base/core/java/android/os/AsyncTask.java

默认执行器和显式并行执行器的差异,来自两个完全不同的队列模型。SERIAL_EXECUTOR 维护一个 进程内 ArrayDeque,前一个包装任务执行完毕后才调用 scheduleNext();THREAD_POOL_EXECUTOR 则使用 SynchronousQueue,最多保留 20 个工作线程。

java
private static class SerialExecutor implements Executor {
    final ArrayDeque<Runnable> mTasks = new ArrayDeque<Runnable>();
    Runnable mActive;

    public synchronized void execute(final Runnable r) {
        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.execute(mActive);
        }
    }
}

串行包装器最终仍把当前 mActive 交给共享线程池。线程池本身的构造如下,它与 SerialExecutor 的等待队列承担不同职责。

java
private static final int CORE_POOL_SIZE = 1;
private static final int MAXIMUM_POOL_SIZE = 20;
private static final int KEEP_ALIVE_SECONDS = 3;

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 不保存等待任务;当 20 个线程都不能接收新任务时,拒绝处理器才懒创建一个 5 线程的备用池和无界 LinkedBlockingQueue。因此“使用并行执行器”不等价于“永远最多 20 个 任务”,也不等价于“任务一定排队等待”;具体结果取决于当前线程是否空闲以及拒绝处理器是否被触发。

这张图表达的是提交分流,不是回调线程图。无论选哪种 Executor,结果回调仍由 mHandler 决定;THREAD_POOL_EXECUTOR 只改变 doInBackground() 的运行位置和并发关系。

4. 后台单元 ​

源码文件:frameworks/base/core/java/android/os/AsyncTask.java

构造函数创建 WorkerRunnable 和重写过的 FutureTask。call() 先把 mTaskInvoked 写为 true,再降低线程优先级并调用子类的 doInBackground()。无论正常返回还是抛出 Throwable, finally 都会调用 postResult(result)。

java
mWorker = new WorkerRunnable<Params, Result>() {
    public Result call() throws Exception {
        mTaskInvoked.set(true);
        Result result = null;
        try {
            Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
            result = doInBackground(mParams);
            Binder.flushPendingCommands();
        } catch (Throwable tr) {
            mCancelled.set(true);
            throw tr;
        } finally {
            postResult(result);
        }
        return result;
    }
};

这里的 catch (Throwable) 并不是把异常转成普通取消。它先把 mCancelled 设为 true,再 重新抛出;因此 FutureTask.done() 仍会看到执行失败,而主线程收到结果消息后会走 onCancelled(result)。真正的异常还会沿执行器线程的 FutureTask 路径继续传播,不能把 onCancelled 理解为异常已经被安全处理。

Binder.flushPendingCommands() 位于后台计算正常返回之后、结果消息入队之前。它不是结果 回调,也不是线程切换;它只处理当前线程待刷新的 Binder 命令。把它放在这里说明了结果回到 回调 Looper 之前,后台单元还存在一个 framework 清理步骤。

5. 回调通道 ​

源码文件:frameworks/base/core/java/android/os/AsyncTask.java

默认构造函数使用主 Looper;隐藏构造函数允许传入 Handler 或 Looper。当传入的是主 Looper 或 null 时,所有实例共享懒创建的 InternalHandler;其他 Looper 则各自创建 Handler。

java
private static Handler getMainHandler() {
    synchronized (AsyncTask.class) {
        if (sHandler == null) {
            sHandler = new InternalHandler(Looper.getMainLooper());
        }
        return sHandler;
    }
}

public AsyncTask(@Nullable Looper callbackLooper) {
    mHandler = callbackLooper == null || callbackLooper == Looper.getMainLooper()
        ? getMainHandler()
        : new Handler(callbackLooper);
    // 随后创建 mWorker 和 mFuture
}

结果和进度都使用同一个 Handler,但用不同的 what 值区分。AsyncTaskResult 把任务实例 也放入 Message.obj,所以消息被哪个 Handler 消费并不改变任务对象的归属。

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

protected final void publishProgress(Progress... values) {
    if (!isCancelled()) {
        getHandler().obtainMessage(MESSAGE_POST_PROGRESS,
                new AsyncTaskResult<Progress>(this, values)).sendToTarget();
    }
}

这两个入口只负责构造并投递消息;真正区分结果消息和进度消息的是回调 Handler 的 handleMessage() 分支。

java
private static class InternalHandler extends Handler {
    @Override
    public void handleMessage(Message msg) {
        AsyncTaskResult<?> result = (AsyncTaskResult<?>) msg.obj;
        switch (msg.what) {
            case MESSAGE_POST_RESULT:
                result.mTask.finish(result.mData[0]);
                break;
            case MESSAGE_POST_PROGRESS:
                result.mTask.onProgressUpdate(result.mData);
                break;
        }
    }
}

sendToTarget() 只是把消息送入 Handler 绑定的 MessageQueue;它不等待目标线程执行。因此 doInBackground() 返回与 onPostExecute() 开始之间至少隔着一次消息入队和一次 Looper 分发。

6. 结果收束 ​

源码文件:frameworks/base/core/java/android/os/AsyncTask.java

结果消息最终进入 finish()。它在读取 mCancelled 后选择取消回调或正常回调,最后才把状态 写为 FINISHED。

java
private void finish(Result result) {
    if (isCancelled()) {
        onCancelled(result);
    } else {
        onPostExecute(result);
    }
    mStatus = Status.FINISHED;
}

这个顺序带来两个边界:

  1. onPostExecute() 尚未开始时调用 cancel(false),即使 cancel() 返回 false,只要 finish() 读取到的 mCancelled 为 true,也不会再选择 onPostExecute()。
  2. 如果 onCancelled() 或 onPostExecute() 自身抛出异常,mStatus = FINISHED 不会执行, 状态可能停留在 RUNNING。因此 FINISHED 表示结果回调正常返回后的状态,不是后台计算 已经结束的全局事实。

状态和回调的关系可以压缩成下表:

状态/标志代表什么不代表什么
PENDING尚未通过入口提交没有对象或没有回调 Handler
RUNNING入口已接受一次执行并写入状态doInBackground 已开始或一定会完成
mTaskInvoked=trueWorkerRunnable.call 已进入任务已成功返回
mCancelled=true取消请求或后台 Throwable 已标记后台线程已经停止
FINISHEDfinish 中的回调正常返回所有排队的进度消息都已消费

7. 取消竞态 ​

源码文件:frameworks/base/core/java/android/os/AsyncTask.java

cancel() 先写原子取消标志,再委托 FutureTask.cancel();它不等待后台线程,也不清理已经 进入回调队列的进度消息。

java
public final boolean cancel(boolean mayInterruptIfRunning) {
    mCancelled.set(true);
    return mFuture.cancel(mayInterruptIfRunning);
}

可以把取消分成三种输入:

取消时机FutureTask 结果后续源码路径
call() 尚未开始任务不进入 call();done() 捕获 CancellationExceptionpostResultIfNotInvoked(null),最终 onCancelled(null)
call() 正在运行,mayInterruptIfRunning=false后台代码继续运行只有 doInBackground() 主动检查 isCancelled() 才能提前返回
call() 正在运行,mayInterruptIfRunning=true执行器尝试中断工作线程中断是否终止业务由 doInBackground() 使用的阻塞 API 决定

publishProgress() 的检查只发生在“准备发送”这一刻。已经进入 MessageQueue 的进度消息不会 被 cancel() 主动移除,而 InternalHandler 的进度分支也没有再次读取 isCancelled()。 因此不要把“取消后不再调用 publishProgress”误写成“取消后所有进度回调都必然消失”;已经 排队的消息仍可能先被目标 Looper 消费。

8. 完成补偿 ​

源码文件:frameworks/base/core/java/android/os/AsyncTask.java

FutureTask.done() 与 WorkerRunnable.call() 都可能触发 postResult(),但通过 mTaskInvoked 避免重复发送:正常后台路径先在 finally 发结果,done() 读取到 true 后 不再补发;取消发生在 call() 之前时,done() 负责补发一个 null 结果。

java
mFuture = new FutureTask<Result>(mWorker) {
    @Override
    protected void done() {
        try {
            postResultIfNotInvoked(get());
        } catch (InterruptedException e) {
            android.util.Log.w(LOG_TAG, e);
        } catch (ExecutionException e) {
            throw new RuntimeException("An error occurred while executing doInBackground()",
                    e.getCause());
        } catch (CancellationException e) {
            postResultIfNotInvoked(null);
        }
    }
};

private void postResultIfNotInvoked(Result result) {
    final boolean wasTaskInvoked = mTaskInvoked.get();
    if (!wasTaskInvoked) {
        postResult(result);
    }
}

这段补偿逻辑解决的是“后台 Callable 从未进入”的结果通知,不是 exactly-once 事务协议。结果 消息仍可能在 Looper 退出时无法分发;回调抛异常也可能阻止状态收束。若业务要求可重试、持久化 或幂等,必须在任务自己的数据协议中实现,不能从 AsyncTask 的 Result 泛型推出这些保证。

9. 线程边界 ​

源码文件:

  • frameworks/base/core/java/android/os/AsyncTask.java
  • frameworks/base/core/java/android/os/Handler.java
  • frameworks/base/core/java/android/os/Looper.java

从源码顺序可以得到一条不含糊的线程路径:

text
调用线程
  └─ executeOnExecutor
      ├─ onPreExecute
      └─ Executor.execute(FutureTask)
          └─ WorkerRunnable.call(执行器线程)
              ├─ doInBackground
              └─ Handler.sendToTarget(回调消息入队)
                  └─ Looper → InternalHandler.handleMessage(回调 Looper 线程)
                      ├─ onProgressUpdate
                      └─ finish → onCancelled / onPostExecute

默认构造函数使最后一段落到主 Looper,但隐藏的 AsyncTask(Looper) 构造函数可以把回调送到 另一条 Looper。因而“AsyncTask 的回调总在主线程”是默认使用方式,不是这个类所有构造路径 都满足的绝对事实。

10. 代码验证 ​

下面的验证不依赖 AndroidX;它们针对这份源码中可观察的状态和线程边界。

10.1 一次性入口 ​

源码文件:frameworks/base/core/java/android/os/AsyncTask.java

对应符号是 executeOnExecutor() 与 mStatus。

建立一个最小子类,在第一次 execute() 后再次调用 execute(),断言第二次抛出 IllegalStateException。关键输入是同一个实例而不是两个实例;断言对象是入口的 mStatus 检查,而不是后台线程是否已经开始。

java
AsyncTask<Void, Void, Void> task = new AsyncTask<Void, Void, Void>() {
    @Override protected Void doInBackground(Void... ignored) {
        return null;
    }
};
task.execute();
assertThrows(IllegalStateException.class, () -> task.execute());

这个实验不能证明后台任务完成,也不能证明 onPostExecute() 已运行;它只覆盖一次性状态门禁。

10.2 取消输入 ​

源码文件:frameworks/base/core/java/android/os/AsyncTask.java

对应符号是 cancel()、isCancelled() 与 finish()。

让 doInBackground() 在循环中记录线程 ID,并周期性检查 isCancelled();主线程调用 cancel(true) 后等待 onCancelled()。应断言取消回调发生在回调 Looper,后台循环是否提前 结束则取决于检查点和阻塞操作。

java
final CountDownLatch cancelled = new CountDownLatch(1);
AsyncTask<Void, Void, Void> task = new AsyncTask<Void, Void, Void>() {
    @Override protected Void doInBackground(Void... ignored) {
        while (!isCancelled()) {
            Thread.yield();
        }
        return null;
    }

    @Override protected void onCancelled(Void ignored) {
        assertEquals(Looper.getMainLooper(), Looper.myLooper());
        cancelled.countDown();
    }
};
task.execute();
task.cancel(true);
assertTrue(cancelled.await(2, TimeUnit.SECONDS));

这个输入证明的是协作式取消和回调线程;它不证明任意网络、文件或数据库调用都会响应中断。

10.3 源码搜索 ​

在源码树中复查主线时,先从状态和两个消息常量入手,再扩展到执行器,能避免把回调顺序误读成 线程池顺序:

bash
rg -n "executeOnExecutor|mStatus|mTaskInvoked|mCancelled|finish\(" \
  frameworks/base/core/java/android/os/AsyncTask.java

rg -n "SERIAL_EXECUTOR|THREAD_POOL_EXECUTOR|sRunOnSerialPolicy|SynchronousQueue" \
  frameworks/base/core/java/android/os/AsyncTask.java

rg -n "MESSAGE_POST_RESULT|MESSAGE_POST_PROGRESS|sendToTarget|handleMessage" \
  frameworks/base/core/java/android/os/AsyncTask.java

11. 替代边界 ​

源码文件:frameworks/base/core/java/android/os/AsyncTask.java

类注释直接指出它存在的边界:它只是 Thread 与 Handler 的辅助类,不是通用线程框架;短任务 适用,长时间运行的线程应转向 java.util.concurrent 等通用工具。类本身在 Android 17 已标记 @Deprecated,注释列出的实际问题包括 Context 泄漏、配置变化后的回调丢失或崩溃、版本间行为 不一致、吞掉 doInBackground() 异常,以及相对于直接使用 Executor 缺少额外价值。

这些问题都能从本文的源码边界落地理解:任务对象不认识 Activity 生命周期,取消不会等待,回调 依赖一个仍然存活的 Looper,全局串行执行器会让无关任务互相排队,异常也没有提交者可读取的 Future 结果。替代方案的选择应从需求反推:需要线程归属时使用明确的 Handler/Executor, 需要结果和取消协议时使用 Future 或应用自己的任务状态,生命周期关联则由上层组件负责。

当读者再次看到旧代码中的 AsyncTask,可以按四个问题判断风险:谁持有任务对象,哪个执行器 真正运行 doInBackground(),哪个 Looper 消费结果消息,以及取消后哪些工作仍可能继续。只要 这四个答案都能回到源码字段和调用链,便不会把“回调 API”误当成完整的生命周期管理器。