欢迎大家前往腾讯云社区,获取更多腾讯海量技术实践干货哦~

作者:汪毅雄

导语: 本文讲述的是Android的消息机制原理,从Java到Native代码进行了梳理,并结合其中使用到的Epoll模型予以介绍。

Android的消息传递,是系统的核心功能,对于如何使用相信大家都已经相当熟悉了,这里简单提一句。我们可以粗糙的认为消息机制中关键的几个类的功能如下:

Handler:消息处理者

Looper:消息调度者

MessageQueue:存放消息的地方

使用过程:

Looper.prepare > #$%^^& > Looper.loop(死循环) --- loop到一个消息 > Handler处理

好了,我们直接看源码吧。

Java层

消息机制是伴随线程的,也就是说上面的几个类在可以在任何一个线程中都有实例的。

先看Looper吧。以主线程为例,Android进程在初始化,会调用prepareMainLooper

public static void prepareMainLooper() {
prepare(false);
synchronized (Looper.class) {
...
sMainLooper = myLooper();
}
}
 private static void prepare(boolean quitAllowed) {
...
sThreadLocal.set(new Looper(quitAllowed));
}
 private Looper(boolean quitAllowed) {
mQueue = new MessageQueue(quitAllowed);
mThread = Thread.currentThread();
}

以上几个方法就是Looper初始化,如果是主线程Looper会创建一个不可退出的MessageQueue,并把looper实例放入线程独立(ThreadLocal)变量中。

Looper#loop

public static void loop() {
final Looper me = myLooper();
final MessageQueue queue = me.mQueue;
for (;;) {
Message msg = queue.next();
if (msg == null) {
return;
}
...
try {
msg.target.dispatchMessage(msg);
}
...
msg.recycleUnchecked();
}
}

Looper prepare后就可以loop了,loop非常简单,一直去queue中拿消息就好了,拿到了交给target也就是Handler处理。大家有可能会奇怪这种死循环,执行起来不会太sb粗暴了吗?其实这个解决方式在queue.next!!!后面再讲。

Handler#dispatchMessage

public void dispatchMessage(Message msg) {
if (msg.callback != null) {
handleCallback(msg);
} else {
if (mCallback != null) {
if (mCallback.handleMessage(msg)) {
return;
}
}
handleMessage(msg);
}
}

handler收到后,如果发现message的callback不为空,则只处理callback。(提一句,我们用的很多的handler.post(Runnable),其实这个Runnable就是这里的callback,也就是说post的Runnable实质上是一个优先级很高的Message),如果没有则尝试交给handler本身的callback处理(handler初始化的时候可以用callback方式构造),再没有才到我们常用的handleMessage方法,这里就是我们经常重写的方法。

再说说消息的发送,一般handler会调用sendMessage方法,但是最终这个方法还是会跑到这里

private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
msg.target = this;
if (mAsynchronous) {
msg.setAsynchronous(true);
}
return queue.enqueueMessage(msg, uptimeMillis);
}

再交给MessageQueue

boolean enqueueMessage(Message msg, long when) {
。。。
synchronized (this) {
。。。
Message prev;
for (;;) {
prev = p;
p = p.next;
if (p == null || when < p.when) {
break;
}
。。。
}
msg.next = p; // invariant: p == prev.next
prev.next = msg;
if (needWake){
nativeWake(mPtr);
}
}
return true;
}

MessageQueue会把消息插入队列,并依次改变队列中各个消息的指针。

咦,好像只用Java层貌似就能把整个消息机制说通了,native代码在哪儿?有何用呢?

但是,刚才提到了Looper初始化的时候也会新建一个MessageQueue

MessageQueue(boolean quitAllowed) {
mQuitAllowed = quitAllowed;
mPtr = nativeInit();
}

好了,我们第一个native方法出来了。这时候我们可以猜得到,MessageQueue才是整个消息机制的核心!

Native层

接上面Java层的代码,MessageQueue构造的时候会调一个nativeInit。

static jlong android_os_MessageQueue_nativeInit(JNIEnv* env, jclass clazz) {
NativeMessageQueue* nativeMessageQueue = new NativeMessageQueue();
。。。
}
NativeMessageQueue::NativeMessageQueue() :
mPollEnv(NULL), mPollObj(NULL), mExceptionObj(NULL) {
mLooper = Looper::getForThread();
if (mLooper == NULL) {
mLooper = new Looper(false);
Looper::setForThread(mLooper);
}
}

native层调用init方法后,会在native层构建一个native Looper!来看看native Looper的初始化

Looper::Looper(bool allowNonCallbacks) :
mAllowNonCallbacks(allowNonCallbacks), mSendingMessage(false),
mPolling(false), mEpollFd(-), mEpollRebuildRequired(false),
mNextRequestSeq(), mResponseIndex(), mNextMessageUptime(LLONG_MAX) {
mWakeEventFd = eventfd(, EFD_NONBLOCK | EFD_CLOEXEC);
...
rebuildEpollLocked();
}

这里创建了一个eventfd,代码来自最新的8.0,这部分和5.0 pipe管道的mWakeReadPipeFd和mWakeWritePipeFd稍微有点不一样,前者是等待/响应,后者是读取/写入。只是android选取方式的不同而已,这块就不细说。

void Looper::rebuildEpollLocked() {
。。。
mEpollFd = epoll_create(EPOLL_SIZE_HINT);
LOG_ALWAYS_FATAL_IF(mEpollFd < , "Could not create epoll instance: %s", strerror(errno));
struct epoll_event eventItem;
memset(& eventItem, , sizeof(epoll_event));
eventItem.events = EPOLLIN;
eventItem.data.fd = mWakeEventFd;
int result = epoll_ctl(mEpollFd, EPOLL_CTL_ADD, mWakeEventFd, & eventItem);
。。。
}

再到rebuildEpollLocked这个方法中,可以看到通过epoll_create创建了一个epoll专用的文件描述符,EPOLL_SIZE_HINT表示mEpollFd上能监控的最大文件描述符数。最后调用epoll_ctl监控mWakeEventFd文件描述符的Epoll事件,即当mWakeEventFd中有内容可读时,就唤醒当前正在等待的线程.。

这里不了解的人可能听着晕,上面这么一大段一句话概括就是:Android native层用了Epoll模型。什么是Epoll模型呢?我先简单介绍一下。

Epoll(必看!!!)

为什么要引入呢?

在Looper.loop的时候提到了,android不会简单粗暴地真的执行啥都没干的死循环。刚才说了,问题出在queue.next。Epoll干的事就是: 如果你的queue中没有消息可执行了,好了你可以歇着了,等有消息的我再告诉你。这个queue.next就是“阻塞”(休眠)在这里。

Epoll简单介绍

1、传统的阻塞型I/O(一边写,一边读),一个线程只能处理一个一个IO流。

2、如果一个线程想要处理多个流,可以采用了非阻塞、轮询I/O方式,但是传统的非阻塞处理多个流的时候,会遍历所有流,但是如果所有流都没数据,就会白白浪费CPU。
。。。
于是出现了select和epoll两种常见的代理方式。

3、select就是那种无差别轮询的代理方式。epoll可以理解为Event poll,也就是说代理者会代理流的时候也伴随着事件,因此有了对应事件,就可以避免无差别轮询了。

4、其通常的操作有:epoll_create(创建一个epoll)、epoll_ctl(往epoll中增加/删除某一个流的某一个事件)、epoll_wait(在一定时间内等待事件的发生)

eventItem.events = EPOLLIN;
eventItem.data.fd = mWakeEventFd;
int result = epoll_ctl(mEpollFd, EPOLL_CTL_ADD, mWakeEventFd, & eventItem);

好了,我们结合Looper初始化的代码来读一下epoll在这里干了什么吧。

我直接翻译了:往mEpollFd代理中、注册、一个叫mWakeEventFd流、的数据流入事件(EPOLLIN)

这样大家应该懂了吧。。。

接上MessageQueue在初始化后,在native创建了一个Looper。
我们继续消息的发送和提取在native层的表现。其实native层主要负责的是消息的调度,比如说何时阻塞、何时唤醒线程,避免CPU浪费。

native发送

发送在native比较简单,handler发送消息后,会到MessageQueue的enqueueMessage,此时在线程阻塞的情况下,会调用nativeWake来唤起线程。

void NativeMessageQueue::wake() {
mLooper->wake();
}
 void Looper::wake() {
。。。
uint64_t inc = ;
ssize_t nWrite = TEMP_FAILURE_RETRY(write(mWakeEventFd, &inc, sizeof(uint64_t)));
if (nWrite != sizeof(uint64_t)) {
。。。。
}
}

这里TEMP_FAILURE_RETRY是一个宏定义,顾名思义,就是不断地尝试往mWakeEventFd流里面写一个无用数据直到成功,以此来唤醒queue.next。这部分就不多说了。

native消息提取

也就是queue.next

Message next() {
。。。
for (;;) {
。。。
nativePollOnce(ptr, nextPollTimeoutMillis);
。。。
}
}

可以看到,又是一个死循环(阻塞)。继续往下看

void NativeMessageQueue::pollOnce(JNIEnv* env, jobject pollObj, int timeoutMillis) {
。。。
mLooper->pollOnce(timeoutMillis);
。。。
}

mLooper->pollOnce

mLooper->pollInner

int Looper::pollInner(int timeoutMillis) {
。。。
int result = POLL_WAKE;
mPolling = true;
struct epoll_event eventItems[EPOLL_MAX_EVENTS];
int eventCount = epoll_wait(mEpollFd, eventItems, EPOLL_MAX_EVENTS, timeoutMillis); mPolling = false;
mLock.lock(); for (int i = ; i < eventCount; i++) {
int fd = eventItems[i].data.fd;
uint32_t epollEvents = eventItems[i].events;
if (fd == mWakeEventFd) {
if (epollEvents & EPOLLIN) {
awoken();
} else {
ALOGW("Ignoring unexpected epoll events 0x%x on wake event fd.", epollEvents);
}
}
。。。
}
。。。
mLock.unlock();
。。。
return result;
}

在这里,我们注意到epoll_wait方法,这里会得到一段时间内(结合消息计算得来的)收到的事件个数,这里对于queue来说就是空闲(阻塞)状态。过了这个时间后,看看事件数,如果为0,则意味着超时。否则,遍历所有的事件,看看有没有mWakeEventFd,且是EPOLLIN事件的,有的话就真正唤醒线程、解除空闲状态。

消息机制在native层的主要表现就是这些。

最后,画了一个粗糙、且不太准确图仅供参考学习

相关阅读


此文已由作者授权腾讯云技术社区发布,转载请注明原文出处

深入理解 Android 消息机制原理的更多相关文章

  1. 深入理解Android消息机制

    在日常的开发中,Android 的消息机制作为系统运行的根本机制之一,显得十分的重要. 从 Handler 发送消息开始 查看源码,Handler的post.send方法最终都会走到 public f ...

  2. Android Handler 消息机制原理解析

    前言 做过 Android 开发的童鞋都知道,不能在非主线程修改 UI 控件,因为 Android 规定只能在主线程中访问 UI ,如果在子线程中访问 UI ,那么程序就会抛出异常 android.v ...

  3. Android消息机制不完全解析(上)

        Handler和Message是Android开发者常用的两个API,我一直对于它的内部实现比较好奇,所以用空闲的时间,阅读了一下他们的源码.    相关的Java Class: androi ...

  4. 史上最详细的Android消息机制源码解析

    本人只是Android菜鸡一个,写技术文章只是为了总结自己最近学习到的知识,从来不敢为人师,如果里面有不正确的地方请大家尽情指出,谢谢! 606页Android最新面试题含答案,有兴趣可以点击获取. ...

  5. Android 消息机制 (Handler、Message、Looper)

    综合:http://blog.csdn.net/dadoneo/article/details/7667726 与 http://android.tgbus.com/Android/androidne ...

  6. Android开发之漫漫长途 ⅥI——Android消息机制(Looper Handler MessageQueue Message)

    该文章是一个系列文章,是本人在Android开发的漫漫长途上的一点感想和记录,我会尽量按照先易后难的顺序进行编写该系列.该系列引用了<Android开发艺术探索>以及<深入理解And ...

  7. Android开发之漫漫长途 Ⅶ——Android消息机制(Looper Handler MessageQueue Message)

    该文章是一个系列文章,是本人在Android开发的漫漫长途上的一点感想和记录,我会尽量按照先易后难的顺序进行编写该系列.该系列引用了<Android开发艺术探索>以及<深入理解And ...

  8. 每日一问:Android 消息机制,我有必要再讲一次!

    坚持原创日更,短平快的 Android 进阶系列,敬请直接在微信公众号搜索:nanchen,直接关注并设为星标,精彩不容错过. 我 17 年的 面试系列,曾写过一篇名为:Android 面试(五):探 ...

  9. Android消息机制:Looper,MessageQueue,Message与handler

    Android消息机制好多人都讲过,但是自己去翻源码的时候才能明白. 今天试着讲一下,因为目标是讲清楚整体逻辑,所以不追究细节. Message是消息机制的核心,所以从Message讲起. 1.Mes ...

随机推荐

  1. MySql sql按时间分组

    select DATE_FORMAT(f.upload_time,'%Y%u') weeks,count(*),sum(p.download_times),sum(p.collection_times ...

  2. MongoDB关系与数据库引用

    MongoDB关系: MongoDB 的关系表示多个文档之间在逻辑上的相互联系.文档间可以通过嵌入和引用来建立联系. 1. 嵌入关系: 形式:把一个文档嵌入到另一个文档中. 优点:数据保存在单一的文档 ...

  3. 如何从零绘制k线图 -- 原生js canvas图表绘制

    样式如下图 源码地址: https://github.com/sutianbinde/charts 编写这个需要具备canvas基础,如果没有canvas基础可以学习我前面的cnavas基础博客. 具 ...

  4. 压缩感知重构算法之压缩采样匹配追踪(CoSaMP)

    压缩采样匹配追踪(CompressiveSampling MP)是D. Needell继ROMP之后提出的又一个具有较大影响力的重构算法.CoSaMP也是对OMP的一种改进,每次迭代选择多个原子,除了 ...

  5. JPA 映射单向多对一的关联关系

    1.首先在多的一端加入一的一端的实体类 //映射单向n-1的关联关 //使用@ManyToOne 来映射多对一的关系 //使用@JoinColumn 来映射外键/可以使用@ManyToOne的fetc ...

  6. 基于Lua脚本解决实时数据处理流程中的关键问题

    摘要 在处理实时数据的过程中需要缓存的参与,由于在更新实时数据时并发处理的特点,因此在更新实时数据时经常产生新老数据相互覆盖的情况,针对这个情况调查了Redis事务和Lua脚本后,发现Redis事务并 ...

  7. 我的Spring学习记录(五)

    在我的Spring学习记录(四)中使用了注解的方式对前面三篇做了总结.而这次,使用了用户登录及注册来对于本人前面四篇做一个应用案例,希望通过这个来对于我们的Spring的使用有一定的了解. 1. 程序 ...

  8. 0_Simple__simpleAssert + 0_Simple__simpleAssert_nvrtc

    在核函数中使用强制终止函数 assert().并且在静态代码和运行时编译两种条件下使用. ▶ 源代码:静态使用 #include <windows.h> #include <stdi ...

  9. 分布式框架Dubbo入门

    Dubbo简介 Dubbo是一个Alibaba开源额分布式服务框架,致力于提供高性能和透明化的RPC远程服务调用方案,以及SOA服务治理方案.dubbo就是个服务框架,只有在分布式的时候,才有dubb ...

  10. 详解tomcat的连接数与线程池

    前言 在使用tomcat时,经常会遇到连接数.线程数之类的配置问题,要真正理解这些概念,必须先了解Tomcat的连接器(Connector). 在前面的文章 详解Tomcat配置文件server.xm ...