Android多线程-AsyncTask的使用和问题(取消,并行,屏幕切换)

AsyncTask是Android提供的一个执行异步工作的类,内部其实是运用了线程池和Handler来进行异步任务的执行和与主线程的交互。AsyncTask只是一个辅助类,适合执行时间短的异步任务。

本文基于Android7.0的代码来说的。

原文地址 http://blog.csdn.net/qq_25806863/article/details/72782050

示例

AsyncTask的使用方法是很简单的。就做一个简单的进度条。

布局是这样的:

里面有一个进度条ProgressBar pb1,开始按钮Button btn1,停止按钮Button stop1

然后实现一个AsyncTask,通过构造方法接收一个ProgressBar和Button进行操作:

public class MyAsyncTask extends AsyncTask<String, Integer, String> {
    private String TAG = this.getClass().getSimpleName();

    Button btn;
    ProgressBar pb;

    public MyAsyncTask(Button btn, ProgressBar pb) {
        this.btn = btn;
        this.pb = pb;
    }

    @Override
    protected String doInBackground(String... params) {
        String result = "完成";
        for (int i = 1; i <= 10; i++) {
            try {
                Log.i(TAG, "doInBackground: "+i);
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            publishProgress(i);
        }

        return result;
    }

    @Override
    protected void onPreExecute() {
        Log.i(TAG, "onPreExecute: 准备工作");
    }

    @Override
    protected void onPostExecute(String s) {
        btn.setText(s);
        Log.i(TAG, "onPostExecute: 回调");
    }

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

然后给两个按钮添加点击事件:

MyAsyncTask task1
task1 = new MyAsyncTask(btn1, pb1);
btn1.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        Log.i(TAG, "onClick: 开始1");
        task1.execute();
    }
});
stop1.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        Log.i(TAG, "onClick: 停止1");
        task1.cancel(true);
    }
});

1.构造参数

AsyncTask定义了三个泛型参数,在继承的时候必须填写。例如上面的AsyncTask<String, Integer, String>

在源码中定义是:

参数含义在下面的方法中会具体用到,先大致了解一下:

  • Params 启动任务的时候输入的参数类型,一般都是String类型,如填个网址啥的。 上面示例中就是String类型。
  • Progress 用来更新进度的类型。表示任务执行的进度的类型。 示例用的进度条,所以选择Integer类型。
  • Result 后台任务执行完后返回的结果, 示例中返回的也是String类型。

2.重写方法

要使用AsyncTask最少需要重写方法doInBackground。因为只有这个方法是抽象方法。

这个方法是在后台线程执行的。可以看到使用的参数类型Params就是在构造时定义的第一个类型。而返回的类型Result就是定义的第三个类型。

初次之外一般为了对任务流程进行控制还会重写下面几个方法onPreExecute,onPostExecute,onProgressUpdate

下面几个方法在AsyncTask中是空的,而且都要求在主线程中执行。

@MainThread
protected void onPreExecute() {
}
@MainThread
protected void onPostExecute(Result result) {
}
@MainThread
protected void onProgressUpdate(Progress... values) {
}
  • onPreExecute() 在异步任务开始前做的操作,
  • onPostExecute(Result result) 后台任务执行完后,通过这个方法能拿到任务返回的结果,进行处理。
  • onProgressUpdate(Progress... values) 这个表示进度变化,参数类型是构造时的第二个类型Progress。进度应该是随着任务的执行实时更新的,但是这个方法要在主线程中运行,而doInBackground是在子线程中运行,所以不能直接在doInBackground中调用onProgressUpdate方法,而是通过调用publishProgress(Progress... values)来间接调用这个方法。

3.开始任务

AsyncTask的开始有下面三种方法:

execute(Params... params)
executeOnExecutor(Executor exec,Params... params) 
execute(Runnable runnable)
  • execute(Params... params) 这个就是在示例中使用的开始任务的方式,传入指定的参数,参数类型要和构造时定义的第一个参数类型Params一样。参数可以为空的,那么在方法doInBackground(Params... params)中的参数也是空的。使用默认的线程池执行任务,会按流程执行onPreExecute,doInBackground(Params... params),onPostExecute(Result result)等方法。

  • executeOnExecutor(Executor exec,Params... params) 如果默认的线程池不能满足你的要求,可以用这个方法用指定线程池来执行任务。流程跟上面是一样的。

  • execute(Runnable runnable) 这个方法传进来的是一个Runnable类型,方法中只有一行代码sDefaultExecutor.execute(runnable)就是用默认的线程池直接执行任务,就是使用线程池了,跟前面那些重写的方法没关系。

4.停止任务

要停止任务可以调用下面的方法:

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

下面是停止的演示:

取消也有一个回调方法可以重写,这里加上,也是运行在主线程中:

@Override
protected void onCancelled() {
    Log.i(TAG, "onCancelled: 取消任务");
    btn.setText("取消了");
}

取消的问题

cancel方法被调用后,onPostExecuteonProgressUpdate方法都不会再调用了。而doInBackground方法却会一直执行下去,也就是后台任务会继续执行。

cancel(boolean mayInterruptIfRunning)这个参数mayInterruptIfRunning文档中表示是否应该立即终止doInBackground中的任务。

然而实际用起来就不是那样的了,无论我们传的是true还是false,而AsyncTask的cancle方法只是打上了一个取消的标记。并不是直接终止任务。如果是true,则会调用一下后台线程的interrupt方法。

当调用了cancle方法后,调用isCancelled方法会返回true。在doInBackground中应该调用isCancelled来检查当前任务是否被取消,以便及时终止任务。

AsyncTask设计成这样就是为了方便更新主线程界面的,所以对用户来说,在调用了cancle方法后,后台的任务就不会在影响到主线程的界面变化了,因为后续的跟主线程交互的方法都不会再执行了。,也可以说是取消了。

而真的要及时取消doInBackground的继续运行则需要在这个方法中进行一些判断。

不做处理

不做处理也就是在doInBackground中不做判断,像下面这样。看一下输出的日志。当然界面的进度条都会停住,只要看doInBackground有没有在点击停止按钮后停下来。

protected String doInBackground(String... params) {
        String result = "完成";
        for (int i = 1; i <= 10; i++) {
            try {
                Log.i(TAG, "doInBackground: "+i);
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            publishProgress(i);
        }
        return result;
    }
  • cancle(true):

  • cancle(false):

可以看到区别是,当值为true的时候,后台线程也会跑完。但是会调用子线程的interrupt方法,而这个现在正在sleep,所以会引发InterruptedException.

而值为false的时候,没有任何变化,后台线程继续跑完。

做处理

1. 判断isCancelled

在不同的运行节点判断这个方法的值:

@Override
protected String doInBackground(String... params) {
    String result = "完成";
    for (int i = 1; i <= 10; i++) {
        try {
            if (isCancelled()){
                Log.i(TAG, "doInBackground: 被标记停止了");
                break;
            }
            Log.i(TAG, "doInBackground: "+i);
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        publishProgress(i);
    }
    return result;
}

这时调用cancle(false):

调用cancle(true)只是会多打印一个异常,一样会停止。

2.抓异常

因为调用cancle(true)的时候有可能会抛出异常,如这个例子中的InterruptedException,因此可以通过异常捕捉来实现。

@Override
protected String doInBackground(String... params) {
    String result = "完成";
    for (int i = 1; i <= 10; i++) {
        try {
            Log.i(TAG, "doInBackground: "+i);
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
            Log.i(TAG, "doInBackground: 捕捉到异常,退出");
            break;
        }
        publishProgress(i);
    }
    return result;
}

这时调用cancle(true):

并行和串行

据说AsyncTask的任务是并行还是串行执行在不同Android版本有所变化,但是从API13开始,AsyncTask的任务执行都是串行的。

何为串行,比如有下面的界面:

有两个task

private ProgressBar pb1;
private ProgressBar pb2;
private Button btn1;
private Button btn2;
private Button stop1;
private Button stop2;
private MyAsyncTask task1, task2;
@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initView();
        task1 = new MyAsyncTask(btn1, pb1);
        task2 = new MyAsyncTask(btn2, pb2);java
        btn1.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.i(TAG, "onClick: 开始1");
                task1.execute();
            }
        });
        btn2.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.i(TAG, "onClick: 开始2");
                task2.execute();
            }
        });
        stop1.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.i(TAG, "onClick: 停止1 ");
                task1.cancel(true);
            }
        });
        stop2.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.i(TAG, "onClick: 停止2");
                task2.cancel(false);
            }
        });
    }

在上面的MyAstncTask中,后台任务要执行10秒。

这里为了区分,打印开始按钮的名字来区分:

public class MyAsyncTask extends AsyncTask<String, Integer, String> {
    private String TAG = this.getClass().getSimpleName();

    Button btn;
    ProgressBar pb;
    String name;
    public MyAsyncTask(Button btn, ProgressBar pb) {
        this.btn = btn;
        this.pb = pb;
        name = btn.getText().toString();
    }

    @Override
    protected String doInBackground(String... params) {
        String result = "完成";
        for (int i = 1; i <= 10; i++) {
            try {
                Log.i(TAG, "doInBackground: "+name+" "+i);
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
                Log.i(TAG, "doInBackground: 捕捉到异常,退出");
                break;
            }
            publishProgress(i);
        }

        return result;
    }

    @Override
    protected void onPreExecute() {
        Log.i(TAG, "onPreExecute: 准备工作 "+name);
    }

    @Override
    protected void onPostExecute(String s) {
        btn.setText(s);
        Log.i(TAG, "onPostExecute: 回调 "+name);
    }

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


    @Override
    protected void onCancelled() {
        Log.i(TAG, "onCancelled: 取消任务 "+name);
        btn.setText("取消了");
    }
}

在点击第一个开始按钮之后点击第二个开始按钮,效果:

打印日志:

虽然点击了开始2,但是依然等第一个任务完成了才开始第二个任务。

想要让任务并行执行怎么办呢?其实他之所以会串行执行任务,是因为内部默认的线程池中将任务进行了排队,保证他们一个一个来。只要我们换个满足要求的线程池来执行任务就行了。AstncTask内部就有一个线程池AsyncTask.THREAD_POOL_EXECUTOR可以使用。当然,用Executors来创建也行。

然后将开始任务的execute(Params... params)方法改为executeOnExecutor(Executor exec,Params... params).这里用AsyncTask.THREAD_POOL_EXECUTOR.

task1.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
task2.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);

效果:

日志:

屏幕横竖屏切换

使用AsyncTask的时候,在屏幕切换也会出现问题。

画面是这样的:

日志是这样的,动图中也能看见:

虽然屏幕切换后,任务也在执行,也在不停地调用更新进度条的方法,最后也执行了onPostExecute方法,但是界面上就是什么变化都没有。

因为在横竖屏切换的时候,Activity会销毁重建,所以AsyncTask所持有的引用就不是新建的Activity的控件了,新的Activity就不会变化了。

其中一种解决方法

很简单,加上这句就行了。

这时屏幕怎么切换都没事

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

推荐阅读更多精彩内容