本章学习Android多线程技术,多线程更新UI,用Handler方式解决两个线程之间的通信,同时理解学习Handler的实现原理(Handler、Lopper、MesageQueue三者的关系),以及AsyncTask异步任务处理类,最后学习线程池的,多个异步任务时,合理利用线程池可以减少系统资源的使用,增加程序的流畅性。
5.1 多线程的建立
每个应用启动都会启动一个线程处理UI相关的事物,例如处理用户的点击事件、屏幕绘制,并把相关的事件分发到相应的组件中进行处理,所以主线程又被称为UI线程。
耗时任务也在UI线程处理的话会影响用户体验,阻塞线程,所以在这种一般都是新建一个子线程来处理耗时任务。Android中与java中新建线程方式一样,例如在onCreate()方法中添加如下代码:
1 | new Thread(new Runnable() { |
5.2 子线程更新UI的方法
Android中是不能在子线程中更新UI的,否则会抛出异常,我们一般是在子线程中执行耗时任务,执行任务完后发送消息给主线程,通知主线程更新UI。例如,下载文件,下载完成后发送一个消息通知主线程更新界面,提示用户文件已经下载完成。
Android中子线程更新UI的方式有以下四种:
- 1,用activity对象的runOnUiThread方法
- 2,View.post(Runnable r)
- 3, AsyncTask系统SDK提供的处理耗时任务的类
- 4,Handler解决多线程间的通信
5.2.1 用activity对象的runOnUiThread方法
5.3 Handler的使用
handler可以发送和处理消息对象或Runnable对象,这些消息对象和Runnable对象与一个线程相关联。
Handler类有两种主要用途:
- 1,执行Runnable对象,还可设置延迟
- 2,两个线程之间发送消息,主要用来给主线程发送消息更新UI
5.3.1 为什么要用Handler
解决多线程并发问题,假设有多个线程去更新Activity的UI,并且都没有加锁,那么界面会显示不正常,这是不被允许的。Android提供了一套更新UI的机制,也可以使用Handler来实现多个线程之间的消息发送。
1,使用Handler的sendMessage发送消息
1 | private Handler handler = new Handler() { |
如上代码:这就是Handler消息机制,我们可以通过这个机制来更新UI,也就是异步消息处理机制,解决了在子线程中进行UI操作的问题,下面我们来分析一下Handler消息处理机制。
如上在MainActivity中的代码,我们定义了一个整型常量,表示更新UI操作(在这里是更新textview),我们新建了一个Handler对象,并重写了handleMessage()方法,在这里对具体的message进行处理,若发现Message的what字段的值等于UPDATE_TEXT,就将TextView显示的内容改成Nice to meet you。
我们再看点击事件的代码,我们并没有在子线程中直接进行UI操作,而是创建了一个Message对象,并将它的what字段值指定为UPDATE_TEXT,然后调用Handler的sendMessage方法将这条Message发送出去。Handler收到这条消息,并在handleMessage()对它进行处理,而此时handlerMessage方法的则是运行在主线程中,我们可以在这里进行UI操作,接下来对Message的what字段进行判断,如果等于UPDATE_TEXT,就将TextView的显示内容改成Nice to meet you。
这就是Android异步消息处理的基本用法,下面分析一下它的原理。
5.4 解析异步消息处理机制
Android异步消息主要分为4个部分组成:Message、Handler、MessageQueue、Looper。下面对这4部分进行介绍。
- 1,Message
它是线程之间传递的消息,它可以在内部传递少量的消息,用于在不同的线程之间交换数据。比如我们刚刚用到的what字段,除此之外还可以使用args1和arg2字段来携带一些整型数据,obj携带一个Object对象。 - 2,Handler
处理者,它主要用于发送和处理消息,发送消息一般是使用Handler的sendMessage方法,而发出的消息经过一系列的处理后,最终会传递到Handler的handleMessage方法中。 - 3,MessageQueue
消息队列,主要用于存放所有通过Handler发送的消息,这部分消息会一直存在于消息队列中,等待被处理,每个线程只会有一个消息队列。 - 4,Looper
它是MessageQueue的管家,调用Looper的loop方法后,就会进入一个无限循环中,每当发现MessageQueue中有一条消息后,就会将它取出,并传递到Handler的handleMessage方法中,每个线程只能有一个Looper对象。
问题:主线程的Looper.loop()死循环为什么不会导致ANR?
回答这个问题先看ActivityThread的源码:
1 | public static void main(String[] args) { |
在java程序中main方法是入口,当main方法的代码结束后程序也结束了,所以我们要让一个程序一直或长时间运行知道用户退出才结束程序,那么就要阻塞这个线程,如果不阻塞,那么一个APP刚刚启动main方法结束后,程序直接退出,这是不合适的。
这个阻塞就是通过Looper.loop()来实现的,再看loop()的源码:
1 | public static void loop() { |
这个方法在获取到调用这个方法的线程(即主线程)的looper后,再通过looper获取到了messageQueue,然后进入一个死循环,我们看官方注释,”might block”(当MessageQueue为空时),所以此时主线程就阻塞在这个地方了,从而导致main方法不会结束而退出而因此避免APP一启动就退出的状况。
那么问题来了,既然阻塞了主线程,那又是如何响应用户操作和回调activity生命周期方法的呢?
答:
1,这就涉及到了Android中的Handler机制原理和IPC机制,简单概括如下:首先启动一个APP时,此时应该还有其他两个Binder线程(用来和系统进行通信操作,接收系统进程发送的通知)。在thread.attach(false)就创建了新binder线程(具体指ApplicationThread,Binder的服务端,用于接收系统服务AMS发送来的事件),该Binder线程通过Handler将Message发送给主线程,而ActivityThread并非线程,它没有继承Thread类。其次,主线程的MeassageQueue没有消息时,便阻塞在loop的queue.next()的nativePollOnce()方法里,此时主线程慧释放cpu资源进入休眠状态,直到下个消息到达或有事务发生,通过pipe管道写端写入数据来唤醒主线程工作。这里采用epoll机制,是一种IO多复用机制,可以监听多个描述符,当某个描述符就绪(读或写就绪),就立即通知相应的程序进行读或者写操作,同步IO,即读写是阻塞的。所以主线程大部分时间是阻塞的,并不会消耗大量CPU资源。
2,Activity的生命周期是怎么实现在死循环体外能够执行起来?
ActivityThread的内部类H继承于Handler,通过handler消息机制,简单说Handler机制用于同一个进程的线程间通信。
Activity的生命周期都是依靠主线程的Looper.loop,当收到不同Message时则采用相应措施:
在H.handleMessage(msg)方法中,根据接收到不同的msg,执行相应的生命周期。
比如收到msg=H.LAUNCH_ACTIVITY,则调用ActivityThread.handleLaunchActivity()方法,最终会通过反射机制,创建Activity实例,然后再执行Activity.onCreate()等方法; 再比如收到msg=H.PAUSE_ACTIVITY,则调用ActivityThread.handlePauseActivity()方法,最终会执行Activity.onPause()等方法。 上述过程,我只挑核心逻辑讲,真正该过程远比这复杂。
主线程的消息又是哪来的呢?当然是App进程中的其他线程通过Handler发送给主线程,请看接下来的内容:system_server进程是系统进程,java framework框架的核心载体,里面运行了大量的系统服务,比如这里提供ApplicationThreadProxy(简称ATP),ActivityManagerService(简称AMS),这个两个服务都运行在system_server进程的不同线程中,由于ATP和AMS都是基于IBinder接口,都是binder线程,binder线程的创建与销毁都是由binder驱动来决定的。
App进程则是我们常说的应用程序,主线程主要负责Activity/Service等组件的生命周期以及UI相关操作都运行在这个线程; 另外,每个App进程中至少会有两个binder线程 ApplicationThread(简称AT)和ActivityManagerProxy(简称AMP),除了图中画的线程,其中还有很多线程,比如signal catcher线程等,这里就不一一列举。
Binder用于不同进程之间通信,由一个进程的Binder客户端向另一个进程的服务端发送事务,比如图中线程2向线程4发送事务;而handler用于同一个进程中不同线程的通信,比如图中线程4向主线程发送消息。
结合图说说Activity生命周期,比如暂停Activity,流程如下:
1,线程1的AMS中调用线程2的ATP;(由于同一个进程的线程间资源共享,可以相互直接调用,但需要注意多线程并发问题)
2,线程2通过binder传输到App进程的线程4;
3,线程4通过handler消息机制,将暂停Activity的消息发送给主线程;
4,主线程在looper.loop()中循环遍历消息,当收到暂停Activity的消息时,便将消息分发给ActivityThread.H.handleMessage()方法,再经过方法的调用,最后便会调用到Activity.onPause(),当onPause()处理完后,继续循环loop下去。
另附另外两个知乎答案
ActivityThread类是Android App进程的初始类。它里面有我们常见的javamain函数,我们都知道这是java程序的入口。
5.5 使用AsyncTask创建后台线程
使用Handler类来发送消息,然后更新UI,这种方式对于整个过程灵活控制,但是也存在缺点,就是代码比较臃肿,当多个任务同时执行时,不易于对线程进行精确控制。为了简化操作,Android提供了工具类android.os.AsyncTask,在代码上要比Handler轻量级。AsyncTask底层是一个线程池,执行多任务时消耗比较少。
AsyncTask的三个参数:Params、Progress和Result。
- 1,Params:启动任务是需要的参数,例如下载链接地址
- 2,Progress:执行的进度
- 3,Result:执行结果
需要重写四个方法:onPreExecute、doInbackground、onProgressUpdate和onPostExecute。
- 1,onPreExecute:执行任务之前调用
- 2,doInbackground:执行任务的方法,该方法是多线程调用,所以耗时操作都写在这个方法中,通过调用publishProgress方法更新进度
- 3,onProgressUpdate:更新任务进度,在调用publishProgress时,这个方法才会被调用,该方法可以把数据直接更新到UI空间上
- 4,onPostExecute:任务执行完毕调用
以我们使用AsyncTask模拟从网上下载文件为例:
1 | private class DownloadFilesTask extends AsyncTask<String,Integer,Long> { |
这是在MainActivity中定义的一个类并让它继承自AsyncTask,定义泛型,重写方法,接下来在onCreate()方法调用这个类。
1 | new DownloadFilesTask().execute("www.downloadfile.com); |
5.6 线程池的使用
线程池是一种多处理形式,处理过程中将任务添加到队列,然后在创建线程后自动启动这些任务。
线程池应用的场景?
比如说一个音乐类App,用户需要下载歌曲,下载歌曲很耗时,需要启动一个新的线程进行下载,之前可能会使用如下代码:
1 | new Thread(new Runnable() { |
如果要下载1000首歌曲呢?开启1000个线程吗?这会导致什么问题?
- 每下载一首歌曲就要新建一个线程,导致频繁的新建销毁线程,会使程序卡主或崩溃
- 这样创建的线程无法统一管理
- 不方便统计(例如已下载歌曲的数量)
而线程池能解决以上问题,它的优点如下:
- 重用已创建线程,不会频繁地创建与销毁线程
- 对线程统一管理、分配、调优和监控
- 控制线程数量,合理使用系统资源,这样不会造成程序卡顿或崩溃
Android中的线程操作是通过ThreadPoolExecutor类实现。最长的构造函数有7个参数。
- corePoolSize:核心线程数
- maximumPoolSize:线程池最大线程数
- keepAliveTime:线程空闲保持时间
- unit
- workQueue:线程池中的队列任务,这个队列保存线程池提交的任务,它的使用与线程池中的线程数量有关
- threadFactory:
- handler