Android AsyncTask 详解

Android AsyncTask 详解

内容划分

  • AsyncTask简介
  • 简单使用
  • 繁杂部分和源码浅析
  • 一些坑的地方

AsyncTask简介

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.

这是Google Android 开发文档上关于AsyncTask的介绍,大概意思是AsyncTask设计为一个对于Thread和Handle的辅助类,主要让开发者方便的使用UI Thread和后台Thread的操作( 比如在后台线程下载文件,同时要在UI线程更新下载进度 )。同时这不是一个通用的多线程编程框架,他被设计为用于能够在 最多几秒的时间内返回结果的任务。

简单使用

这里我们模拟一个后台下载一些文件,并在用户界面显示一个ProgressDialog来显示下载进度的功能。

/**
 * author: zyinux
 * date: on 2018/10/26
 */
public class DownloadTask extends AsyncTask<String,Integer,Boolean> {

    ProgressDialog progressDialog;

    Context context;

    public DownloadTask(Context context) {
        this.context=context;

    }

    @Override
    protected void onPreExecute() {
        progressDialog=new ProgressDialog(context);
        progressDialog.setMax(100);
    // progressDialog.setCancelable(false);
    //注意这里我将上一行代码注释掉,使得dialog能够被取消,至于为什么这么做后面解释
        progressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
        progressDialog.show();
    }

    @Override
    protected Boolean doInBackground(String... strings) {
        int pro=0;

        while (true){
            pro++;
            publishProgress(pro);
            if (pro>=100){
                break;
            }
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        return true;
    }

    @Override
    protected void onProgressUpdate(Integer... values) {
        progressDialog.setProgress(values[0]);
    }

    @Override
    protected void onPostExecute(Boolean aBoolean) {
        progressDialog.dismiss();
        if (aBoolean){
            Toast.makeText(context,"下载成功",Toast.LENGTH_SHORT).show();
        }else {
            Toast.makeText(context,"下载失败",Toast.LENGTH_SHORT).show();
        }
    }
}

下面是Activity中调用的主要代码

new DownloadTask(this).execute("testurl");
//使用非常简单,new 之后执行execute传入执行的参数即可

运行效果如图


image

很普通的效果,下面来分析下上面的代码
首先是

public class DownloadTask extends AsyncTask<String,Integer,Boolean>

这一行泛型尖括号里的三个类型,具体对应三个

  • Params 执行时发送给任务的参数的类型
  • Progress 后台执行过程进度的类型
  • Result 执行结果返回值的类型
    当不需要这些参数的时候可以设置为<Void,Void,Void>
    接着是实现的几个主要方法
protected void onPreExecute() {}
protected Boolean doInBackground(String... strings) {}
protected void onProgressUpdate(Integer... values) {}
protected void onPostExecute(Boolean aBoolean) { }
  • onPreExecute 方法执行在UI线程,会在做后台任务之前调用,可以在这里执行一些初始化操作,例如上面的显示Dialog
  • doInBackground 改方法执行在后台线程,任务中的耗时操作都应该在这里执行,AsyncTask内部维持了一个线程池,来对该方法调用执行优化。该方法的参数类型就是上面设置的 Params ,也就是执行调用代码中execute里传递来的参数。在该方法内部可以调用publishProgress方法来传递当前的进度。
  • onProgressUpdate 在publishProgress方法后,系统会调用该方法,该方法运行在UI Thread,所以可以在这里做UI更新的操作,比如更新ProgressDialog的进度。这里传递的参数的类型就是上文里的 Progress。
  • onPostExecute 在doInBackground方法执行完成后会执行该方法,同样运行在UI Thread。而传入的参数就是doInBackground方法的返回值,该类型由上文的Result指定。

繁杂部分和源码浅析

上面基本讲解了AsyncTask的使用方法了。细心的小伙伴可能注意到我上面的这两句代码

// progressDialog.setCancelable(false);
//注意这里我将上一行代码注释掉,使得dialog能够被取消,至于为什么这么做后面解释

现在来解释这里这么写的原因,假设我们运行app,并执行DownloadTask,这时候屏幕上弹出一个进度框,目前为止一切都没有问题。这个时候我们点击屏幕的其他地方,进度框会被取消掉,接着我们再次执行DownloadTask,小伙伴们猜猜现在会发生什么?

由于不太方便录屏和传gif图,我这里就简单说下会发生的事情:进度框会再度弹出,这没什么问题,但是进度条会停留在0%不动,直到一段时间之后弹出Toast显示下载完成,接着进度条开始慢慢增加,当达到百分之百时再次弹出Toast提示下载完成。

为什么会这样?

首先我们知道,取消dialog并不会取消掉AsyncTask,所以再次执行DownloadTask时,相当于此时有两个AsyncTask任务在执行。这就引出了一个问题,多个AsyncTask执行时是串行还是并行?

串行还是并行?

先说答案,默认是串行的,为什么,我们来看源码。
当执行 new DownloadTask(this).execute("testurl"); 后:

    @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) {
        //看这里我们直到onPreExecute方法一定在doInBackground方法之前调用,并且是在UI Thread
        onPreExecute();
        /**
        *具体执行方法在这里 我们直到这个exec就是上一步传进来的sDefaultExecutor 
        *这是一个用来管理线程池的框架
        */
        exec.execute(mFuture);
        return this;
    }

    初始化的地方,重点这里初始化为static final 是一个类静态变量,是类实例共享的
    public static final Executor SERIAL_EXECUTOR = new SerialExecutor();    

    继续看这个SerialExecutor
    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);
            }
        }
    }

    将任务保存在ArrayDeque中,这是一个FIFO的队列,最后执行这个队列中的每一个任务。所以当执行多个AsyncTask时,他们是串行执行的。
上面说了这时一般情况,那么特殊情况呢?
DownloadTask task=new DownloadTask(this);
task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);

这时候多个任务就不是一定排队按顺序执行了,具体执行顺序要看系统对线程的调度了,小伙伴们可以自己测试一下看看。不过一般不推荐这么使用,除非你有特殊需求。

一些坑的地方

关于cancel方法

public final boolean cancel(boolean mayInterruptIfRunning) {}

传入的参数表示当前任务执行时是否可以取消。但是当你的doInBackground方法中执行一个循环或者一个IO流读写任务,即使你传入了true,改方法也无法取消这个任务的执行。区别在于调用这个方法后,doInBackground执行完成时会调用onCancelled方法,而不是onPostExecute方法,所以cancel无法保证任务能够被取消

内存泄漏

上面的示列代码从Activity中传入了一个context。而AsyncTask的生命周期和Activity是无关的,那么当Activity被finish后,AsyncTask依然存在,而他持有着Activity的引用导致Activity无法被垃圾回收。
同时如果使用了非静态匿名内部类来实现AsyncTask,由于Java内部类的特点,他同样会持有当前Activity的引用造成内存泄漏。

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