Handler原理
- 前言
- 1. Handler作用
- 2. Handler概述
- 3. 核心类
- 一、Handler源码分析
- 1.创建Handler
- 2.发送消息
- 3.取消息
- 4.消息处理
- 5.线程切换
- 二、相关内容
- 1.Handler原理
- 2.epoll机制
- 2.1什么是epoll机制?
- 2.2 select/poll/epoll
- 2.3 Handler中epoll是如何实现消息队列的阻塞和唤醒?
- 2.4 epoll高效的本质:
- 2.5 epoll扩展内容
- 3.ThreadLocal
- 4.异步消息与消息屏障
- 补充:Choreographer
- 5.消息池Message.obtain()
- 5.内存泄漏
- 三、常见问题
- Q1:Looper死循环为什么不会导致线程卡死?子线程中维护Looper在消息队列无消息的时候处理方案是怎么样的?
- Q2:描述下什么是epoll机制?
- Q3:关于ThreadLocal,你是怎么理解的?
- Q4:Message有哪几种创建方式?哪种效果更好?为什么?
- Q5:为什么不能在子线程更新UI?
- Q6:为什么主线程可以直接new Handler?其它子线程可以吗?怎么做?
- Q7:一个线程有几个Looper?如何保证?又可以有几个Handler?
- Q8:Handler内存泄漏的原因,其它内部类为什么没有这个问题?
- Q9:Handler中生产者-消费者设计模式你理解不?
- Q10:既然存在多个Handler往MessageQue中添加数据(发消息时各个Handler处于不同线程),内部如何保证安全?
- 多个Handler发送消息;Looper怎么知道要把消息给哪个Handler处理?
- Q11:使用Handler的postDelay()后消息队列会发生什么变化?
- 参考
前言
1. Handler作用
Handler作用:线程切换,线程异步消息处理。
Q:Android为什么要有handler呢?
Android UI操作并不是线程安全的,并且这些操作必须在UI线程执行。
如果非要在子线程中更新UI,那会出现什么情况呢?
android.view.ViewRoot$CalledFromWrongThreadException:
Only the original thread that created a view hierarchy can touch its views.
很容易抛一个CalledFromWrongThreadException异常。
如果在子线程访问UI线程,Android提供了以下的方式:
Activity.runOnUiThread(Runnable)
View.post(Runnable)
View.postDelayed(Runnable, long)
Handler
2. Handler概述
异步消息处理线程启动后会进入一个无限的循环体之中,每循环一次,从其内部的消息队列中取出一个消息,然后回调相应的消息处理函数,执行完成一个消息后则继续循环。若消息队列为空,线程则会阻塞等待。
3. 核心类
- Handler
- Looper
- Mesage
- MessageQue
Handler负责发送消息,Looper负责的就是创建一个MessageQueue,然后进入一个无限循环体不断从该MessageQueue中读取Mesage。
一、Handler源码分析
1.创建Handler
创建两个Handler对象,一个在主线程中创建,一个在子线程中创建,代码如下所示:
import android.os.Bundle;
import android.os.Handler;
public class MainActivity extends AppCompatActivity {
private Handler handler1;
private Handler handler2;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
handler1 = new Handler();
new Thread(new Runnable() {
@Override
public void run() {
handler2 = new Handler();
}
}).start();
}
}
运行程序,你会发现,在子线程中创建的Handler是会导致程序崩溃的:
我们尝试在子线程中先调用一下Looper.prepare()呢,代码如下所示:
new Thread(new Runnable() {
@Override
public void run() {
Looper.prepare();
handler2 = new Handler();
}
}).start();
果然这样就不会崩溃了。
为什么不调用Looper.prepare()就不行呢?我们来看下Handler的源码(基于android-30),Handler的构造函数如下所示:
在第224行调用了Looper.myLooper()方法获取了一个Looper对象,如果Looper对象为空,则会抛出一个运行时异常。
什么时候Looper对象会为空呢?接着看Looper.myLooper()中的代码:
这个方法非常简单,就是从sThreadLocal对象中取出Looper。如果sThreadLocal中有Looper存在就返回Looper,如果没有Looper存在就返回空,通过注释也可以看出。
接着查找是在哪里给sThreadLocal设置Looper,发现是Looper.prepare()方法
可以看到,首先判断sThreadLocal中是否已经存在Looper了,如果还没有则创建一个新的Looper设置进去。这样也就完全解释了为什么我们要先调用Looper.prepare()方法,才能创建Handler对象。同时也可以看出每个线程中最多只会有一个Looper对象。
Q:这里你可能会有个疑问,主线程中的Handler也没有调用Looper.prepare()方法,为什么就没有崩溃呢?
这是由于在程序启动的时候,系统已经帮我们自动调用了Looper.prepare()方法。查看ActivityThread中的main()方法,代码如下所示:
public static void main(String[] args) {
Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "ActivityThreadMain");
// CloseGuard defaults to true and can be quite spammy. We
// disable it here, but selectively enable it later (via
// StrictMode) on debug builds, but using DropBox, not logs.
CloseGuard.setEnabled(false);
Environment.initForCurrentUser();
// Set the reporter for event logging in libcore
EventLogger.setReporter(new EventLoggingReporter());
// Make sure TrustedCertificateStore looks in the right place for CA certificates
final File configDir = Environment.getUserConfigDirectory(UserHandle.myUserId());
TrustedCertificateStore.setDefaultUserDirectory(configDir);
Process.setArgV0("<pre-initialized>");
Looper.prepareMainLooper();
// Find the value for {@link #PROC_START_SEQ_IDENT} if provided on the command line.
// It will be in the format "seq=114"
long startSeq = 0;
if (args != null) {
for (int i = args.length - 1; i >= 0; --i) {
if (args[i] != null && args[i].startsWith(PROC_START_SEQ_IDENT)) {
startSeq = Long.parseLong(
args[i].substring(PROC_START_SEQ_IDENT.length()));
}
}
}
ActivityThread thread = new ActivityThread();
thread.attach(false, startSeq);
if (sMainThreadHandler == null) {
sMainThreadHandler = thread.getHandler();
}
if (false) {
Looper.myLooper().setMessageLogging(new
LogPrinter(Log.DEBUG, "ActivityThread"));
}
// End of event ActivityThreadMain.
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
Looper.loop();
throw new RuntimeException("Main thread loop unexpectedly exited");
}
可以看到,在第20行调用了Looper.prepareMainLooper()方法,而这个方法又会再去调用Looper.prepare()方法,代码如下所示:
我们应用程序的主线程开启的时候就会创建一个Looper对象,从而不需要再手动去调用Looper.prepare()方法了。
这样基本就将Handler的创建过程完全搞明白了,总结一下就是在主线程中可以直接创建Handler对象,而在子线程中需要先调用Looper.prepare()才能创建Handler对象。
2.发送消息
看完了如何创建Handler之后,接下来我们看一下如何发送消息,new出一个Message对象,然后可以使用setData()方法或arg参数等方式为消息携带一些数据,再借助Handler将消息发送出去就可以了,示例代码如下:
new Thread(new Runnable() {
@Override
public void run() {
Message message = handler1.obtainMessage();
message.arg1 = 1;
Bundle bundle = new Bundle();
bundle.putString("data", "data");
message.setData(bundle);
handler1.sendMessage(message);
}
}).start();
这里Handler到底是把Message发送到哪里去了呢?为什么之后又可以在Handler的handleMessage()方法中重新得到这条Message呢?看来又需要通过阅读源码才能解除我们心中的疑惑了:
调用了sendMessageDelayed:
接着调用了sendMessageAtTime,这个方法的源码如下所示:
sendMessageAtTime()方法接收两个参数,其中msg参数就是我们发送的Message对象,而uptimeMillis参数则表示发送消息的时间,它的值等于自系统开机到当前时间的毫秒数再加上延迟时间,如果你调用的不是sendMessageDelayed()方法,延迟时间就为0,然后将这两个参数都传递到MessageQueue的enqueueMessage()方法中。
这个MessageQueue又是什么东西呢?其实从名字上就可以看出了,它是一个消息队列,用于将所有收到的消息以队列的形式进行排列,并提供入队和出队的方法。这个类是在Looper的构造函数中创建的,因此一个Looper也就对应了一MessageQueue。
那么enqueueMessage()方法毫无疑问就是入队的方法了,我们来看下这个方法的源码:
(Handler中提供了很多个发送消息的方法,最终都会辗转调用到enqueueMessage()方法中)
boolean enqueueMessage(Message msg, long when) {
synchronized (this) {
//仅保留核心代码
msg.markInUse();
msg.when = when;
Message p = mMessages;
boolean needWake;
if (p == null || when == 0 || when < p.when) {
// New head, wake up the event queue if blocked.
msg.next = p;
mMessages = msg;
needWake = mBlocked;
} else {
// Inserted within the middle of the queue. Usually we don't have to wake
// up the event queue unless there is a barrier at the head of the queue
// and the message is the earliest asynchronous message in the queue.
needWake = mBlocked && p.target == null && msg.isAsynchronous();
Message prev;
for (;;) {
prev = p;
p = p.next;
if (p == null || when < p.when) {
break;
}
if (needWake && p.isAsynchronous()) {
needWake = false;
}
}
msg.next = p; // invariant: p == prev.next
prev.next = msg;
}
}
return true;
}
MessageQueue并没有使用一个集合把所有的消息都保存起来,它只使用了一个mMessages对象表示当前待处理的消息。然后观察上面的代码我们就可以看出,所谓的入队其实就是将所有的消息按时间来进行排序,这个时间就是msg.when。具体的操作方法就根据时间的顺序调用msg.next,从而为每一个消息指定它的下一个消息是什么。当然如果你是通过sendMessageAtFrontOfQueue()方法来发送消息的,它也会调用enqueueMessage()来让消息入队,只不过时间为0,这时会把mMessages赋值为新入队的这条消息,然后将这条消息的next指定为刚才的mMessages,这样也就完成了添加消息到队列头部的操作。
3.取消息
入队操作我们就已经看明白了,那出队操作是在哪里进行的呢?这个就需要看一看Looper.loop()方法的源码了,如下所示:
public static final void loop() {
Looper me = myLooper();
MessageQueue queue = me.mQueue;
while (true) {
Message msg = queue.next(); // might block
if (msg != null) {
if (msg.target == null) {
return;
}
if (me.mLogging!= null) me.mLogging.println(
">>>>> Dispatching to " + msg.target + " "
+ msg.callback + ": " + msg.what
);
msg.target.dispatchMessage(msg);
if (me.mLogging!= null) me.mLogging.println(
"<<<<< Finished to " + msg.target + " "
+ msg.callback);
msg.recycle();
}
}
}
可以看到,这个方法从第4行开始,进入了一个死循环,然后不断地调用的MessageQueue的next()方法,我想你已经猜到了,这个next()方法就是消息队列的出队方法。不过由于这个方法的代码稍微有点长,我就不贴出来了,它的简单逻辑就是如果当前MessageQueue中存在mMessages(即待处理消息),就将这个消息出队,然后让下一条消息成为mMessages,否则就进入一个阻塞状态,一直等到有新的消息入队。继续看loop()方法的第14行,每当有一个消息出队,就将它传递到msg.target的dispatchMessage()方法中,那这里msg.target又是什么呢?其实就是Handler啦,你观察一下上面sendMessageAtTime()方法的第6行就可以看出来了。
ps:这里msg.target通过target将Handler存入Message,是为了解决在多个Hander的情况无法找到处理当前消息的Handler问题。实际上是一种架构设计上的妥协,我们常见的Hander内存泄漏问题也是源于此。最终导致Activity无法及时回收:
Thread–>Looper–>MessageQue–>Message.target–>mHandler–>Activity
4.消息处理
那么发送消息后,最终又是怎么调用到handleMessage的呢?接下来看一下Handler中dispatchMessage()方法的源码,如下所示:
在第101行进行判断,如果mCallback不为空,则调用mCallback的handleMessage()方法,否则直接调用Handler的handleMessage()方法,并将消息对象作为参数传递过去。这样我相信大家就都明白了为什么handleMessage()方法中可以获取到之前发送的消息了吧!
5.线程切换
我们接下来继续分析一下,为什么使用异步消息处理的方式就可以对UI进行操作了呢?这是由于Handler总是依附于创建时所在的线程,比如我们的Handler是在主线程中创建的,而在子线程中又无法直接对UI进行操作,于是我们就通过一系列的发送消息、入队、出队等环节,最后调用到了Handler的handleMessage()方法中,这时的handleMessage()方法已经是在主线程中运行的,因而我们当然可以在这里进行UI操作了。整个异步消息处理流程的示意图如下图所示:
另外除了发送消息之外,我们还有以下几种方法可以在子线程中进行UI操作:
- Handler的post()方法
- View的post()方法
- Activity的runOnUiThread()方法
我们先来看下Handler中的post()方法,代码如下所示:
public final boolean post(Runnable r)
{
return sendMessageDelayed(getPostMessage(r), 0);
}
原来这里还是调用了sendMessageDelayed()方法去发送一条消息啊,并且还使用了getPostMessage()方法将Runnable对象转换成了一条消息,我们来看下这个方法的源码:
private final Message getPostMessage(Runnable r) {
Message m = Message.obtain();
m.callback = r;
return m;
}
在这个方法中将消息的callback字段的值指定为传入的Runnable对象。咦?这个callback字段看起来有些眼熟啊,喔!在Handler的dispatchMessage()方法中原来有做一个检查,如果Message的callback等于null才会去调用handleMessage()方法,否则就调用handleCallback()方法。那我们快来看下handleCallback()方法中的代码吧:
private final void handleCallback(Message message) {
message.callback.run();
}
竟然就是直接调用了一开始传入的Runnable对象的run()方法。因此在子线程中通过Handler的post()方法进行UI操作就可以这么写:
public class MainActivity extends Activity {
private Handler handler;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
handler = new Handler();
new Thread(new Runnable() {
@Override
public void run() {
handler.post(new Runnable() {
@Override
public void run() {
// 在这里进行UI操作
}
});
}
}).start();
}
}
虽然写法上简洁很多,但是原理是完全一样的,我们在Runnable对象的run()方法里更新UI,效果完全等同于在handleMessage()方法中更新UI。
然后再来看一下View中的post()方法,代码如下所示:
public boolean post(Runnable action) {
Handler handler;
if (mAttachInfo != null) {
handler = mAttachInfo.mHandler;
} else {
ViewRoot.getRunQueue().post(action);
return true;
}
return handler.post(action);
}
原来就是调用了Handler中的post()方法。
最后再来看一下Activity中的runOnUiThread()方法,代码如下所示:
public final void runOnUiThread(Runnable action) {
if (Thread.currentThread() != mUiThread) {
mHandler.post(action);
} else {
action.run();
}
}
如果当前的线程不等于UI线程(主线程),就去调用Handler的post()方法,否则就直接调用Runnable对象的run()方法。
通过以上所有源码的分析,我们已经发现了,不管是使用哪种方法在子线程中更新UI,其实背后的原理都是相同的,必须都要借助异步消息处理的机制来实现。
二、相关内容
1.Handler原理
Handler发送消息后,通过MessageQue将Message入队存放,再通过Looper开启循环取出消息,取出消息后再调用Handler分发和处理消息。
sendMessage–》enqueueMessage–》Looper.loop–》dispatchMessage–》handleMessage
2.epoll机制
2.1什么是epoll机制?
epoll是linux系统一种高效的IO多路复用机制,采用事件驱动的方式实现。可以同时监控多个描述符,当某个描述符就绪(读或写就绪),则立刻通知相应程序进行读或写操作,本质同步I/O,即读写是阻塞的。
在linux 没有实现epoll事件驱动机制之前,我们一般选择用select或者poll等IO多路复用的方法来实现并发服务程序。在linux新的内核中,有了一种替换它的机制,就是epoll。
2.2 select/poll/epoll
在 select/poll中,通过遍历文件描述符,而epoll则是通过监听回调的的机制。
2.3 Handler中epoll是如何实现消息队列的阻塞和唤醒?
阻塞:通过调用 epoll_wait 方法,当前线程进入休眠,等待被唤醒。
唤醒:对 mWakeEventFd 发起写入操作,从而唤醒 nativePollOnce 中通过 epoll_wait 进入休眠的线程。
参考:Handler 中的 epoll 源码分析:https://jishuin.proginn.com/p/763bfbd35a2d
epoll_wait这里也是整个Android消息机制阻塞的真正位置,阻塞等待期间可以保证线程进入休眠状态,不占用CPU资源,同时监听所注册的事件。
2.4 epoll高效的本质:
1.减少用户态和内核态之间的文件句柄拷贝;
2.减少对可读可写文件句柄的遍历。
具体实现是:通过红黑树和双链表数据结构,并结合回调机制,造就了epoll的高效。
2.5 epoll扩展内容
IO多路复用:
IO 多路复用是一种同步 IO 模型,实现一个线程可以监视多个文件句柄。一旦某个文件句柄就绪,就能够通知应用程序进行相应的读写操作,没有文件句柄就绪时会阻塞应用程序,交出 cpu。
与多进程和多线程技术相比,IO 多路复用技术的最大优势是系统开销小,系统不必为每个 IO 操作都创建进程或线程,也不必维护这些进程或线程,从而大大减小了系统的开销。
epoll优势:
1.监视的描述符数量不受限制,所支持的FD上限是最大可以打开文件的数目,具体数目可以cat /proc/sys/fs/file-max查看,一般来说这个数目和系统内存关系很大,以3G的手机来说这个值为20-30万。
2.IO性能不会随着监视fd的数量增长而下降。epoll不同于select和poll轮询的方式,而是通过每个fd定义的回调函数来实现的,只有就绪的fd才会执行回调函数。
ps:如果没有大量的空闲或者死亡连接,epoll的效率并不会比select/poll高很多。但当遇到大量的空闲连接的场景下,epoll的效率大大高于select/poll。
select、poll、epoll三种多路复用机制适用场景?
Q: 设想一下如下场景:有100万个客户端同时与一个服务器进程保持着TCP连接。而每一时刻,通常只有几百上千个TCP连接是活跃的(事实上大部分场景都是这种情况)。如何实现这样的高并发?
在select/poll时代,服务器进程每次都把这100万个连接告诉操作系统(从用户态复制句柄数据结构到内核态),让操作系统内核去查询这些套接字上是否有事件发生,轮询完后,再将句柄数据复制到用户态,让服务器应用程序轮询处理已发生的网络事件,这一过程资源消耗较大,因此,select/poll一般只能处理几千的并发连接。
epoll:要实现上面说是的场景,只需要在进程启动时建立一个epoll对象,然后在需要的时候向这个epoll对象中添加或者删除连接。同时,epoll_wait的效率也非常高,因为调用epoll_wait时,并没有一股脑的向操作系统复制这100万个连接的句柄数据,内核也不需要去遍历全部的连接。
epoll高效处理事件:
epoll通过红黑树和双链表数据结构,并结合回调机制,实现高效地处理事件:
检测到有事件发生时,
发生的事件都会挂载在红黑树中,重复添加的事件就可以通过红黑树高效的识别出来(红黑树的插入时间效率是lgn,其中n为树的高度)。
另外,添加到epoll中的事件都会与设备(网卡)驱动程序建立回调关系,也就是说,当相应的事件发生时会调用这个回调方法。发生的事件添加到rdlist双链表中。
epoll机制工作模式,及高效的原因?
epoll高效的原因:
当调用 epoll_wait检查是否有发生事件的连接时,只是检查 eventpoll对象中的 rdllist双向链表是否有 epitem元素而已,如果 rdllist链表不为空,则把这里的事件复制到用户态内存中,同时将事件数量返回给用户。因此,epoll_wait的效率非常高。epoll_ctl在向 epoll对象中添加、修改、删除事件时,从 rbr红黑树中查找事件也非常快,也就是说,epoll是非常高效的,它可以轻易地处理百万级别的并发连接。
eventfd文件描述符:
eventfd 是 Linux 系统中一个用来通知事件的文件描述符,基于内核向用户空间应用发送通知的机制,可以有效地被用来实现用户空间事件驱动的应用程序。
简而言之:eventfd 就是用来触发事件通知,它只有一个系统调用接口:
int eventfd(unsigned int initval, int flags);
表示打开一个 eventfd 文件并返回文件描述符,支持 epoll/poll/select 操作。
之所以要了解 eventfd,是因为在 Android 6.0 后,Handler 底层替换为 eventfd/epoll 实现。而 6.0 之前是由 pipe/epoll 实现的。
Android中Handler为什么使用epoll?根本原因是什么?
一种说法是:当通过 postDelayed 发送延时消息后,会调用 epoll_wait 的 timeout 进行等待,利用了 epoll_wait 函数的挂起,达到了延时的目的,时间到后阻塞结束,epoll_wait 返回,线程被重新唤醒并获得CPU资源。
第二种说法是:为了省电,虽然 Looper 无限循环,但是没活干了就释放 CPU 资源进入休眠状态,并不会消耗大量资源。应该不是根本原因,使用 Java wait / notify 也可以达到目的(wait释放锁,sleep不释放锁)。
还有一种说法是:为了可以同时处理 native 侧消息。
到底根本原因是什么呢?
我认为是为了处理native侧的消息。
更详细一点,为了处理键盘鼠标轨迹球等输入设备的消息。Android基于linux内核,是以事件为驱动的,将各种输入设备被抽象成了文件,监控这些文件描述符的I/O事件正是epoll的拿手好戏。
epoll由来?
其实研究到最后,我才悟出了为什么epoll要叫epoll,因为它和原来poll机制的最大区别就是改善成了event(事件)驱动,这个e如果我没猜错的话应该代表的就是event。
关于Linux系统调用为什么可以挂起且不占用CPU时间片,CPU的定时器和中断又是怎么实现的?那可能就涉及到硬件知识了。
3.ThreadLocal
ThreadLocal是什么?
ThreadLocal起作用的根源是Thread类:每个线程都有一个threadLocals 属性,它是一个ThreadLocal.ThreadLocalMap 类型的Map集合。而ThreadLocal是维护ThreadLocal.ThreadLocalMap这个属性的一个工具类。
ThreadLocal具体实现:
当变量被访问时,ThreadLocal在访问变量的每个线程中都创建了一个副本变量(即每个线程的threadLocals属性),每个线程可以访问自己内部的副本变量,且该副本只能由当前 Thread 使用,从而实现线程间的数据隔离。
ThreadLocal本质是采用空间换时间的方式,避免多线程资源抢占的耗时。
ThreadLocal原理:
ThreadLocalMap 类相当于一个Map,key 是 ThreadLocal 本身,value 就是我们的值。
当我们通过 threadLocal.set(new Integer(123)); ,我们就会在这个线程中的 threadLocals 属性中放入一个键值对,key 是这个threadLocal.set(new Integer(123)) 的 threadlocal,value就是值new Integer(123)。
当我们通过 threadlocal.get() 方法的时候,首先会根据这个线程得到这个线程的 threadLocals 属性,然后由于这个属性放的是键值对,我们就可以根据键 threadlocal 拿到值。
ThreadLocal在Handler中作用:
利用ThreadLocal将Looper对象与当前线程进行关联(把Looper存到Thread对象map中), 实现Looper–Thread一一对应。
保证一个线程的Looper唯一,存到线程
Looper的生命周期,是当前线程的生命周期
一个线程只有一个Looper,可以有多个Handler。
4.异步消息与消息屏障
什么是消息屏障?
当要处理的消息为消息屏障,则延迟执行后续的普通消息,优先执行异步消息,常用在绘制任务的处理。消息屏障使得消息处理有了优先级。
有些博文说它是同步屏障,其实两者是同一个东西。
屏障消息如何插入消息队列?
同步屏障是通过MessageQueue的postSyncBarrier方法插入到消息队列的。从这个方法可以知道:
屏障消息和普通消息的区别在于屏障没有tartget,普通消息有target是因为它需要将消息分发给对应的target,而屏障不需要被分发,它就是用来挡住普通消息来保证异步消息优先处理的。
屏障和普通消息一样可以根据时间来插入到消息队列中的适当位置,并且只会挡住它后面的同步消息的分发。
postSyncBarrier返回一个int类型的数值,通过这个数值可以撤销屏障。
postSyncBarrier方法是私有的,如果我们想调用它就得使用反射。
插入普通消息会唤醒消息队列,但是插入屏障不会。
为什么要有消息屏障?
在Android系统中存在一个VSync垂直同步信号,它主要负责每16ms更新一次屏幕展示,如果用户同步消息在16ms内没有执行完成,那么VSync消息的更新操作就无法执行在用户看来就出现了掉帧或卡顿的情况,为此Android开发要求每个消息的执行需要限制在16ms之内完成。
但是消息队列中可能会包含多个同步消息,假如当前主线程消息队列有10个同步消息,每个同步消息要执行10ms,总共也就需要执行100ms,这段时间内就会有近7帧无法正常刷新展示,应用执行过程中遇到这种情况还是很普遍的。
Android系统设计时自然也会考虑到这种情况,同步消息会导致延迟主要原因在于排队等候,如果消息发送后不必排队等待直接就执行就能够解决消息延迟问题。Android系统中的异步消息就是专门解决消息处理延迟的问题,它需要配合同步屏障(SyncBarrier)一起工作,在发送异步消息的时候向消息队列投放同步屏障对象,消息队列会返回同步屏障的token,此时消息队列中的同步消息都会被暂停处理,优先执行异步消息处理,等异步消息处理完成再通过消息队列移除token对应的同步屏障,消息队列继续之前暂停的同步消息处理。
主线程发送异步消息及发送消息屏障流程?
源码分析:
(1)ViewRootImpl发消息屏障到主线程消息队列,Choreographer把绘制任务放入队列,并申请VSync信号;
(2)Choreographer的内部类FrameDisplayEventReceiver提供接收VSync信号以及申请VSync信号的方法;
(3)当FrameDisplayEventReceiver接收到VSync信号,往主线程发送异步消息,消息任务就是自己本身;
(4)MessageQueue取到异步消息进行处理;
(5)FrameDisplayEventReceiver.run方法被执行,遍历绘制任务队列,以此执行;
(6)最后移除消息屏障;
(7)VSync信号是SurfaceFlinger实现并定时发送;
详细可参考:https://www.jianshu.com/p/c1f6e1181e8e
补充:Choreographer
该类中文名字编舞者,它负责通过内部类的父类DisplayEventReceiver.scheduleVsync注册VSync信号,通过内部类FrameDisplayEventReceiver.onVsync接收信号。
5.消息池Message.obtain()
5.内存泄漏
Q:既然Handler容易引发内存泄漏,Google为什么不对此进行改进?
通过target将Handler存入Message,是为了解决在多个Hander的情况无法找到处理当前消息的Handler问题。实际上是一种架构设计上的妥协。
三、常见问题
Q1:Looper死循环为什么不会导致线程卡死?子线程中维护Looper在消息队列无消息的时候处理方案是怎么样的?
1、主线程本身就是需要一直运行的,因为要处理各个View,界面变化。所以需要这个死循环来保证主线程一直执行下去,不会被退出。
2、真正会卡死的操作是在某个消息处理的时候操作时间过长,导致掉帧、ANR,而不是loop方法本身。
3、在主线程以外,会有其他的线程来处理接受其他进程的事件,比如Binder线程(ApplicationThread),会接受AMS发送来的事件
4、在收到跨进程消息后,会交给主线程的Hanlder再进行消息分发。所以Activity的生命周期都是依靠主线程的Looper.loop,当收到不同Message时则采用相应措施,比如收到msg=H.LAUNCH_ACTIVITY,则调用ActivityThread.handleLaunchActivity()方法,最终执行到onCreate方法。
5、当没有消息的时候,会阻塞在loop的queue.next()中的nativePollOnce()方法里,此时主线程会释放CPU资源进入休眠状态,直到下个消息到达或者有事务发生。所以死循环也不会特别消耗CPU资源。
当消息不可用或者没有消息的时候就会阻塞在next方法,而阻塞的办法是通过pipe/epoll机制。
Q2:描述下什么是epoll机制?
epoll机制是一种IO多路复用的机制,具体逻辑就是一个进程可以监视多个描述符,当某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作,这个读写操作是阻塞的。在Android中,会创建一个Linux管道(Pipe)来处理阻塞和唤醒。
- 当消息队列为空,管道的读端等待管道中有新内容可读,就会通过epoll机制进入阻塞状态。
- 当有消息要处理,就会通过管道的写端写入内容,唤醒主线程。
Q3:关于ThreadLocal,你是怎么理解的?
在每个线程中都有一个threadLocals变量,这个变量存储着ThreadLocal和对应的需要保存的对象。
这样带来的好处就是,在不同的线程,访问同一个ThreadLocal对象,但是能获取到的值却不一样。
其实就是其内部获取到的Map不同,Map和Thread绑定,所以虽然访问的是同一个ThreadLocal对象,但是访问的Map却不是同一个,所以取得值也不一样。
好处:线程之间进行隔离。
Q4:Message有哪几种创建方式?哪种效果更好?为什么?
new Message和message.obtain方式,message.obtain方式更好,内部消息池会进行复用,避免创建过多对象。
Q5:为什么不能在子线程更新UI?
因为Android中的UI控件不是线程安全的,如果多线程访问UI控件那还不乱套了。
那为什么不加锁呢?
会降低UI访问的效率。本身UI控件就是离用户比较近的一个组件,加锁之后自然会发生阻塞,那么UI访问的效率会降低,最终反应到用户端就是这个手机有点卡。
太复杂了。本身UI访问时一个比较简单的操作逻辑,直接创建UI,修改UI即可。如果加锁之后就让这个UI访问的逻辑变得很复杂,没必要。
所以,Android设计出了 单线程模型 来处理UI操作,再搭配上Handler,是一个比较合适的解决方案。
子线程访问UI的 崩溃原因 和 解决办法?
崩溃发生在ViewRootImpl类的checkThread方法中,其实就是判断了当前线程 是否是 ViewRootImpl创建时候的线程,如果不是,就会崩溃。
而ViewRootImpl创建的时机就是界面被绘制的时候,也就是onResume之后,所以如果在子线程进行UI更新,就会发现当前线程(子线程)和View创建的线程(主线程)不是同一个线程,发生崩溃。
解决办法有三种:
在新建视图的线程进行这个视图的UI更新,主线程创建View,主线程更新View。
在ViewRootImpl创建之前进行子线程的UI更新,比如onCreate方法中进行子线程更新UI。
子线程切换到主线程进行UI更新,比如Handler、view.post方法。
Q6:为什么主线程可以直接new Handler?其它子线程可以吗?怎么做?
可以,但要调用Looper.prepare()方法;主线程启动时候就创建了MainLooper,构造方法中默认调用了Looper.prepare()方法。
Q7:一个线程有几个Looper?如何保证?又可以有几个Handler?
一个,通过ThreadLocal中的ThreadLocal存储当前线程ID和Looper对象保证。可以有多个Handler。
Looper、MessageQueue、线程是一一对应关系,而他们与Handler是可以一对多的。
Q8:Handler内存泄漏的原因,其它内部类为什么没有这个问题?
message.target持有Handler引用;内存泄漏是因为长生命周期对象持有了短生命周期对象的引用,Handler内存泄漏,内部类持有外部类的引用只是表象,源头是由于message.target持有Handler引用,导致Thread持有了Activity的引用,Activity无法及时回收。其它内部类虽然持有了外部类的引用,但只要它的实例持有对象的生命周期不大于外部类对象,就不会造成内存泄漏。
Q9:Handler中生产者-消费者设计模式你理解不?
Handler机制就是一个生产者消费者模式。可以这么理解,Handler发送消息,它就是生产者,生产的是一个个Message。Looper可以理解为消费者,在其loop()方法中,死循环从MessageQueue取出Message进行处理。而MessageQueue就是缓冲区了,Handler产生的Message放到MessageQueue中,Looper从MessageQueue取出消息。
Q10:既然存在多个Handler往MessageQue中添加数据(发消息时各个Handler处于不同线程),内部如何保证安全?
Looper创建时,会创建一个MessageQueue,且是唯一对应的
这也就说明一个Thread,Looper,MessageQueue都是唯一对应的关系
那么在添加消息时,synchronized (this) 的this 就是MessageQueue,而根据对应关系,这里加锁,其实就等于锁住了当前线程。就一个线程内算多个Handler同时添加消息,他们也会被锁限制,从而保证了消息添加的有序性,取消息同理
多个Handler发送消息;Looper怎么知道要把消息给哪个Handler处理?
target标志
在Handler发送消息的时候,会把当前Handler保存到Message的target变量里,当处理消息的时候,就可以直接取出Handler并处理消息;
Q11:使用Handler的postDelay()后消息队列会发生什么变化?
无论是即时消息还是延迟消息,都是计算出具体的时间,然后作为消息的when字段进程赋值。
然后在MessageQueue中找到合适的位置(安排when小到大排列),并将消息插入到MessageQueue中。
这样,MessageQueue就是一个按照消息时间排列的一个链表结构。
参考
https://blog.csdn.net/guolin_blog/article/details/9991569 郭霖,Android异步消息处理机制完全解析
https://www.cnblogs.com/lao-liang/p/5073257.html 《深入理解Android内核设计思想》
https://www.jianshu.com/p/c1f6e1181e8e 编舞者Choreographer,绘制任务处理流程
https://jishuin.proginn.com/p/763bfbd35a2d Handler 中的 epoll 源码分析
常见Handler问题