如需转载请评论或简信,并注明出处,未经允许不得转载
目录
前言
之前写过一篇文章Android消息机制(Handler、Looper、MessageQueue),这篇文章主要是从Java层入手,分析了和我们实际开发最贴近的消息机制原理。自认为对java层的消息机制流程已经非常熟悉了,但是面试是被问到“Handler是如何实现延迟消息的?”,“为什么Looper.loop()中的死循环不会导致ANR?”这些问题的时候,还是不能完全说出个所以然来,所以就决定再去看看native层到底做了哪些事情。如果你对java层的消息机制还不是特别熟悉的话,建议先看上一篇文章哦
消息队列的创建
从上一章中,我们知道MessageQueue
是在Looper
的构造方法中创建的,但是我们没有对MessageQueue
做更深入的分析
Looper.java
private Looper(boolean quitAllowed) {
mQueue = new MessageQueue(quitAllowed);
mThread = Thread.currentThread();
}
跟进一下MessageQueue
的构造方法,看看里面做了什么
MessageQueue.java
MessageQueue(boolean quitAllowed) {
mQuitAllowed = quitAllowed;
mPtr = nativeInit();
}
这里有一个nativeInit()
方法,这是一个native层的方法
core/jni/android_os_MessageQueue.cpp
static jlong android_os_MessageQueue_nativeInit(JNIEnv* env, jclass clazz) {
NativeMessageQueue* nativeMessageQueue = new NativeMessageQueue();
...
//增加引用计数
nativeMessageQueue->incStrong(env);
//使用强制类型转换符reinterpret_cast把NativeMessageQueue指针强转成long类型并返回到java层
return reinterpret_cast<jlong>(nativeMessageQueue);
}
NativeMessageQueue::NativeMessageQueue() :
mPollEnv(NULL), mPollObj(NULL), mExceptionObj(NULL) {
//从当前线程的本地缓存(相当于ThreadLocal)拿到looper
mLooper = Looper::getForThread();
if (mLooper == NULL) {
//如果looper == null,创建一个looper加入到线程本地缓存中
mLooper = new Looper(false);
Looper::setForThread(mLooper);
}
}
我们发现,native层中也会创建一个Looper
和MessageQueue
,且native层的Looper
创建和获取方式和java层非常的相似。native层的Looper
与Java层的Looper
没有必然的关系,只是在native层重实现了一套类似功能的逻辑
system/core/libutils/Looper.cpp
再来看看Looper
的构造方法做了什么
Looper::Looper(bool allowNonCallbacks) :
mAllowNonCallbacks(allowNonCallbacks), mSendingMessage(false),
mPolling(false), mEpollFd(-1), mEpollRebuildRequired(false),
mNextRequestSeq(0), mResponseIndex(0), mNextMessageUptime(LLONG_MAX) {
//创建fd(文件操作符)
mWakeEventFd = eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC);
LOG_ALWAYS_FATAL_IF(mWakeEventFd < 0, "Could not make wake event fd: %s",
strerror(errno));
AutoMutex _l(mLock);
//重建管道
rebuildEpollLocked();
}
这个eventfd实际上就是一个文件描述符,那什么是文件描述符呢?
文件描述符:简称fd,它就是一个int值,又叫做句柄,在Linux中,打开或新建一个文件,它会返回一个文件描述符,读写文件需要使用文件描述符来指定待读写的文件,所以文件描述符就是指代被打开的文件,所有对这个文件的IO操作都要通过文件描述符
void Looper::rebuildEpollLocked() {
//关闭旧的管道
if (mEpollFd >= 0) {
close(mEpollFd);
}
//创建一个新的epoll文件描述符,并注册wake管道
mEpollFd = epoll_create(EPOLL_SIZE_HINT);
struct epoll_event eventItem;
memset(& eventItem, 0, sizeof(epoll_event));
//设置mWakeEventFd的可读事件(EPOLLIN)监听
eventItem.events = EPOLLIN;
eventItem.data.fd = mWakeEventFd;
//将唤醒事件fd(mWakeEventFd)添加到epoll文件描述符(mEpollFd),进行监听
int result = epoll_ctl(mEpollFd, EPOLL_CTL_ADD, mWakeEventFd, & eventItem);
//将各种事件,如键盘、鼠标等事件的fd添加到epoll文件描述符(mEpollFd),进行监听
for (size_t i = 0; i < mRequests.size(); i++) {
const Request& request = mRequests.valueAt(i);
struct epoll_event eventItem;
request.initEventItem(&eventItem);
int epollResult = epoll_ctl(mEpollFd, EPOLL_CTL_ADD, request.fd, & eventItem);
if (epollResult < 0) {
ALOGE("Error adding epoll events for fd %d while rebuilding epoll set: %s",
request.fd, strerror(errno));
}
}
}
}
rebuildEpollLocked()
方法的主要作用其实就是通过epoll机制设置了mWakeEventFd的可读事件和其他各种事件的监听
epoll机制是Linux最高效的I/O复用机制,使用一个文件描述符管理多个描述符
I/O复用机制:多人聊天室就是一种非常适合I/O多路复用的例子,可能会同时有很多个用户登录,但是不会同时在同一个时刻发言。如果用普通I/O模型,则需要开很多的线程,大部分线程是空闲的,而且在处理多个客户的消息的时候需要切换线程,对系统来讲也是比较重的。而使用I/O多路复用则可以重复使用一条线程,减少线程空闲和切换的情况
epoll模型主要就是三个部分
epoll_create:创建epoll文件描述符
epoll_ctl:添加文件描述符监听(如监听mWakeEventFd的EPOLLIN事件)
epoll_wait:阻塞监听add进来的描述符,只要其中任意一个或多个描述符可用或者超时就会返回(这个我们稍后会介绍)
在进一步分析消息机制的native层原理之前,我们可以先画一个基本的系统架构图,捋一捋他们之间的关系
发送消息
我们应用层调用Handler
的sendmessage()
、sendEmptyMessageDelayed()
、postDelayed()
、post()
最终都会调用到MessageQueue#enqueueMessage()
MessageQueue.java
boolean enqueueMessage(Message msg, long when) {
synchronized (this) {
....
//将消息延迟时间赋值给msg的成员变量
msg.when = when;
Message p = mMessages;
boolean needWake;
//① p == null,代表消息队列为空
//② when == 0,当我们调用sendMessageAtFrontOfQueue的时候
//③ when < p.when,新来的消息比消息队列中第一个消息还早
if (p == null || when == 0 || when < p.when) {
//插到头结点
msg.next = p;
mMessages = msg;
needWake = mBlocked;
} else {
//如果不能插到头结点,按时间将消息进行排序
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;
prev.next = msg;
}
if (needWake) {
//发送一个eventFd可读事件
nativeWake(mPtr);
}
}
return true;
}
从这个函数中我们了解到,所谓的延迟消息,并不是延迟去发送消息,而是延迟去处理消息。延迟消息和普通消息一样,都会在第一时间发送到消息队列中
core/jni/android_os_MessageQueue.cpp
static void android_os_MessageQueue_nativeWake(JNIEnv* env, jclass clazz, jlong ptr) {
NativeMessageQueue* nativeMessageQueue = reinterpret_cast<NativeMessageQueue*>(ptr);
nativeMessageQueue->wake();
}
void NativeMessageQueue::wake() {
mLooper->wake();
}
system/core/libutils/Looper.cpp
void Looper::wake() {
uint64_t inc = 1;
//使用write函数往mWakeEventFd写入字符inc,epoll就会收到可读事件,被唤醒
ssize_t nWrite = TEMP_FAILURE_RETRY(write(mWakeEventFd, &inc, sizeof(uint64_t)));
...
}
还记得这个mWakeEventFd
是什么吗?它就是我们一开始在native Looper
的构造方法中创建的用于可读事件通知的文件操作符
总结一下发送消息就是根据消息的时间戳顺序,往消息队列中插入消息,每插入一条消息,就会往mWakeEventFd写入字符inc,epoll就会收到可读事件,被唤醒
接下来我们看看epoll收到可读事件后,会发生什么?
读取消息
Looper.loop()
内部实际上调用的是MessageQueue#next()
来读取消息
MessageQueue.java
这里最重要的就是nativePollOnce()
方法,他是一个native层方法,它会一直阻塞,这里面就包含了epoll接收可读事件的相关逻辑
Message next() {
final long ptr = mPtr;
if (ptr == 0) {
return null;
}
...
//nextPollTimeoutMillis表示nativePollOnce的超时时间
int nextPollTimeoutMillis = 0;
for (;;) {
if (nextPollTimeoutMillis != 0) {
Binder.flushPendingCommands();
}
//这是个native层方法,会一直阻塞,直到下一条可用的消息返回
//ptr就是NativeMessageQueue的指针
nativePollOnce(ptr, nextPollTimeoutMillis);
synchronized (this) {
final long now = SystemClock.uptimeMillis();
Message msg = mMessages;
if (msg != null) {
if (now < msg.when) {
//如果消息触发时间还没到,计算还需要等多久
nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
} else {
//如果触发时间到了,取出消息
mMessages = msg.next;
msg.next = null;
return msg;
}
} else {
//nextPollTimeoutMillis设置成-1代表没有消息,nativePollOnce就会无限等待
nextPollTimeoutMillis = -1;
}
...
}
}
core/jni/android_os_MessageQueue.cpp
static void android_os_MessageQueue_nativePollOnce(JNIEnv* env, jobject obj,
jlong ptr, jint timeoutMillis) {
NativeMessageQueue* nativeMessageQueue = reinterpret_cast<NativeMessageQueue*>(ptr);
nativeMessageQueue->pollOnce(env, obj, timeoutMillis);
}
system/core/libutils/Looper.cpp
int Looper::pollOnce(int timeoutMillis, int* outFd, int* outEvents, void** outData) {
int result = 0;
for (;;) {
...
//当result不等于0时,就会跳出循环,返回到java层
if (result != 0) {
return result;
}
//处理内部轮询
result = pollInner(timeoutMillis);
}
}
该方法内部是一个死循环,核心在于调用了pollInner
方法,pollInner
方法返回一个int值result,代表着本次轮询是否成功处理了消息,当result不等于0时,就会跳出循环,返回到java层继续处理java层消息
接下来我们重点看一下pollInner()
做了什么,这是整个消息机制的核心
system/core/libutils/Looper.cpp
int Looper::pollInner(int timeoutMillis) {
//...
//事件集合(eventItems),EPOLL_MAX_EVENTS为最大事件数量,它的值为16
struct epoll_event eventItems[EPOLL_MAX_EVENTS];
//1.epoll_wait有四种情况
//① 当timeoutMillis == -1或timeoutMillis > 0,进入休眠等待
//② 如果有事件发生,且timeoutMillis == 0,就从管道中读取事件放入事件集合(eventItems)返回事件个数
//③ 如果休眠timeoutMillis时间后还没有被唤醒,就会返回0
//④ 如果出错,就会返回-1
int eventCount = epoll_wait(mEpollFd, eventItems, EPOLL_MAX_EVENTS, timeoutMillis);
....
//获取锁
mLock.lock();
//2.遍历事件集合(eventItems)
for (int i = 0; i < eventCount; i++) {
int fd = eventItems[i].data.fd;
uint32_t epollEvents = eventItems[i].events;
//如果文件描述符为mWakeEventFd
if (fd == mWakeEventFd) {
//并且事件类型为EPOLLIN(可读事件),这说明当前线程关联的管道的另外一端写入了新数据
if (epollEvents & EPOLLIN) {
//调用awoken方法不断的读取管道数据
awoken();
} else {
ALOGW("Ignoring unexpected epoll events 0x%x on wake event fd.", epollEvents);
}
} else {
//如果是其他文件描述符,就进行它们自己的处理逻辑
...
}
}
//3、下面是处理Native的Message
Done:;
mNextMessageUptime = LLONG_MAX;
//mMessageEnvelopes是一个Vector集合,它代表着native中的消息队列
while (mMessageEnvelopes.size() != 0) {
nsecs_t now = systemTime(SYSTEM_TIME_MONOTONIC);
//取出MessageEnvelope,MessageEnvelop有收件人Hanlder和消息内容Message
const MessageEnvelope& messageEnvelope = mMessageEnvelopes.itemAt(0);
//判断消息的执行时间
if (messageEnvelope.uptime <= now) {//消息到达执行时间
{
//获取native层的Handler
sp<MessageHandler> handler = messageEnvelope.handler;
//获取native层的消息
Message message = messageEnvelope.message;
mMessageEnvelopes.removeAt(0);
mSendingMessage = true;
//释放锁
mLock.unlock();
//通过MessageHandler的handleMessage方法处理native层的消息
handler->handleMessage(message);
}
mLock.lock();
mSendingMessage = false;
//result等于POLL_CALLBACK,表示某个监听事件被触发
result = POLL_CALLBACK;
} else {//消息还没到执行时间
mNextMessageUptime = messageEnvelope.uptime;
//跳出循环,进入下一次轮询
break;
}
}
//释放锁
mLock.unlock();
...
return result;
}
总结一下,pollInner
主要做了三件事情:
- 执行epoll_wait方法,等待事件发生或者超时(重要)
1)当timeoutMillis == -1或timeoutMillis > 0,进入休眠等待
2)如果有事件发生,且timeoutMillis == 0,就从管道中读取事件放入事件集合(eventItems)返回事件个数
3)如果休眠timeoutMillis时间后还没有被唤醒,就会返回0
4)如果出错,就会返回-1
- 遍历事件集合(eventItems),检测哪一个文件描述符发生了IO事件
遍历事件集合中,如果是mWakeEventFd
,就调用awoken方法不断的读取管道数据,直到清空管道,如果是其他的文件描述符发生了IO事件,让它们自己处理相应逻辑
- 处理native层的Message
只要epoll_wait
方法返回后,都会进入Done
标记位的代码段,就开始处理处理native层的Message
,处理完native层消息后,又会返回到java层处理java层的消息
用一个图总结一下前面的过程
到此为止,native源码分析的差不多了,对于native层消息机制,我们已经具备了不错的理论基础,接下来开始回答文章一开始的问题吧
如何实现延迟消息
我们先来看看应用层是如何调用的,这个大家应该都非常清楚
SystemClock.uptimeMillis()
是从开机到当前的毫秒数,也就是说最终传给Looper::pollInner(int timeoutMillis)
的时间是以开机时间为基准的一个时间戳
public final boolean sendMessageDelayed(Message msg, long delayMillis){
if (delayMillis < 0) {
delayMillis = 0;
}
return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
}
我们上面介绍epoll_wait的时候,说过这个方法的四种情况
1)当timeoutMillis == -1或timeoutMillis > 0,进入休眠等待
2)如果有事件发生,且timeoutMillis == 0,就从管道中读取事件放入事件集合(eventItems)返回事件个数
3)如果休眠timeoutMillis时间后还没有被唤醒,就会返回0
4)如果出错,就会返回-1
所以当epoll收到一条延迟消息的时候,timeoutMillis > 0,所以就符合第一条,这时候就会epoll进入休眠等待,直到休眠了timeoutMillis的时间,就会被唤醒并返回。也就是说此时nativePollOnce不再阻塞,就会从消息队列中取出消息进行分发。所以,消息延迟就是基于epoll_wait的超时时间来实现的
为什么Looper.loop()中的死循环不会导致ANR
首先,我们得需要知道ANR是怎么产生的,有的人可能会说主线程执行了耗时操作、Activity
的最长执行时间是5秒、BroadcastReceiver
的最长执行时间则是10秒、Service
的最长执行时间是20秒等等这些。但是我们今天要说的,其实是要更深一步
ANR是怎么产生的
ANR的时候,我们会看到系统会弹出一个对话框,我们先找到这个对话框是从哪里弹出来的
ActivityManagerService.java
final void appNotResponding(ProcessRecord app, ActivityRecord activity,
ActivityRecord parent, boolean aboveSystem, final String annotation) {
...
Message msg = Message.obtain();
msg.what = ActivityManagerService.SHOW_NOT_RESPONDING_UI_MSG;
msg.obj = new AppNotRespondingDialog.Data(app, activity, aboveSystem);
mService.mUiHandler.sendMessage(msg);
}
这里的SHOW_NOT_RESPONDING_UI_MSG
消息最终会执行到AppErrors#handleShowAnrUi()
,从而打开一个AppNotRespondingDialog
AppErrors.java
void handleShowAnrUi(Message msg) {
...
Dialog dialogToShow = new AppNotRespondingDialog(mService, mContext, data);
...
dialogToShow.show()
}
接下来我们来看看这个超时是如何触发的,这里拿Service举例
ActiveService.java
这是Service
开始启动的方法
private final void realStartServiceLocked(ServiceRecord r,
ProcessRecord app, boolean execInFg){
...
bumpServiceExecutingLocked(r, execInFg, "create")
...
app.thread.scheduleCreateService(...)
...
}
private final void bumpServiceExecutingLocked(ServiceRecord r, boolean fg, String why) {
...
scheduleServiceTimeoutLocked(r.app);
...
}
static final int SERVICE_TIMEOUT = 20*1000;
// How long we wait for a service to finish executing.
static final int SERVICE_BACKGROUND_TIMEOUT = SERVICE_TIMEOUT * 10;
void scheduleServiceTimeoutLocked(ProcessRecord proc) {
...
Message msg = mAm.mHandler.obtainMessage(
ActivityManagerService.SERVICE_TIMEOUT_MSG);
msg.obj = proc;
//发送一个超时的消息,这个超时消息如果最终被接收,将会执行appNotResponding()
mAm.mHandler.sendMessageDelayed(msg,
proc.execServicesFg ? SERVICE_TIMEOUT : SERVICE_BACKGROUND_TIMEOUT);
}
通过看源码我们发现,实际上就是在Service
启动的时候,会往消息队列中发送一个20s的延迟消息。如果这个消息在20s之内没有被remove(),那就会弹出ANR的弹窗!为了不弹出这个ANR的弹窗,如果Android系统交给你们设计,你们会怎么做呢?
我想你们应该很容易想到吧,Andorid系统在Service启动完成后就会remove消息,同样的道理,输入事件,BroadcastReceiver启动等都会发送一个延迟消息,之后等到成功响应后就会remove这个延迟消息
我们通过两个图来对比一下启动Service正常的情况和发生ANR的情况
looper为什么不会ANR
了解了ANR发生的原理,我们再来回答looper()
为什么不会ANR应该会更准确一些
我认为这里要回答两点,一点是epoll的原理,还有一点就是ANR的触发原理
在Linux中,文件、socket、管道(pipe)等可以进行IO操作的对象都可以称之为流,既然是IO流,那肯定会有两端:read端和write端,我们可以创建两个文件描述符wiretFd和readFd,对应read端和write端,当流中没有数据时,读线程就会阻塞(休眠)等待,当写线程通过wiretFd往流的wiret端写入数据后,readFd对应的read端就会感应到,唤醒读线程读取数据,大概就是这样的一个读写过程。读线程进入阻塞后,并不会消耗CPU时间,这是epoll机制高效的原因之一
Activity
启动事件时一个Message
,ANR也是一个延迟Message
,当Activity
启动完成后就移除ANR的Message
,这是多么自然的事情,怎么会导致ANR呢?
其他问题
主线程中进行耗时操作一定会ANR吗
通过阅读源码可以找到四种ANR类型(下面推荐了四篇文章,感兴趣的可以跟着代码看一看)
-
Activity
对输入事件在5秒内没有处理完毕(https://blog.csdn.net/chewbee/article/details/72670038) -
BroadcastReceiver
的onReceive
函数时10秒内没有执行完毕(https://blog.csdn.net/chewbee/article/details/72672095) -
Service
的各个生命周期函数时20秒内没有执行完毕(https://blog.csdn.net/chewbee/article/details/72671665) -
ContentProvider
执行相关操作的超时时间是20s(https://blog.csdn.net/chewbee/article/details/72670603)
如果触发了上面这些情景,就会发生ANR,反之即使在主线程做了耗时操作,你也看不到ANR弹窗
可以做下面这个实验
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
这样仅仅会让这个Activtiy延迟了10s才真正启动,但是由于Activity没有接收输入时间,所以不会出现ANR弹窗。但是如果在Service中sleep超过20秒,就会出现ANR弹窗,这里Service和Activity其实是有区别的
总结
本文分析了native层MessageQueue
、Looper
的创建过程,也通过了解epoll的唤醒和阻塞机制,解决了“Handler是如何实现延迟消息的?”,“为什么Looper.loop()中的死循环不会导致ANR?”这两个问题。看源码有时候虽然辛苦,但是看完之后感觉还是有不少收获的。所以大家以后有什么非常不解的问题,不妨研究研究源码吧