Android 异步技术-AsyncTask

简介

AsyncTask是Android系统提供的一个轻量级的异步工具类,基于Executor框架封装, 官方对其的定义如下:

AsyncTask enables proper and easy use of the UI thread. This class allows you to perform background operations and publish results on the UI thread without having to manipulate threads and/or handlers.
AsyncTask is designed to be a helper class around Thread and Handler and does not constitute a generic threading framework. AsyncTasks should ideally be used for short operations (a few seconds at the most.) If you need to keep threads running for long periods of time, it is highly recommended you use the various APIs provided by the java.util.concurrent package such as Executor, ThreadPoolExecutor and FutureTask.

从官方的定义可以看出AsyncTask是被设计用来更容易的使用UI线程,允许在后台线程完成耗时的操作,然后把结果发布到UI线程中,它并不是一个通用的异步框架,所以一般情况下,只应用于一些最多几秒钟简短的操作,如果后台线程需要运行更长的时间,则应该考虑直接使用线程池。

优势
  • 方便的实现了UI线程和后台线程的异步通信,不需要自己通过使用Thread+Handler这种较为复杂的组合实现
  • 基于线程池实现,在应用范围内所有的AsyncTask复用了统一的线程池资源,避免了频繁的线程创建/销毁带来的开销
不足
  • 使用不当易导致内存泄漏

如果AsyncTask被声明为Activity非静态的内部类,由于Java的非静态内部类默认会持有一个外部类的引用,此时如果activity被关闭,而AsyncTask仍然在执行后台操作的话,会导致activity未被回收,导致内存泄漏。

  • 结果丢失

当activity因为配置变更(例如旋转屏幕)导致重建时,之前运行的AsyncTask会持有重建之前的activity的引用,此时的结果回调是无法修改UI的。

  • 生命周期

AsyncTask不会随activity的销毁自行销毁,在关闭activity时需正确的取消任务。

  • 在不同版本中任务的 并行/串行 执行顺序的表现不一致,官方对其任务执行顺序表述如下:

When first introduced, AsyncTasks were executed serially on a single background thread. Starting with Build.VERSION_CODES.DONUT, this was changed to a pool of threads allowing multiple tasks to operate in parallel. Starting with Build.VERSION_CODES.HONEYCOMB, tasks are executed on a single thread to avoid common application errors caused by parallel execution.
If you truly want parallel execution, you can invoke executeOnExecutor(Executor, Object[]) with THREAD_POOL_EXECUTOR.

从官方说明可以看出,在API版本低于11(Android 3.0)的时候,AsyncTask的任务执行顺序在不同的版本是不一样的,有些版本是串行执行,有些版本是并发执行,所以如果应用仍需兼容低版本的系统,在使用AsyncTask的时候就需要仔细考虑任务的执行顺序,必要时需要针对不同的系统做兼容处理,不过目前应该很少有应用还需要兼容这么老的系统了,所以基本可以忽略此差异。

基础使用

AsyncTask是一个抽象类,声明如下:

public abstract class AsyncTask<Params, Progress, Result> { 
 ... 
}

// 类中参数为3种泛型类型
// 整体作用:控制AsyncTask子类执行线程任务时各个阶段的返回类型
// 具体说明:
    // a. Params:开始异步任务执行时传入的参数类型,对应excute()中传递的参数
    // b. Progress:异步任务执行过程中,返回下载进度值的类型,不需要进度可使用java.lang.Void
    // c. Result:异步任务执行完成后,返回的结果类型,与doInBackground()的返回值类型保持一致
}

在使用时首先需要实现自己的业务子类,例如:

public static class MyTask extends AsyncTask<String,Integer,String>{
        @Override
        protected void onPreExecute() {
            // 运行线程:主线程
            // 是否必须:否
            // 方法说明:任务执行前调用,可在此做一些后台任务执行前的准备工作,例如UI提示等
        }

        @Override
        protected String doInBackground(String... strings) {
            // 运行线程:后台线程
            // 是否必须:是
            // 方法说明:执行后台耗时任务,接收输入的不定长参数列表,返回任务执行结果
            return null;
        }

        @Override
        protected void onPostExecute(String s) {
            // 运行线程:主线程
            // 是否必须:否
            // 方法说明:接收后台任务执行结果,用于将后台执行结果刷新到界面中
        }

        @Override
        protected void onProgressUpdate(Integer... values) {
            // 运行线程:主线程
            // 是否必须:否
            // 方法说明:后台任务执行进度通知,在执行后台任务时调用 publishProgress(Progress... values) 方法触发。

        }

        @Override
        protected void onCancelled(String s) {
            // 运行线程:主线程
            // 是否必须:否
            // 方法说明:当cancel(boolean)方法被调用,并且doInBackground(String... strings)执行完毕时被调用,接收的参数为后台线程执行结果
            //         如果复写此方法,则不应调用super.onCancelled(s)
        }

        @Override
        protected void onCancelled() {
            // 运行线程:主线程
            // 是否必须:否
            // 方法说明:当cancel(boolean)方法被调用,并且doInBackground(String... strings)执行完毕时被调用,忽略后台线程执行结果
        }
    }

业务子类实现完之后直接提交任务执行即可:

MyTask myTask = new MyTask();
myTask.execute("params1","params2","params3");

需要注意的是,通过execute方法提交的任务,在3.0版本之后,一个应用内所有的AsyncTask提交的任务默认都是串行执行的,如果需要任务并发执行,则需要调用executeOnExecutor方法,传入线程池,线程池可以使用AsyncTask提供的AsyncTask.THREAD_POOL_EXECUTOR,例如:

myTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR,"params1","params2","params3");

实现原理

本节基于Android 10源码分析 AsyncTask的具体实现原理,在简介中有提到,AsyncTask是基于Executor框架封装封装的一个轻量级的异步工具类,也就是说在它的内部是通过线程池来实现的线程调度和复用,另外为了将工作线程的执行结果通知到主线程,还使用了Android的Handler机制处理工作线程和主线程之间的通信问题。

线程池

通过上面 的介绍,我们已经知道了AsyncTask是使用线程池来执行具体的任务的,那我们首先来看看它内部线程池相关的部分:

private static final int CORE_POOL_SIZE = 1;
private static final int MAXIMUM_POOL_SIZE = 20;
private static final int KEEP_ALIVE_SECONDS = 3;
/**
 * An {@link Executor} that can be used to execute tasks in parallel.
 */
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;
}

THREAD_POOL_EXECUTOR就是AsyncTask内部用来执行任务的线程池,可以看到它被声明为静态的,也就是说在同一个进程内部,所有的AsyncTask都是使用的同一个线程池,具体的相关配置参数为:核心线程数=1,最大线程数=20,非核心线程最长存活时间=3S,采用直接提交队列。即该线程池只有一个核心线程,当有多个任务同时提交时,会最多开启20个行程去并发执行任务,当有更多的任务被提交时,会执行自定义的拒绝策略sRunOnSerialPolicy

private static final int BACKUP_POOL_SIZE = 5;

// Used only for rejected executions.
// Initialization protected by sRunOnSerialPolicy lock.
private static ThreadPoolExecutor sBackupExecutor;
private static LinkedBlockingQueue<Runnable> sBackupExecutorQueue;

private static final RejectedExecutionHandler sRunOnSerialPolicy =
        new RejectedExecutionHandler() {
    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        android.util.Log.w(LOG_TAG, "Exceeded ThreadPoolExecutor pool size");
        // As a last ditch fallback, run it on an executor with an unbounded queue.
        // Create this executor lazily, hopefully almost never.
        synchronized (this) {
            if (sBackupExecutor == null) {
                sBackupExecutorQueue = new LinkedBlockingQueue<Runnable>();
                sBackupExecutor = new ThreadPoolExecutor(
                        BACKUP_POOL_SIZE, BACKUP_POOL_SIZE, KEEP_ALIVE_SECONDS,
                        TimeUnit.SECONDS, sBackupExecutorQueue, sThreadFactory);
                sBackupExecutor.allowCoreThreadTimeOut(true);
            }
        }
        sBackupExecutor.execute(r);
    }
};

在自定义的拒绝策略里,为了节约系统资源,采用延迟初始化的方式加载了一个线程池sBackupExecutor,并用其内部的对象锁保证只初始化一次,这个线程池被定义为核心线程数和最大线程数都是5,并采用无界队列,同时允许核心线程超时销毁。

任务调度-串行

在内部线程池的代码中并没有看到任何的同步代码,那么AsyncTask是通过什么方式保证了任务的串行执行呢?直接看在使用中调用的串行执行方法execute的代码:

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

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方法
    onPreExecute();
    // 保存参数
    mWorker.mParams = params;
    // 执行任务
    exec.execute(mFuture);
    return this;
}

可以看到,execute内部还是调用的是executeOnExecutor方法,可是这个方法在上面使用部分中介绍的明明是用于执行并发逻辑的,此处的串行又是怎么实现的呢?其实关键就在于传入的线程池参数sDefaultExecutor,正是由它保证了所有任务的串行执行,下面看看它的实现

/**
 * An {@link Executor} that executes tasks one at a time in serial
 * order.  This serialization is global to a particular process.
 */
public static final Executor SERIAL_EXECUTOR = new SerialExecutor();
// 注意,此处为静态实例,也就是说同一个进程内,所有的AsyncTask都使用同一个Executor,并且使用了volatile修饰,保证了多线程环境中的可见性
private static volatile Executor sDefaultExecutor = SERIAL_EXECUTOR;
private static class SerialExecutor implements Executor {
    // 双向队列,用于存储所有的任务
    final ArrayDeque<Runnable> mTasks = new ArrayDeque<Runnable>();
    Runnable mActive;

    // 使用synchronized锁保证多任务执行时是串行执行
    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
            THREAD_POOL_EXECUTOR.execute(mActive);
        }
    }
}

通过源码分析可以对AsyncTask串行执行流程总结如下:

定义了一个进程内全局的串行任务调度器sDefaultExecutor,在这个调度器内使用队列缓存了所有通过execute方法提交的任务,并且使用synchronized关键字对保证在多线程环境下任务的提交与执行是串行进行的,但是sDefaultExecutor并不提供具体的执行任务的能力,任务是是由默认的线程池THREAD_POOL_EXECUTOR执行的。

任务调度-并发

在理解了串行执行的逻辑之后,并发的处理逻辑就很简单了,在基础使用一节中有提到如果需要并发执行,只需调用executeOnExecutor方法,传入线程池,线程池可以使用AsyncTask提供的AsyncTask.THREAD_POOL_EXECUTOR,由于传入的线程池并没有使用任何的同步方法,所以提交的任务自然就根据线程池的配置并发的执行了。

任务执行与线程通信

下面是AsyncTask的初始化方法:

public AsyncTask(@Nullable Looper callbackLooper) {
    mHandler = callbackLooper == null || callbackLooper == Looper.getMainLooper()
        ? getMainHandler()
        : new Handler(callbackLooper);

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

    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);
            }
        }
    };
}

可以看到首先初始化了一个Handler,用于线程间的通信,然后初始化了一个WorkerRunnable对象,这个对象实现了Callable接口,同时存储了任务提交时的参数信息,最后WorkerRunnable对象又作为参数传给了FutureTask,用于等待任务执行完成,通知结果,而这个FutureTask对象正是在任务调度中,最终被提交给线程池运行的task,所以整个的任务执行的逻辑可以总结如下:

  1. 业务层通过调用execute/executeOnExecutor方法传入任务执行参数
  2. 使用WorkerRunnable对象包装传入的业务层参数,
  3. 提交FutureTask到线程池中执行
  4. WorkerRunnable对象的 call 方法被调用,回调到业务层实现的doInBackground方法
  5. doInBackground方法执行完毕时,FutureTask对象的done方法被回调,通过handler机制回调结果到主线程

参考

Android 多线程:手把手教你使用AsyncTask

Android 多线程:AsyncTask的原理 及其源码分析

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容