Android 进阶之多线程技术
我们很多人应该都知道在Android开发中有两种线程,一种是主线程又称为UI线程,另一个是子线程后称为后台工作线程,Android程序的有部分代码是必须执行在主线程的比如系统事件(例如设备屏幕发生旋转),输入事件(例如用户点击滑动等),程序回调服务,UI绘制以及闹钟事件等等。一旦我们在主线程里面添加了操作复杂的代码,或者线程阻塞的任务,这些代码就很可能阻碍主线程去响应交互事件,有可能倒置在16ms内不能完成单次刷新的操作。导致掉帧的现象,用户就有可能感知到卡顿现象了。
在开发中为了让我们的Apk运行地更加流畅,我们往往需要使用多线程技术让耗时的操作运行在后台。
一旦我们在主线程里面添加了操作复杂的代码,这些代码就很可能阻碍主线程去响应点击/滑动事件,阻碍主线程的UI绘制等等。我们知道,为了让屏幕的刷新帧率达到60fps,我们需要确保16ms内完成单次刷新的操作。一旦我们在主线程里面执行的任务过于繁重就可能导致接收到刷新信号的时候因为资源被占用而无法完成这次刷新操作,这样就会产生掉帧的现象,刷新帧率自然也就跟着下降了(一旦刷新帧率降到20fps左右,用户就可以明显感知到卡顿不流畅了)。
众所周知,Android程序的大多数代码操作都必须执行在主线程,例如系统事件(例如设备屏幕发生旋转),输入事件(例如用户点击滑动等),程序回调服务,UI绘制以及闹钟事件等等。那么我们在上述事件或者方法中插入的代码也将执行在主线程。
ANR
什么是ANR
ANR 是 Application Not Response的简称,当某个应用处于长期假死状态,Android系统会弹出如下窗口,显示XXX 无响应 并给出两个按钮,要求用户选择关闭应用,还是继续等待。
在Android中对未响应定义如下:
Activity对一个输入事件在5秒内没有响应,
Broadcast Receiver在10秒内没有完成它的onReceiver处理程序.
造成ANR的原因及如何避免:
所有的Android应用程序组件(包括Activity,Service,BroadCast Receiver)都在应用程序主线程中运行,因此任何组件中的费时操作都可能阻塞其他组件,为了确保应用程序能够快速响应应用程序交互或者系统事件,必须将所有的费时处理从应用程序主线程移动到子线程中。
Android多线程机制
为了避免产生ANR现象,在应用程序中通过创建工作线程,将耗时操作移动到工作线程中完成,但是在应用程序组件中Notification和Intent总是在GUI线程(主线程)中进行接收和处理,在GUI线程(主线程)中创建的View或者Toast的显示都应该在GUI线程(主线程)中完成。解决这个矛盾。Android采用了Handler机制来实现工作线程与GUI线程同步,从而更新UI。该模型如下图所示:线程通过Looper建立自己的消息循环,消息队列是一个先入先出的队列,工作线程可以往指定的Handler对象投递消息,主线程的Looper负责从消息队列中取出Message并分发到消息指定目标Handler对象进行处理。
Handler 对象
Handler负责往消息队列上添加消息,处理消息,一个线程可以有多个Handler,当消息队列循环到某个Message的时候,便调用对应Handler的handlerMessage()方法对其进行处理。
Handler可投递的对象有“消息对象”和“线程对象”,具体方法如下:
投递消息:
sendMessage(runable)
sendMessageAtTime(runable,long)
sendMessageDelayed(runable,long)将消息对象从消息队列中移除:
removeMessage(what)投递线程对象:
post(runable)
postAtTime(runable,long)
postDelayed(runable,long)将线程对象从消息队列中移除:
removeCallbacks(runable)
主线程从消息队列中取出线程对象后并未开启新的线程来完成线程对象中的任务,而是仍然在当前线程中执行,只是调用了线程对象的run方法。
Looper 对象
Looper是用来封装消息循环和消息队列的一个类,一个Looper对应一个消息队列,一个线程最多只能有一个Looper,默认情况下Android会为主线程创建Looper并开启消息循环,但是使用Thread创建的工作线程没有开启消息循环,所以,在主线程中,应用下面代码创建Handler对象时,就不会出错;而如果在新创建的非线程中,应用下面的代码就会产生异常。
Handler handler = new Handler(); |
如果想要在非主线程中创建Handler对象,首先需要使用Looper类的prepare()方法来初始化一个Looper对象,然后创建一个Handler对象,再使用Looper类的looper()方法启动Looper,从消息队列里获取和处理消息。写在Looper.loop()之后的代码不会被执行,这个函数的内部是一个循环,当调用Handler.getLooper().quit()方法之后,loop()方法才会终止,后面的代码才能运行。
public class LooperThread extends Thread { |
Looper 有如下方法可以使用:
Message 对象
在创建Looper对象的时候会创建一个消息队列来容纳消息,每个线程只可以拥有一个消息队列,其中的Message是由Looper来分发的,同时Message不能直接添加到消息队列中,只能通过与Looper关联的Handler来添加。
Messsage对象可以通过如下两类方法获取:
- 通过Message构造方法新建一个Message对象
- 通过Message.obtain方法或者Handle.obtainMessage方法来获取
后者并不一定直接创建新的实例,而是查看消息池中是否有满足要求的Message实例,存在则直接取出并返回这个实例,如果没有,则使用给定的参数创建一个Message对象。
Message msg = handler.obtainMessage(...........); |
Message msg = Message.obtain(........); |
除了使用arg1,arg2,obj作为数据载体外还可以使用bundle来携带数据
Bundle bundle = new Bundle(); |
使用Thread创建线程
- 通过Thread类的构造方法创建线程
Thread thread = new Thread(new Runnable(){ |
2.通过实现Runnable接口创建线程
public class MainActivity extends Activity implements Runnable{ |
Thread 常用方法
一个例子
public class MainActivity extends Activity implements OnClickListener { |
使用HandlerThread创建线程
HandlerThread用于创建线程,与Thread不同的是HandlerThread类能够创建一个含有Looper的线程
生成一个HandlerThread对象
HandlerThread task = new HandlerThread(“thread”); |
在调用HandlerThread.getLooper方法前,必须调用该类的start方法,不然会报空指针
task.start(); |
looper = mHandlerThread.getLooper(); |
Message msg = myHandler.obtainMessage(); |
一个例子
使用post提交Runable线程对象,在Runable线程对象的run方法中输出当前线程的ID。代码如下,通过该实验可以发现Handler只是调用Runable线程对象的run方法,所有的操作都在主线程中执行,并没有创建一个新的进程。要想让投递的方法在子线程中运行就要用到HandlerThread类。下一个实验所示。
public class MainActivity extends Activity implements OnClickListener { |
另一个例子
使用post提交Runable线程对象,与上面的例子比较,不同的是该实验往HandlerThread投递。这时候Runable线程对象运行在新的线程上。代码如下,结果如下
public class MainActivity extends Activity implements OnClickListener { |
AsyncTask
AsyncTask允许定义将在后台执行的操作,并提供了可以用来监控进度以及在GUI线程上发布结果的方法。
AsyncTask处理线程的创建,管理和同步等全部工作,它可以创建一个异步任务,该任务由两个部分组成,将在后台执行到处理以及在处理完成后执行到UI更新。对于生命周期较短并且需要在UI上显示进度和结果到后台操作是很好的解决方案。但是AsyncTask在设备的方向发生变化而导致Activity被摧毁和重新创建时候会被取消,因此对于生命周期较长的后台操作,使用Service比较合适.
使用AsyncTask进行异步处理
- 继承AsyncTask:
public class Mytask extends AsyncTask<输入变量类型,更新变量类型,结果值类型> |
- 重写相关方法:
- doInBackground:这个方法将会在后台线程执行,可以把运行时间较长的代码放置到这里,而且不能试图在此处理程序中与UI对象交互,可以调用publishProcess方法以传递参数值给onProgressUpdate,当后台任务完成后,可以返回最终结果并作为参数传递给onPostExecute处理程序,在该处理程序中更新UI
- onProgressUpdate 当中间进度更新变化的时候更新UI,
- onPostExecute当doInBackground完成后该方法的放回值就会传入到这个事件处理程序中,在这个处理程序中可以更新UI
- 运行异步任务:
new Mytask().execute(Inputparam); |
- 判断AsyncTask是否正在运行
if(mTask!=null&&mTask.getStatus() == AsyncTask.Status.RUNNING){
} - 取消AsyncTask:
mTask.cancel(true); |
- 一个AsyncTask只能使用一次,当你想再次使用的话,只能再new一个,否则会出现如下错误:
java.lang.IllegalStateException: Cannot execute task: the task has already been executed (a task can be executed only once) |
AsyncTask使用时需要注意的问题:
- 默认情况下,AsyncTask任务都是被线性调度执行的,他们处在同一个任务队列当中,按顺序逐个执行。一旦其中的某个AsyncTask执行时间过长,队列中的其他剩余AsyncTask都处于阻塞状态,必须等到该任务执行完毕之后才能够有机会执行下一个任务。情况如下图所示:
为了解决上面提到的线性队列等待的问题,我们可以使用AsyncTask.executeOnExecutor()强制指定AsyncTask使用线程池并发调度任务。
- 在把AsyncTask写成Activity的内部类的时候就很容易因为AsyncTask生命周期的不确定而导致Activity发生泄漏。
线程池
我们在设计模式中在介绍享元模式的时候介绍了用池技术,在Android中也可以通过线程池将任务分解,并发执行,
下面是来自胡凯博客上的一个例子,在这个例子宏我们需要一次性decode 40张图片,每个线程需要执行4ms的时间,如果我们使用专属单线程的方案,所有图片执行完毕会需要花费160ms(40*4),但是如果我们创建10个线程,每个线程执行4个任务,那么我们就只需要16ms就能够把所有的图片处理完毕。
为了降低开发难度,系统为我们提供了ThreadPoolExecutor帮助类来简化实现。
线程优先级
默认情况下,新创建的线程的优先级和创建它的父线程保持一致。也就是说如果主UI线程创建出了十几个子线程,那么这些子线程的优先级也和默认和主线程保持一致,为了不让新创建的工作线程和主线程抢占CPU资源,需要把这些线程的优先级适当降低,从而提高主线程所能得到的系统资源。
在Android系统里面,我们可以通过android.os.Process.setThreadPriority(int)设置线程的优先级,参数范围从-20到24,数值越小优先级越高。同时也定义了一些特定的优先级供日常使用。