因为只有主线程能修改UI组件,所以主线程又被称作UI线程;但是如果主线程进行耗时操作,那么会被阻塞(ANR异常,系统无法响应输入事件和BroadCast),而子线程又不能修改UI组件,只能在新进程里发出一通消息,这时就需要采用handle的传递机制;
Handle的作用有两个:在新启动的线程发送消息(一般采用sendEmptyMessage);在主线程接受,处理消息(重写handleMessage);
handler机制相当于一个快递流水线公司:
handler:快递员
Message:快递
MessageQueue:快递分拣传送带
Looper:快递公司
总结
Handler机制可以简述为:
Handler将Message发送到Looper的消息队列中,即MessageQueue,等待Looper的循环读取Message,处理Message,然后调用Message的target,即附属的Handler的dispatchMessage()方法,将该消息回调到handleMessage()方法中,然后完成更新UI操作。
Message:
创建方式:
1.new Message()
2.Messasge.obtain() (更优)
优先采用第二种方法创建message,会优先在message池中获取,如果池中没有就new一个新的message;
message在池中以链表形式串在一起,通过pool指针指向头部的message并取出,需要回收的时候就重新将pool指针指向回收的message;
Handler:
1.handler的构造方法:
1.获取handler所在线程的looper对象:mLooper = Looper.mylooper();
2.对looper判空,不为空就去拿looper的消息队列messagequeue : mQueue = mLooper.mQueue
3.设置callback回调: mCallBack = callback
Handler只是消息的处理者,听命于大佬Looper(领导),Handler将消息通过messagequeue上报Looper,Looper轮询完这些消息后,给Handler进行分发处理;
需要注意的几点:
1.新创建的线程就是在主线程中,这样就无需创建Looper对象,系统已经自动创建了;
2.新创建的线程是自创的子线程(Thread),这样就需要自己获取Looper对象并启动它;通过调用Looper的prepare()方法即可获取Looper对象,然后在调用Looper的静态方法loop()来启动它;子线程创建Handler之前:保证调用了Looper.prepare创建Looper对象 如果子线程中new Handler也想更新UI,那只能通过new Handler(Looper.getMainLooper())方法强行切换到主线程进行UI更新;
这个prepare和myLooper()流程是通过threadLocal实现,下面有讲
2.Handler sendMessage() Post()等相关API方法
1.不管有没有延时去sendmessage,最后对会调用sendMessageAtTime,并调用enqueueMessage将message发送到消息队列中,根据延时时间决定message在消息队列中的指针顺序。
- msg.target = this 发送到消息队列之前绑定message和handler,用于Looper分发消息。
2.如果采用的是post一个runnable,那么会先将runnable封装成一个message,然后再进行sendmessage;如果采用post runnable的方式,那么就会在把runnable封装成message的时候,设定callback,在最后dispatch(见下面)去判断是否含有callback,有的话就回调handleCallBack否则就是普通的handleMessage()回调;
3.Handler dispatchMessage()方法
Looper处理完message,会调用handler的dispatchMessage方法分发消息,内核其实是通过我们重写的handleMessage方法来更新UI任务;
MessageQueue
前面讲到Handler在发送消息时会调用MessageQueue的enqueueMessage()方法
说明:
1.messagequeue是一个先进先出的队列,内部采用了单向链表的数据结构来储存message;
2.Looper通过调用 Message.next()方法,取出MessageQueue中的消息;
Looper
Looper在Handler机制中扮演最关键的一环,它不断地从消息队列中取出消息,并进行分发处理事件(dispatchmessage)。
prepare:
ThreadLocal
ThreadLocal可以提供我们一个局部变量,而且这个变量与一般变量还不同,他是每个线程独有的,与其他线程互不干扰的。
ThreadLocal是一个线程内部的数据存储(这里存放的就是Looper对象)entry 类,key就是当前线程的ThreadLocal,一个线程只有一个ThreadLocal,value就是要存储的数据。
通过它可以在指定的线程中存储数据,数据存储以后,只有在指定线程中可以获取到存储的数据,对于其它线程来说无法获取到数据,通过一个entry 来存储线程和looper的对应关系
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
//维护了一个类似map(key是当前线程绑定的threadlocal, v是要存取的值)
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
...
}
Looper.prepare方法作用是将Looper和当前线程进行绑定(也就是将当前线程存入threadLocal)
prepare之后紧接着调用handler构造方法并传入myLooper(),这里则可以获取绑定或称之存储到ThreadLocal的looper
// Handler.java
public Handler(@NonNull Looper looper,@Nullable Callback callback, boolean async) {
// 关键代码:通过ThreadLocal获取当前线程的Looper
mLooper = Looper.myLooper();
if (mLooper == null) {
throw new RuntimeException("Can't create handler inside thread " + Thread.currentThread()
+ " that has not called Looper.prepare()");
}
mQueue = mLooper.mQueue;
// ...其他初始化
}
loop():
loop方法是启动Looper的方法,获取当前looper的消息队列通过for死循环取出消息message(若message为空则CPU进行休眠状态直到下一个message的到来),再msg.target拿到handler引用,通过
调用dispatchMessage进行消息分发,让handler处理消息;
主线程中已经在ActivityThread中的main方法里创建主线程的looper并loop启动
1.新创建的线程就是在主线程中,这样就无需创建Looper对象,系统已经自动创建了;
2.新创建的线程是自创的子线程(Thread),这样就需要自己获取Looper对象并启动它;通过调用Looper的prepare()方法即可获取Looper对象,然后在调用Looper的静态方法loop()来启动它;子线程创建Handler之前:保证调用了Looper.prepare创建Looper对象
屏障消息和异步消息:
屏障消息:
在next()方法中,有一个屏障的概念(message.target ==null为屏障消息),如下代码:
if (msg != null && msg.target == null) {
// Stalled by a barrier. Find the next asynchronous message in the queue.
do {
prevMsg = msg;
msg = msg.next;
} while (msg != null && !msg.isAsynchronous());
}
MessageQueue#postSyncBarrier()
private int postSyncBarrier(long when) {
// Enqueue a new sync barrier token
synchronized (this) {
final int token = mNextBarrierToken++;
//从消息池中获取Message
final Message msg = Message.obtain();
msg.markInUse();
//就是这里!!!初始化Message对象的时候,并没有给target赋值,因此 target==null
msg.when = when;
msg.arg1 = token;
//...
屏障消息作用:出现屏障的时候,会过滤同步消息,返回异步消息;具体可以看queue的next拿消息方法;
//关键!!!
if (msg != null && msg.target == null) {
do {
prevMsg = msg;
msg = msg.next;
} while (msg != null && !msg.isAsynchronous());
//如果target==null,那么它就是屏障,需要循环遍历,一直往后找到第一个异步的消息
}
当消息队列开启同步屏障的时候(即标识为msg.target == null
),消息机制在处理消息的时候,优先处理异步消息。这样,同步屏障就起到了一种过滤和优先级的作用。
下面用示意图简单说明:
如上图所示,在消息队列中有同步消息和异步消息(黄色部分)以及一道墙----同步屏障(红色部分)。有了同步屏障的存在,msg_2 和 msg_M 这两个异步消息可以被优先处理,而后面的 msg_3 等同步消息则不会被处理。那么这些同步消息什么时候可以被处理呢?那就需要先移除这个同步屏障,即调用removeSyncBarrier()
。
异步消息:
在Hadnler无参的构造函数中,默认设置的消息都是同步的;异步消息通常代表着中断,输入事件和其他信号,这些信号必须独立处理,即使其他工作已经暂停;
也就是说异步和同步消息在队列中的情况下如果出现了屏障消息,优先取出异步消息进行处理;
如何设置异步消息:
直接调用Message的setAsynchronous()方法,方法如下:
Android有哪些常见的屏障消息发送时机:
1.ViewRootImpl.scheduleTraversals()
在scheduleTraversals方法中通过调用postSyncBarrier()方法发送一个屏障过去后再发送异步消息,因为绘制过程中为了保证绘制流程,防止后序同步消息的执行,直到视图绘制完成才执行同步消息;
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
//开启同步屏障
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
//发送异步消息
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
if (!mUnbufferedInputDispatch) {
scheduleConsumeBatchedInput();
}
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}
postCallback()
最终走到了ChoreographerpostCallbackDelayedInternal()
:
2.view的invalidate(原理和上面一致)
PostDelay是怎么实现的?
消息的放置
消息入队:调用 sendDelayMessage() 或 postDelayed() 时,系统会创建一个 Message 对象并设置其 when 字段(执行时间戳),计算方式为:
when = SystemClock.uptimeMillis() + delayMillis
有序插入:MessageQueue 内部维护一个按 when
排序的单链表:
- 新消息会根据
when
值插入到合适位置 - 队列总是保持按执行时间从早到晚的顺序排列
- 如果消息没有延迟(delayMillis=0),
when
就是当前时间,会尽可能早执行
消息的取出
消息轮询:Looper 通过 loop() 方法不断调用 MessageQueue.next() 获取下一条消息
延迟处理:
- next() 方法会检查队首消息的 when 值
- 如果 when > now(未到执行时间),计算需要等待的时间:
nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE)
- 线程会进入休眠状态(通过 nativePollOnce),直到:
- 到达消息执行时间
- 有新消息插入队列前面
- 有其他中断
并不是通过延时放入队列消息去做的,而是正常放入队列并标记延时时间msg.when
for (;;) {
if (nextPollTimeoutMillis != 0) {
Binder.flushPendingCommands();
}
nativePollOnce(ptr, nextPollTimeoutMillis);
···
if (msg != null) {
if (now < msg.when) {
// Next message is not ready. Set a timeout to wake up when it is ready.
nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
} else {
// Got a message.
mBlocked = false;
if (prevMsg != null) {
prevMsg.next = msg.next;
} else {
mMessages = msg.next;
}
msg.next = null;
if (DEBUG) Log.v(TAG, "Returning message: " + msg);
msg.markInUse();
return msg;
}
} else {
// No more messages.
nextPollTimeoutMillis = -1;
}
···
}
}
很贴心的给出了注释解释“ Next message is not ready. Set a timeout to wake up when it is ready.”,翻译“下一条消息尚未准备好。设置一个超时,以便在准备就绪时唤醒。”
根据当前这条消息的when来判断,如果头部的这个Message是有延迟而且延迟时间没到的(now < msg.when),不返回message 而且会计算一下时间(保存为变量nextPollTimeoutMillis),然后再循环的时候判断如果这个Message有延迟,就调用nativePollOnce(ptr, nextPollTimeoutMillis)进行阻塞;
如果在阻塞这段时间里出现 无延迟message加入MessageQueen中又是怎么实现立即处理这个message的呢?
写的很清楚, New head, wake up the event queue if blocked
在这里p 是现在消息队列中的头部消息,我们看到| when < p.when 的时候它交换了放入message与原来消息队列头部P的位置,并且 needWake = mBlocked; (在next()中当消息为延迟消息的时候mBlocked=true),继续向下通过nativeWake 唤起线程;
(也就是说 有新的msg或者到了执行延时msg时就会唤醒线程)
IdleHandler(MessageQueue空闲执行的任务)
IdleHandler出现需要的场景:
MessageQueue 是一个基于消息触发时间的优先级队列,所以队列出现空闲存在两种场景。
- MessageQueue 为空,没有消息;
- MessageQueue 中最近需要处理的消息,是一个延迟消息(when>currentTime),需要滞后执行;
当MessageQueue在上述两种情况下,会触发IdleHandler创建并执行(如果有);
当前空闲任务IdleHandler完成后,会将nextPollTimeoutMillis(相当于loop的休眠时间)置为0,也就是不阻塞消息队列,后续有message将继续执行;当然要注意这里执行的代码同样不能太耗时,因为它是同步执行的,如果太耗时肯定会影响后面的 message 执行。
HandlerThread:
原理流程:
1.创建一个HandlerThread类对象,即新开一个工作线程
2.为当前线程创建一个Looper对象和MessageQueue对象,同时启动线程进行工作,开启looper消息循环;
3.创建工作线程的Handler,并将Hanlder绑定到Looper对象,从而绑定到HandlerThread的工作线程;
4.Handler开始发Message(原理和Handler一致)
5.结束工作线程,暂停线程的消息循环(quit/quitsafetly,后者更安全,退出消息循环时在不在意队列正在处理消息);
如下: 可以看到handlerThread其实就是thread然后直接start,在它自身run方法中封装了loop获取和启动等逻辑,方便后续handler的操作。
// 步骤1:创建HandlerThread实例对象
// 传入参数 = 线程名字,作用 = 标记该线程
HandlerThread mHandlerThread = new HandlerThread("handlerThread");
// 步骤2:启动线程
mHandlerThread.start();
// 步骤3:创建工作线程Handler & 复写handleMessage()
// 作用:关联HandlerThread的Looper对象、实现消息处理操作 & 与其他线程进行通信
// 注:消息处理操作(HandlerMessage())的执行线程 = mHandlerThread所创建的工作线程中执行
Handler workHandler = new Handler( handlerThread.getLooper() ) {
@Override
public boolean handleMessage(Message msg) {
...//消息处理
return true;
}
});
// 步骤4:使用工作线程Handler向工作线程的消息队列发送消息
// 在工作线程中,当消息循环时取出对应消息 & 在工作线程执行相关操作
// a. 定义要发送的消息
Message msg = Message.obtain();
msg.what = 2; //消息的标识
msg.obj = "B"; // 消息的存放
// b. 通过Handler发送消息到其绑定的消息队列
workHandler.sendMessage(msg);
// 步骤5:结束线程,即停止线程的消息循环
mHandlerThread.quit();
常见问题:
1.Handler明明在子线程中发送的消息了怎么会跑到主线程中?
在哪通过handler.sendMessage无所谓,重要的是Looper的位置,Looper在哪个线程执行的,handler的dispatchmessage回调就在哪个线程执行的,和在哪个线程发送消息无关;
2.Handler的消息如何在handleMessage中被接收到的呢?
在handler.sendmessage这一步中,通过msg.target = this,将handler和当前message进行绑定,这样就能在dispatchmessage(msg)找到对应的handler,并执行对应的handlemessage方法
3.new Handler(Looper.getMainLooper())和new Handler()有区别吗?
new handler()持有的是当前线程的Looper,而new handler(Looper.getMainLooper())持有的是主线程的Looper;
4.当next()没有消息了之后一直死循环,主线程会被卡死吗?
我们知道MessageQueue中,每次在next(),取message的时候,如果没有message了,他就会处于挂起状态(这样做的目的),当有消息到来,或者下一个消息的时间到达之后,都会唤醒线程,那么这个是如何实现的呢?
挂起线程
jni方法Java层定义挂起线程方法,方法位于MessageQueue#next()方法中
private native void nativePollOnce(long ptr, int timeoutMillis);
当消息队列为空或者delayTime还在等待时触发
-
底层行为:线程阻塞在
epoll_wait
系统调用上,释放 CPU 资源(省性能)。
线程唤醒
jni方法Java层定义唤醒线程方法,方法位于MessageQueue#enqueueMessage方法中
private native static void nativeWake(long ptr);
-
触发条件:
-
新消息插入到空队列时(即休眠中被唤醒)。
-
插入的消息是异步消息或延迟消息到期。
-
底层行为:向管道写入数据,epoll_wait
检测到可读事件,线程被唤醒。
这里用到了Linux pipe/epoll机制,此时主线程会释放CPU资源进入休眠状态,直到下个消息到达或者有事务发生,通过往pipe管道写端写入数据来唤醒主线程工作。这里采用的epoll机制,是一种IO多路复用机制,可以同时监控多个描述符,当某个描述符就绪(读或写就绪),则立刻通知相应程序进行读或写操作,本质同步I/O,即读写是阻塞的。 所以说,主线程大多数时候都是处于休眠状态,并不会消耗大量CPU资源。
主线程从休眠状态到唤醒状态,通过epoll机制;
选择 epoll
的原因是?
epoll机制好处
-
高效:相比轮询,
epoll
只在事件发生时唤醒线程。 -
低耗:休眠时几乎不占用 CPU。
-
精准:延迟消息通过
epoll_wait
的超时参数实现定时唤醒