线程在Android开发中的应用

时间:2022-08-13 04:59:11

线程在Android开发中的应用

摘 要:Android中线程主要是用来处理一些耗时操作,防止UI线程阻塞,并进行异步处理以提高程序的效率的线程。在Android应用开发中,要时时考虑到用户的体验效果,因此对应用程序的执行效率要有很高的要求,这就使开发人员在Android开发中无可避免地使用线程。本文主要讨论Android应用开发中线程使用的意义、使用线程的方法、主线程和子线间的通信,以及介绍了Android SDK中提供的一些有关线程的工具类。

关键字:Android;主线程;子线程;消息

中图分类号: TP311.52 文献标识码:A DOI:10.3969/j.issn.1003-6970.2013.08.008

本文著录格式:[1]纪晓阳.线程在Android开发中的应用[J].软件,2013,34(8):24-26

0 前言

Android应用程序通常是运行在一个单独的线程(如:main)里。如果我们的应用程序所做的事情在主线程里占用了太长的时间的话,就会引发ANR(Application Not Responding)对话框,因为你的应用程序长期占用着主线程,而主线程一般是用来处理用户输入事件或者Intent广播。对于ANR的概念,在Android里,应用程序的响应性是由ActivityManager[1]和WindowManager[1]系统服务监视的当它监测到以下情况中任一个时:

a. 在5秒内没有响应输入的事件(例如,按键按下,屏幕触摸);

b. BroadcastReceiver [2]在10秒内没有执行完毕。

Android就会针对特定的应用程序显示一个ANR对话框(如图1所示为一个转码应用Lame中出现的ANP)显示给用户。

所以为了避免我们的应用程序出现ANR,就要让运行在主线程里的任何方法都尽可能少做耗时操作,例如网络或数据库操作,或者高耗时的计算(如改变位图尺寸)等。这时,我们就可以开启子线程来处理这些耗时操作以避免出现ANR。同时应用程序也应该避免在BroadcastReceiver里做耗时的操作或计算。但不再是开启子线程里做这些任务(因为BroadcastReceiver的生命周期短),如果响应Intent广播需要执行一个耗时的动作的话,应用程序应该启动一个Service来处理这些耗时操作。本文是举例介绍用户输入事件无响应的问题事例,主要讨论Android 程序中线程间的通信以及Android SDK中一些使用到线程的类和方法。

1 如何创建线程

Java提供了线程类Thread来创建多线程的程序,创建线程与创建普通的类的对象的操作是一样的,而线程就是Thread类或其子类的实例对象。每个Thread对象描述了一个单独的线程。要产生一个线程,有两种方法:

a. 需要从Java.lang.Thread类派生一个新的线程类,重载它的run()方法;

b. 实现Runnalbe[2]接口,重载Runnalbe接口中的run()方法。

这两种方法的区别就是,一个是直接创建Thread对象,另外一个是需要implement了Runnable接口对象作为创建Thread对象的参数。Runnable其实我们称为线程任务。

所以为了避免图1中出现的错误,我们可以在主线程中开启一个线程来执行耗时操作(如转码方法converTarg(src,targ)),主要代码如下:

当我们开启一个子线程,在子线程中进行转码操作时,如果在转码执行过程中,用户需要与界面进行交互(如触摸手机屏幕),这时事例应用Lame就不会出现图1的ANR对话框,而是继续进行正常转码(如图2),因为我们的转码操作是在子线程中进行的,没有占用主线程。

2 线程和主线程之间的通信

Android线程与一般的Java多线程处理方式是不同的,其中重点是消息发送和计划任务,接受消息发送和计划任务的处理是目标线程,它是通过Looper[2]机制维护消息队列,如果应用中有包含更新UI处理,则要把更新UI的处理代码放置在目标线程中,这个时候还要保障更新UI的线程是主线程。

开启线程主要就是处理应用程序中的耗时操作,处理完了就要把处理后的结果返回给主线程,由主线程再做进一步处理(如更新界面数据)。为了解决类似的问题,Android设计了一个Message Queue(消息队列 ), 线程间可以通过该Message Queue并结合Handler[3]和Looper组件进行信息交换。

Message对象为线程间交流的信息,该对象内包含子线程处理后的数据。Handler对象是Message的主要处理者,负责Message的发送,Message内容的执行处理。在主线程中创建Handler对象,子线程就是通过使用该Handler对象的sendMessage(Message)方法来发送消息。而使用Handler,需要实现该类的 handleMessage(Message)方法,该方法是处理这些Message的操作内容,例如更新用户界面。通常需要子类化Handler来实现handleMessage方法。

Message Queue用来存放通过Handler的消息,按照先进先出执行。每个Message Queue都会有一个对应的Handler。Handler会向Message Queue通过两种方法发送消息:sendMessage或post。这两种消息都会插在Message Queue队尾并按先进先出执行。但通过这两种方法发送的消息执行的方式略有不同:通过sendMessage发送的是一个Message对象,会被 Handler的handleMessage()函数处理;而通过post方法发送的是一个Runnalbe对象,则会自己执行。

Looper负责对每条线程里的Message Queue的管理。Android没有全局意义的Message Queue,而Android会自动替主线程(UI线程)建立Message Queue,但在子线程里并没有建立Message Queue。所以调用Looper.getMainLooper()方法得到的主线程的Looper不为NULL,但调用Looper.myLooper() 得到当前线程的Looper就有可能为NULL。传送的信息最终谁来执行处理信息,需要判断Handler对象里面的Looper对象是属于哪条线程的,就由该线程来执行。当Handler对象的构造函数的参数为空,则为当前所在线程的Looper;Looper.getMainLooper()得到的是主线程的Looper对象,Looper.myLooper()得到的是当前线程的Looper对象。

更新UI的时候主线程必须是目标线程,此时我们通过Looper和HandlerThread达到这个目的。Android中每一个线程都跟着一个Looper,Looper可以帮助线程维护一个消息队列,Looper对象的执行需要初始化Looper.prepare()方法,使用Looper.loop()方法启动消息队列管理机制,退出时还要使用Looper.release()方法释放资源。

Android使用Looper机制才能接收消息和处理计划任务,上面的方法代码编写起来还是有点繁琐,所以Android提供了一个线程类——HanderThread类,HanderThread类继承了Thread类,它封装了Looper对象,使我们不用关心Looper的开启和释放的细节问题。HandlerThread对象中可以通过getLooper方法获取一个Looper对象引用。

不管是主线程(一般是我们的UI线程)还是子线程,只要有Looper的线程,别的线程就可以向这个线程的消息队列中发送消息和计划任务,然后做相应的处理。

3 AsyncTask的使用

Android另外提供了一个工具类:AsyncTask[4]。它使创建需要与用户界面交互的长时间运行的任务变得更简单,相对来说AsyncTask更轻量级一些,适用于简单的异步处理,不需要借助线程和Handler即可实现。AsyncTask必须被子类化使用,AsyncTask定义了三种泛型类型 Params,Progress和Result,代码实现如下:

其中,Params表示启动任务执行的输入参数,比如HTTP请求的URL。Progress表示后台任务执行的百分比。Result表示后台执行任务最终返回的结果,比如String。

AsyncTask子类中需要实现至少一个以下方法:

a. onPreExecute()。该方法在执行实际的后台操作前被UI 线程调用。可以在该方法中做一些准备工作,如在界面上显示一个进度条。

b. doInBackground(Params...)。该方法在onPreExecute 方法执行后马上执行,运行在后台线程中。它主要负责执行那些很耗时的后台计算工作,可以调用 publishProgress方法来更新实时的任务进度。该方法是抽象方法,子类必须实现,否则AsyncTask也就无意义可言了。

c. onProgressUpdate(Progress...)。在publishProgress方法被执行后调用,UI 线程将调用这个方法从而在界面上展示任务的进展情况,例如通过一个进度条进行展示。

d. onPostExecute(Result)。该方法在doInBackground 执行完成后被UI 线程调用,后台的计算结果将通过该方法传递到UI 线程。

我们在使用异步操作的时候需要注意task的实例必须在UI 线程中创建,execute方法必须在UI 线程中调用,而且该task只能被执行一次,否则多次调用时将会出现异常。AsyncTask不能完全取代线程,在一些逻辑较为复杂或者需要在后台反复执行的逻辑就可能需要线程来实现了。

通过AsyncTask的源码中的字段sExecutor(代码如图3)我们可以很清楚的知道其实现原理也包含了ThreadPoolExecutor的概念(将在本文后面介绍),所以AsyncTask就相当于Handler,Thread和ThreadPoolExecutor三者的结合,十分方便开发人员使用。

4 线程池

在应用程序中开启多个线程,主要是想更多的获得CPU的执行权,从而提高应用程序的实行效率。例如我们可以开启多条线程执行下载任务,从而提高下载速度,使用户有更好的体验,但是,当我们过多的开启线程时,也会带来一些不必要的问题,会使应用程序的效率降低。这时我们就可以使用ThreadPoolExecutor[5]这个类来解决这个问题。

ThreadPoolExecutor实现了线程池的概念,它不仅仅是简单的多个thread的集合,它还提供了对线程池的管理策略。当一个任务通过execute(Runnable)方法欲添加到线程池时:

a. 如果此时线程池中的数量小于corePoolSize,即使线程池中的线程都处于空闲状态,也要创建新的线程来处理被添加的任务。

b. 如果此时线程池中的数量等于 corePoolSize,但是缓冲队列 workQueue未满,那么任务被放入缓冲队列。

c. 如果此时线程池中的数量大于corePoolSize,缓冲队列workQueue满,并且线程池中的数量小于maximumPoolSize,建新的线程来处理被添加的任务。

d. 如果此时线程池中的数量大于corePoolSize,缓冲队列workQueue满,并且线程池中的数量等于maximumPoolSize,那么通过 handler所指定的策略来处理此任务。也就是说处理任务的优先级为:核心线程corePoolSize、任务队列workQueue、最大线程maximumPoolSize,如果三者都满了,使用handler处理被拒绝的任务。

e. 如果此时线程池中的线程数量大于 corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被终止。这样,线程池可以动态的调整池中的线程数。

由于减少了每个任务调用的开销,它们通常可以在执行大量异步任务时提供增强的性能,并且还可以提供绑定和管理资源(包括执行集合任务时使用的线程)的方法。每个 ThreadPoolExecutor 还维护着一些基本的统计数据,如完成的任务数。(图4)

在实际使用时我们一般是使用类Executors的一些静态方法来产生一个线程池ExecutorService对象。例如newFixedThreadPool(int nThreads),通过Executors的内部源码(代码如图4)实现的分析我们可以看出其方法返回也是引用到了类ThreadPoolExecutor。在Executors类中还提供了很多其他的静态方法用来创建ExecutorService对象,可以根据需要来选择合适的方法。

参考文献

[1] 金泰延. Android框架揭秘[M]北京:人民邮电出版社, 2012

[2] 靳岩,姚尚朗.Android开发入门与实战[M].北京:人民邮电出版社,2009

[3] 杨丰盛.Android应用开发揭秘[M].北京:机械工业出版社,2010

[4] Hervé Guihot.Android应用性能优化 [M]. 北京:人民邮电出版社 .2012

[5] Ed Burnette. Android基础教程[M].北京:人民邮电出版社,2009

上一篇:无盘技术应用管理与集中控制研究 下一篇:基于FP—Growth关联规则算法的接警参数挖掘分...