通信
《Android开发艺术探索》
Handler
是Android
消息机制的上层接口,使得在开发过程中只需要和Handler
交互即可。Handler
的运行需要底层的MessageQueue
(消息队列:存储消息数据)【单链表】和Looper
(循环:无限循环地去遍历)支撑。Looper
中有特殊概念ThreadLocal
【不是线程,在每个线程中存储数据】。Handler
创建时会采用当前线程的Looper
来构造消息循环系统。Handler
内部通过ThreadLocal
获取到当前线程的Looper
。ThreadLocal
可以在不同的线程中互不干扰地存储并提供数据,通过ThreadLocal
可轻松获取每个线程的Looper
。- 线程默认没有
Looper
,若需要使用Handler
就必须为线程创建Looper
。 UI
线程(ActivityThread
)在创建时就会初始化Looper
,也即Handler
可以在主线程中默认使用的原因。
Android只能主线程访问UI
ViewRootImpl
的checkThread
方法对UI操作做了验证(限制UI只能在主线程中访问)
1 | void checkThread(){ |
Handler
切线程的作用,让耗时操作放到子线程中,更新UI时切到主线程。
Android不允许在子线程中访问UI
原因:Android
的UI
控件不是线程安全的,若多线程中并发访问可能会导致UI
控件处于不可预期的状态。
若给UI
控件加上锁机制,缺点:
- 加上锁机制会让
UI
访问的逻辑变复杂 - 加上锁机制会降低
UI
访问的效率,因为锁机制会阻塞某些线程的执行
消息机制
相关概念
概念 | 定义 | 作用 | 备注 |
---|---|---|---|
主线程 (UI线程、Main Thread) |
当应用程序第1次启动时,会同时自动开启1条主线程 | 处理与UI相关的事件 (如更新、操作等) |
主线程与子线程通信媒介=Handler |
子线程 (工作线程) |
认为手动开启的线程 | 执行耗时操作 (如网络请求、数据加载等) |
主线程与子线程通信媒介=Handler |
消息 (Message) |
线程间通讯的数据单元 (即Handler接受 & 处理的消息对象) |
存储需操作的通信信息 | / |
消息队列 (Message Queue) |
一种数据结构 (存储特点:先进先出) |
存储 Handler 发送过来的消息(Message) | / |
处理者 (Handler) |
* 主线程与子线程的通信媒介 * 线程消息的主要处理者 |
* 添加消息(Message)到消息队列(Message Queue) * 处理循环器(Looper)分派过来的消息(Message) |
/ |
循环器 (Looper) |
消息队列(Message Queue)与处理者(Handler)的通信媒介 | 消息循环,即 * 消息获取:循环取出消息队列(Message Queue)的消息(Message) * 消息分发:将取出的消息(Message)发送给对应的处理者(Handler) |
* 每个线程中只能拥有1个Looper * 1个Looper可绑定多个线程的Handler * 即多个线程可往1个Looper所持有的MessageQueue中发送消息,提供了线程间通信的可能 |
- 主线程(UI线程、Main Thread)
- 定义:当应用程序第一次启动时,会同时自动开启一条主线程
- 作用:处理与UI相关的事件
- 子线程(工作线程)
- 定义:人为手动开启的线程
- 作用:执行耗时操作(如网络请求、数据加载等)
- 消息(Message)
- 定义:线程间通讯的数据单元(即Handler接受&处理的消息对象)
- 作用:
Handler、Looper、MessegeQueue关系
Looper运行模型
用Handler的原因即其作用
- 为什么用Handler消息传递机制
能保证 :多个线程并发更新UI;同时保证线程安全 - Handler作用
即工作线程需要更新UI时,通过Handler通知主线程,从而在主线程中更新UI操作
Handler机制
Handler
创建时会采用当前线程的Looper
来构建内部的消息循环系统,如果当前线程没有Looper
,那么就会报错。解决:为当前线程创建Looper
,或者在一个有Looper
的线程中创建Handler
也行1
2
3
4
5
6
7
8
9
10
11
12
13
14class LooperThread extends Thread{
public Handler mHandler;
public void run(){
//为Thread创建Looper
Looper.prepare();
mHandler = new Handler(){
public void handleMessage(Message msg){
//process incoming message here
}
};
//looper开始循环遍历messageQueue工作
Looper.loop();
}
}Handler
创建完毕后,其内部Looper
以及MessageQueue
即可与Handler
一起协同工作了
可以通过Handler
的post
方法(最终也是通过send
方法来完成的)将一个Runnable
投递到Handler
内部的Looper
中去处理;
也可以通过Handler
的send
方法发送个消息,这个消息同样会在Looper
中去处理;send
工作过程:Handler
的send
被调用时,它会调用MessageQueue
的enqueueMessage
方法将这个消息放入消息队列中,而后Looper
发现新消息到来会去处理这个消息,最终消息中的Runnable
或者Handler
的handleMessage
方法会被调用。
注意:Looper
是运行在创建Handler
所在的线程中的,这样Handler
中的业务逻辑就会被切换到创建Handler
所在的线程中去执行了。Handler有两个主要用途:
(1)安排Message
和runnables
在将来的某个时刻执行;
(2)将要在不同于自己的线程上执行的操作排入队列。(在多个线程并发更新UI的同时保证线程安全。)Android规定访问UI只能在主线程中进行,因为Android的UI控件不是线程安全的,多线程并发访问会导致UI控件处于不可预期的状态。为什么系统不对UI控件的访问加上锁机制?缺点有两个:
- 加锁会让UI访问的逻辑变得复杂;
- 其次锁机制会降低UI访问的效率。
如果子线程访问UI,那么程序就会抛出异常。
ViewRootImpl
对UI操作做了验证,这个验证工作是由ViewRootImpl
的checkThread
方法完成:ViewRootImpl.java
1
2
3
4
5
6void checkThread() {
if (mThread != Thread.currentThread()) {
throw new CalledFromWrongThreadException(
"Only the original thread that created a view hierarchy can touch its views.");
}
}
Message
:Handler
接收和处理的消息对象MessageQueue
:Message
的队列,先进先出,每一个线程最多可以拥有一个Looper
:消息泵,是MessageQueue
的管理者,会不断从MessageQueue
中取出消息,并将消息分给对应的Handler
处理,**每个线程只有一个Looper
**。
Handler
创建的时候会采用当前线程的Looper
来构造消息循环系统,需要注意的是,线程默认是没有Looper
的,直接使用Handler
会报错,如果需要使用Handler
就必须为线程创建Looper
,因为默认的UI
主线程,也就是ActivityThread
,ActivityThread
被创建的时候就会初始化Looper
,这也是在主线程中默认可以使用Handler
的原因。
Handler源码
IdleHandler
工作原理
ThreadLocal
ThreadLocal
是一个线程内部的数据存储类,通过它可以在指定的线程中存储数据,其他线程则无法获取。Looper
、ActivityThread
以及AMS中都用到了ThreadLocal
。当不同线程访问同一个ThreadLocal
的get方法,ThreadLocal
内部会从各自的线程中取出一个数组,然后再从数组中根据当前ThreadLcoal
的索引去查找对应的value
值。
在指定的线程中存储数据,只有在这个线程中才可以获取到存储的数据,其他线程无法获取到。【数据为线程私有】
ThreadLocal.java
1 | public void set(T value) { |
应用场景:
1. 场景1:当某些数据是以线程为作用域切不同线程有不同的数据副本时,考虑采用ThreadLocal
。若不用ThreadLocal
,那么就得提供个类似于LooperManager
的全局哈希表供Handler
查找指定线程的Looper
来管理数据。
2. 场景2:复杂逻辑下的对象传递,比如监听器的传递,
例子:
1 | private ThreadLocal<Boolean> mBooleanThreadLocal = new ThreadLocal<Boolean>(); |
1 | //输出结果: |
不同线程访问同一个
ThreadLocal
对象,但是通过ThreadLocal
获取到的值确实不一样的。不同线程访问同一个
ThreadLocal
的get方法,ThreadLocal
内部会从各自的线程中取出一个数组,然后再从数组中根据当前ThreadLocal
的索引去查找出对应的value值。显然,不同线程中的数组是不同的,这就是为何可通过ThreadLocal
在不同线程中维护一套数据副本并且互不干扰的原因。
MessageQueue
MessageQueue
主要包含两个操作:插入和读取。读取操作本身会伴随着删除操作,插入和读取对应的方法分别是enqueueMessage
和next
。MessageQueue
内部实现并不是用的队列,实际上通过一个单链表的数据结构来维护消息列表。next
方法是一个无限循环的方法,如果消息队列中没有消息,那么next
方法会一直阻塞。当有新消息到来时,next
方法会放回这条消息并将其从单链表中移除。
MessageQueue.java
1 | boolean enqueueMessage(Message msg, long when) { |
Looper
Looper会不停地从MessageQueue中查看是否有新消息,如果有新消息就会立刻处理,否则会一直阻塞。
Looper.java
1 | private Looper(boolean quitAllowed) { |
可通过Looper.prepare()为当前线程创建一个Looper:
1 | new Thread("Thread#2") { |
除了prepare方法外,Looper还提供了prepareMainLooper方法,主要是给ActivityThread创建Looper使用,本质也是通过prepare方法实现的。由于主线程的Looper比较特殊,所以Looper提供了一个getMainLooper方法来获取主线程的Looper。
Looper提供了quit和quitSafely来退出一个Looper,二者的区别是:quit会直接退出Looper,而quitSafly只是设定一个退出标记,然后把消息队列中的已有消息处理完毕后才安全地退出。Looper退出后,通过Handler发送的消息会失败,这个时候Handler的send方法会返回false。因此在不需要的时候应终止Looper。
Looper.java
1 | public static void loop() { |
loop
方法是一个死循环,唯一跳出循环的方式是MessageQueue
的next方法返回了null
。当Looper
的quit
方法被调用时,Looper
就会调用MessageQueue
的quit
或者qutiSafely
方法来通知消息队列退出,当消息队列被标记为退出状态时,它的next方法就会返回null。loop
方法会调用MessageQueue
的next
方法来获取新消息,而next
是一个阻塞操作,当没有消息时,next
会一直阻塞,导致loop
方法一直阻塞。Looper
处理这条消息:msg.target.dispatchMessage(msg)
,这里的msg.target
是发送这条消息的Handler
对象。
Looper源码
Handler
Handler的工作主要包含消息的发送和接收的过程。消息的发送可以通过post/send的一系列方法实现,post最终也是通过send来实现的。
Message
ThreadPoolExecutor
子线程与主线程通信
Activity.runOnUIThread(Runnable)
View.Post(Runnable)
View.PostDelayed(Runnable, long)
AsyncTask
3个参数
- Params:后台线程所需的参数
- Progress:后台线程处理作业的进度
- Result:后台线程运行的结果,也就是需要提交给UI线程的信息
4个方法
onPreExecute
:运行在UI线程,主要目的是为后台线程的运行做准备doInBackground
:运行在后台线程,它用来负责运行任务。它拥有参数Params,并且返回ResultonProgressUpdate
:运行在UI线程,主要目的是用来更新UI线程中显示进度的UI控件onPostExecute
:运行在UI线程,doInBackground
后被调用,在此方法中,就可以将Result更新到UI控件上
Handler.Post(Runnable)
Handler.PostDelayed(Runnable, long)
在子线程中创建Handler,但实例化在run()外=>handler内是主线程
1 | private void showChildMessageQueue(){ |
在子线程中创建Handler,但实例化在run()内=>handler内是子线程
1 | private void showChildMessageQueue(){ |
Looper无限循环为啥不阻塞UI线程?
AcitivityThread
是个final的类,不是主线程,主线程是Zygote fork
出来的。ActivityThread
里有main
方法,就是程序入口,main
中有Looper.prepareMainLooper()
和looper.loop()
Looper
无限循环的原因:ActivityThread
的main()
主要是做消息循环(保证main()
不会结束掉),一旦退出消息循环则App也会退出。Looper.loop()
不会阻塞UI线程原因:Looper.loop()
功能是不断收发事件的消息(Activity的生命周期都依靠于主线程的Looper.loop()
来调度)。在进入死循环前会创建个新binder
线程(ActivityThread.main()
中thread.attach(false);
就是建立Bindler
通道–创建新线程)这个线程会接受系统服务AMS
发送来的事件
当没有事件需要处理时,主线程就会阻塞;当子线程往消息队列发送消息,并且往管道文件写数据时,主线程就被唤醒。- 主线程中的
Looper
从消息队列读取消息,当读完所有消息时,主线程阻塞。子线程往消息队列发送消息,并且往管道文件写数据,主线程即被唤醒,从管道文件读取数据,主线程被唤醒只是为了读取消息,当消息读取完毕,再次睡眠。因此loop
的循环并不会对CPU
性能有过多的消耗。
通信相关问题
Can’t create handler inside thread that has not called Looper.prepare()
现象:报错Can't create handler inside thread that has not called Looper.prepare()
原因:在子线程中new Handler
(加Toast
也有这个问题)导致的
分析:因为Handler
与其调用者在同一线程中,Handler
中执行了延迟操作,会导致调用的线程阻塞。每个Hander
对象都会绑定一个Looper
对象,每个Looper
对象对应一个消息队列(MessageQueue
),若在创建Handler
时不指定与其绑定的Looper
对象则默认将当前线程的Looper
绑定到该Handler
上。
在主线程中直接使用new Hanlder()
创建Handler
对象,其将自动与主线程的Looper
对象绑定;在子线程中直接new
创建会报错,因为子线程中没有开启Looper
,而Handler
对象必须绑定Looper
对象。
解决:
方案一:需先在该子线程中手动开启Looper
(Looper.prepare()-->Looper.loop()
),然后将其绑定到Handler
对象上。
1 | final Runnable runnable = new Runnable(){ |
方案二:通过Looper.getMainLooper()
,获得主线程的Looper
,将其绑定到此Handler
对象上。
1 | final Runnable runnable = new Runnable() { |
sending message to a Handler on dead thread
toast报这个异常。解决:在主线程创建个Handler,toast写里面,需要吐司的地方去发送消息给这个handler