分类目录归档:多线程开发

多线程开发 第5节 AsyncTask

更新时间 修改意见
2016-08-02 陈敏

第5节 AsyncTask

首先举个例子,获取手机上视频信息所需要的时间是个不能确定的事情。如果视频很少,也许几十毫秒就能完成,如果视频很多(比如几十个),也许就要花二十多秒。

安卓应用只有一个主线程-各个组件都是在这个线程中运行。作为组件的之一的Activity就是在这个线程中更新应用界面的,例如,用户点击界面上的一个按钮,按钮得到响应,整个过程就是在这个主线程里。所以这个主线程绝对不可以做耗时的操作。假如在按钮中做了耗时的操作,那么当它进行耗时操作的时候,你去点击界面上的其它按钮是不会有反应的,就好像程序冻在了那里。

代码的执行一旦连续占用这个线程超过一定的时间,系统就会弹出“程序无响应的”提示。

因此,我们可以考虑把获取视频信息的操作放到一个单独的线程thread中进行。

这就好比你在正在做一件事情A,突然另一件事情B来打扰你,你不得不停下手头的工作来完成,做完了才能继续之前的工作;这时如果有另外一个人(另一个线程)来帮助你,把事情B全部包揽了,那你就不用分心了。当另一个人把事情B做完后,告诉你一声就可以了。

5.1 异步操作

启动一个新的线程,分担耗时工作的方法是一种异步操作:我让你帮我做一件事情,布置任务后,我就去做其他的事情了,等你做完了再告诉我结果;

与它对应的是同步操作:我让你帮我做一件事情,布置任务后,我啥也不做,就等着你做完了告诉我结果;

获取视频信息是个异步操作,启动一个新线程-工作线程thread-查询视频信息,查询完成后,工作线程再将结果通知到主线程,让主线程将查询到的结果显示到界面上。界面的更新一定要在主线程中进行,不能在别的线程修改,否则系统会提示运行错误,这一点相当重要。因此我们一定要将查询的结果发送给主线程,让主线程处理界面的更新。

5.2 异步操作的方案

安卓系统提供的异步操作方案有:

  1. 创建工作线程thread和Handler,利用Handler在工作线程和主线程之间传递数据;
  2. 使用AsyncTask帮助类,AsyncTask中封装了工作线程,通过AsyncTask完成工作线程和主线程之间的数据传递;

第1种方案,已经在第3节里面介绍过了。

这里我们介绍第2种方案。

AsyncTask实际上它也是通过方案1实现的,只不过Android SDK对这套机制做了进一步封装,让使用者用起来更加方便。

AsyncTask适用于,

  1. 使用场景简单,只是单个任务的异步操作,没有多个线程之间的数据同步考虑;
  2. 使用方便,不用考虑太多的新线程创建的细节;

5.3 AsyncTask的使用

AsyncTask需要被继承成为一个新的子类来使用,在被继承时,要指定三种参数的类型-Param Progress Result,还需要实现doInBackground(Param...)函数,此外通常还要实现onProgressUpdate(Progress...) onPostExecute(Result) 两个回调函数。

  1. doInBackground(Param... params)函数:传入参数的Param类型就是AsyncTask中指定的Param类型。它运行在新创建的工作线程当中。

    使用MyTask时,要在主线程中使用excute()方法传入不定长参数,让Task运行起来,

    不定长参数会以数组的形式传递到doInBackground()函数当中,

  2. onProgressUpdate(Progress... progresses)函数:传入参数的Progress类型就是AsyncTask中指定的Progress类型。它运行在主线程中。

    doInBackground()中执行的是一个很耗时的工作,有时需要向主线程报告当前的运行状况,这就要使用到publishProgress()函数,publishProgress()也是使用的不定长参数,

    不定长参数会以数组的形式传递到onProgressUpdate()函数当中,

  3. onPreExecute()函数:当Task开始执行的时候,通过这里告诉主线程Task要开始工作了。它运行在主线程当中。

  4. onPostExecute(Result result)函数:传入参数的Result类型就是AsyncTask中指定的Result类型。它运行在主线程当中。

    doInBackground()函数返回的类型也是Result

    返回的结果作为参数传递给onPostExecute()函数,

  5. onCancel()函数会在调用者取消AsyncTask的工作的时候被触发。它运行在主线程当中。

    要取消AsyncTask的工作,首先要在主线程中调用cancel()方法,

    因为在doInBackground()中执行的是一个很耗时的工作,需要时不时的检查自己是否被取消执行了,

    最后,onCancelled()函数会被触发,这个函数会在主线程中被执行,

    一个AsyncTask只能被excute()一次,一旦运行之后,就不能重复的对它再次调用excute()了。


综合上面的分析,自定义一个AsyncTask的方法如下,

使用一个AsyncTask的方法如下,


/**************************************************************************/
* 版权声明
* 本教程只在CSDN安豆网发布,其他网站出现本教程均属侵权。
/
**************************************************************************/

多线程开发 第4节 HandlerThread

更新时间 修改意见
2016-08-02 陈敏

第4节 HandlerThread

3.1章节介绍了Thread、Looper与Handler的关系。为了让开发者能简单的使用具备LooperThread,而不需要开发者写代码组合它们,Android SDK提供了HandlerThread

HandlerThread的原理很简单,

  1. 创建一个线程A;
  2. 在A上创建一个Looper

这样这个新创建的线程就可以不停的接收和处理任务了。

4.1 HandlerThread的使用

  1. 创建并启动HandlerThread

  2. 获取可以访问HandlerThread对象任务队列的Handler;把Thread的Looper指定给Handler

  3. 使用HandlerHandlerThread对象布置任务;

  4. HandlerThread对象退出;

4.2 HandlerThread与Handler、Thread名称区分

HandlerThread与Handler、Thread名字相似,这里再做一下澄清。

  • Thread:线程,单纯的一个线程;
  • HandlerThread:具备任务队列的线程,它就是一个线程;
  • Handler:可以访问指定线程任务队列的一个工具;

/**************************************************************************/
* 版权声明
* 本教程只在CSDN安豆网发布,其他网站出现本教程均属侵权。
/
**************************************************************************/

多线程开发 第3节 Handler

更新时间 修改意见
2016-08-02 陈敏

第3节 Handler

多个线程之间除了有“执行的同步”关系,还有“数据的共享”关系,以及“功能的委托”关系。

例如之前提到的视频信息显示到列表上,

  1. 委托数据查询功能:主线程启动一个工作线程thread-查询视频信息;
  2. 委托数据的界面更新功能:工作线程查询完成后,因为不能够修改界面元素,所以必须将结果通知到主线程,委托主线程将得到的结果显示到界面上。

为此,Android SDK提供了Handler帮助各个不同的线程之间传递数据或委托功能处理。

3.1 Thread、Looper与Handler的关系

一个线程创建并start以后,就会开始执行它Runnable中的run()方法。如果要这个线程一直运行而不退出的话,就要在里面设置一个循环,

Looper可以为Thread创建这样一个循环的环境,

Looper具有一个消息队列,可以存放任何线程(包括它自己)给自己布置的任务。这些任务被一条一条的放在队列当中,在loop()函数中被取出来-执行,然后又取出来-执行,周而复始,永不停止。

Handler是和Looper相关联的,通过Handler,任何线程可以把需要完成的任务放到Looper的消息队列里面。

Thread就好比一条产线,Looper中的消息队列就是这个流水线上的传送带,带子上分成了很多格,每一格放要处理的原料和处理这些原料的工人(原料和工人打包成了一个Message)。等轮到格子上的工人时,工人才能开始处理格子里放的原料。Handler就像是一个转运工具,提供给别的模块使用。这个工具可以把原料和工人放到产线的传送带上。

注意,一条产线只有一条传送带(Looper);但可以有多个为同一条产线提供转运服务的转运工具(Handler)。

所以使用这种产线的流程是,

  1. 创建一条产线A;
  2. 在这条产线A上创建一条传送带;
  3. 当别的模块B要向这条产线布置任务的时候,就要创建在产线A上工作的工人;
  4. B要告诉工人携带哪些原料,怎么处理这些原料;
  5. 等轮到产线A上对应的格子被处理的时候,上面的工人就开始操作了;

3.2 Handler的使用

Handler最常见的使用场景是:

  1. 为了进行耗时操作,主线程创建一个工作线程完成耗时操作;
  2. 工作线程开始耗时工作,时不时向主线程发送消息,告知当前完成的状态;
  3. 主线程根据工作线程的报告,更新界面元素,展示工作进度;

为了实现这样的功能,我们首先需要知道,

  1. 每个Activity都是运行在程序的主线程当中的;
  2. 只有主线程能修改界面元素,其他线程修改的话,应用会崩溃;
  3. 主线程在创建之后,系统已经为它设置了Looper,主线程已经有了消息处理的队列(生产流水线和流水线上的格子);

Handler处理这种场景时,我们有2种方式。

3.2.1 使用sendMessage()

  1. 创建一个能向主线程消息队列中布置任务的Handler;如果创建时直接new Handler(),说明这个Handler是放在主线程的消息队列中的“工人”;
  2. 重写HandlerhandleMessage()函数;
  3. handleMessage()函数中,根据msg.what的不同,进行对应的处理;

任何想要布置任务的线程只需要利用HandlersendMessage()函数,就能把任务放到主线程的消息队列中,

这样主线程的消息队列中就有了这个新任务。等到这个消息被处理的时候,主线就可以根据参数修改界面元素了。

除了使用

也可以使用

这里使用的Message就携带了“工人”和“原料”,Message通过Handler对象获取,

发送Message可以直接发送,也可以延时发送,

3.2.2 使用post()

  1. 创建一个能向主线程消息队列中布置任务的Handler;如果创建时直接new Handler(),说明这个Handler是放在主线程的消息队列中的工人

  2. 在工作线程中使用Handlerpost()方法,里面的Runnable就是在Handler所服务线程中运行;

还可以使用

3.3 Handler任务的移除

大多数情况下,当Activity退出以后,需要将它布置给主线程的任务给移除掉。如果不移除,可能会遇到大麻烦。可以想象一下,

  1. 工作线程通过Handler给主线程布置了一个任务-根据一个参数修改界面显示,此时这个任务已经放到了主线程的任务队列里面;
  2. 用户突然退出了这个Activity,Activity被销毁了,但是主线程是不会退出的(Activity只是主线程上长的一个果子,果子被摘了,但是不会影响树的存在);
  3. 主线程的任务队列依次执行到了工作线程布置的任务,任务要求更新Activity上的界面元素,但是这个Activity已经被销毁了;

这时,程序的行为表现有可能和你希望的不同,出现一些与期望不符的现象,严重时,可能会造成程序崩溃。所以当一个Activity退出销毁的时候,一定要把相关的任务移除。这样做还有助于避免内存泄露。

3.3.1 使用removeMessages()

通过它移除任务队列中所有特定Message,

3.3.2 使用removeCallbacks()

通过它移除任务队列中所有特定Runnable,

这里的mHandlerRunnable就是之前post()方式使用的mHandlerRunnable,所以在使用post()方式的时候,我们要把Runnable的引用保存起来,以便以后的移除操作。


/**************************************************************************/
* 版权声明
* 本教程只在CSDN安豆网发布,其他网站出现本教程均属侵权。
/
**************************************************************************/

多线程开发 第2节 线程Thread

更新时间 修改意见
2016-08-02 陈敏

第2节 线程Thread

一个Activity运行在一个主线程上,它不能进行耗时巨大的操作,也不能执行耗时不确定的操作。因此需要在新的线程中进行这些耗时的操作,这种进行耗时操作的线程称作工作线程。

2.1 Thread的简单使用

创建一个新的线程,

  1. 创建一个Runnable,重写它的run()函数,这个函数里用来进行耗时的操作;

  2. Runnable为参数,创建一个Thread,调用Threadstart()方法后,新线程就运行起来了,并执行Runnable中的run()函数;

  3. run()函数执行完毕后,新线程就退出;

在线程执行耗时操作的过程中,有时要取消这个线程的操作,

  1. 最好的办法是在run()函数中增加一个标志位,让工作线程可以检查这个标志位是否被设置上,如果设置上了,就让run()函数,立即返回,

  2. 如果主线程要取消这个线程的工作,修改这个标志位就好了,

2.2 Thread的创建

Thread可以拥有不同的优先级,从低到高有10级。操作系统根据线程的优先级来进行资源调度,优先为优先级高的线程分配CPU资源。在默认情况下,新创建的线程使用默认的优先级NORM_PRIORITY。设置优先级的方式很简单,

可以为线程取名字,当我们用Android Monitor工具调试应用的时候,就能看到创建线程时它的名字,方便我们观察、调试程序,

有时,一个应用会同时启动多个Thread,在创建它们的时候可以为它们设置ThreadGroup参数,将它们分成一组,便于对这些线程进行统一的管理。比如,中断这个组里所有线程的运行;

2.3 Thread的停止

Thread的停止就是指这个线程的退出。线程的退出原因无外乎两种,

  1. 工作线程的工作完成了;
  2. 工作线程虽然正在进行耗时工作,但是被取消了,要提前结束;

2.3.1 正常退出

Runnable中的run()函数执行完并返回后,当前的Thread就退出了。

2.3.2 使用标志位

  1. run()函数中增加一个标志位,工作线程会随时检查这个标志位是否被设置上,如果设置上了,就让run()函数,立即返回,

  2. 如果主线程要取消这个线程的工作,修改这个标志位就好了,

2.3.3 使用interrupt()方法

interrupt()是线程提供的一个标准的线程退出方法,如果当前的工作线程正被Object.waitThread.joinThread.sleep阻塞,那么使用thread.interrupt()之后,正在运行的线程会抛出一个InterruptedException异常,

不过interrupt()方法并不是万能的,不是所有阻塞情况下都能够让线程立即退出。

例如当该线程正在用ServerSocketaccept()方法等待连接的时候,即使调用了这个工作线程的interrupt()方法,该线程还是不会抛出异常的。

它只对Object.waitThread.joinThread.sleep这几种阻塞有效果。对于网络读取数据时阻塞状态解除是没有效果的。

*对于网络读取数据时造成的阻塞,我们会在以后相应的章节详细介绍解决方法。

2.4 线程之间的同步

线程间的同步就是指线程A执行到一个地方的时候,停了下来,等待线程B的执行结果;等线程B执行出结果后,线程A才能继续执行。

2.4.1 join()方法

最常见的就是主线程的执行,依赖于工作线程的退出。

主线程启动了工作线程以后,有时候需要等到工作线程结束以后再进行接下来的操作。

例如一个Activity,在onCreate()的时候创建了一个工作线程Thread B;后来在用户的要求下,Activity退出,要被销毁了;销毁Activity时,主线程要等到Thread B执行完了才能继续接着进行剩下的清理工作,那么Activity可以在它的onDestroy()函数中可以使用join()方法,等待子线程的结束,

join()方法,会一直处于阻塞状态,直到线程B退出。

onDestroy()中使用join()有天生的缺点:不能在主线程中进行耗时不可控的操作(例如这里等待工作线程执行完毕),万一工作线程的退出花费了很长的时间,那就有问题了。这里的场景只是用来举一个例子而已。

2.4.2 wait()方法

利用Objectwait()方法实现线程间的同步,需要线程之间共享一个“锁”-Object对象。

当主线程A执行到一个阶段的时候,如果要等待线程B,就把锁置于等待状态,

此时,线程A就出于阻塞的状态,不能往下执行了。

线程B继续它的运行,当它执行到一个阶段的时候,将锁设置成放行状态,

此时,线程A的阻塞状态解除,可以继续往下执行了。


/**************************************************************************/
* 版权声明
* 本教程只在CSDN安豆网发布,其他网站出现本教程均属侵权。
/
**************************************************************************/

多线程开发 第1节 概述

更新时间 修改意见
2016-08-02 陈敏

第1节 概述

安卓应用只有一个主线程-各个组件都是在这个线程中运行。作为组件的之一的Activity就是在这个线程中更新应用界面的,例如,用户点击界面上的一个按钮,按钮得到响应,整个过程就是在这个主线程里。所以这个主线程绝对不可以做耗时的操作。假如在按钮中做了耗时的操作,那么当它进行耗时操作的时候,你去点击界面上的其它按钮是不会有反应的,就好像程序冻在了那里。

我们的代码一旦连续占用这个线程超过一定的时间,系统就会弹出“程序无响应的”提示,这个提示叫做ANR-Applicatin No Response。

这就好比你在正在做一件事情A,突然另一件事情B来打扰你,你不得不停下手头的工作来完成,做完了才能继续之前的工作;这时如果有另外一个人(另一个线程)来帮助你,把事情B全部包揽了,那你就不用分心了。当另一个人把事情B做完后,告诉你一声就可以了。

启动一个新的线程,分担耗时工作的方法是一种异步操作:我让你帮我做一件事情,布置任务后,我就去做其他的事情了,等你做完了再告诉我结果;

与它对应的是同步操作:我让你帮我做一件事情,布置任务后,我啥也不做,就等着你做完了告诉我结果;

例如,一个视频播放应用,获取视频信息所需要的时间是个不能确定的事情。如果视频很少,也许几十毫秒就能完成,如果视频很多(比如几十个),也许就要花二十多秒。

因此,我们可以考虑把获取视频信息的操作放到一个单独的线程thread中进行。启动一个新线程-工作线程thread-查询视频信息,查询完成后,工作线程再将结果通知到主线程,让主线程将查询到的结果显示到界面上。因为界面的更新一定要在主线程中进行,不能在别的线程修改,否则系统会提示运行错误。因此我们一定要将查询的结果发送给主线程,让主线程处理界面的更新。

这里就涉及到了线程的创建,工作分配,以及它们之间的配合-信息的传递。


/**************************************************************************/
* 版权声明
* 本教程只在CSDN安豆网发布,其他网站出现本教程均属侵权。
/
**************************************************************************/