通信
《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.java1
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