Android开发教程——重新认识一下Handler

这篇文章 不是带着大家去了解Handler 工作原理等这些老生常谈的问题,是主要向大家介绍Handler 的阻塞原理和消息屏障机制,这里做个提示 可以让大家按需阅读。

Handler 可以说是App的心脏,推动着整个App所有事件的执行。接下来就一起探究下Handler的阻塞和消息平屏障。

阻塞机制

先理解下什么叫做阻塞?

比如我们定了一个外卖,我们不用一直问骑手外卖有没有送到,我们可以先继续做其他事情,骑手到了之后会给我们打电话。这个过程就是阻塞。

在MessageQueue中取出消息后,通过时间比较来计算出 需要阻塞的时间,如果没有时间,需要阻塞时间就赋值为-1

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 {
    }
} else {
    // No more messages.
    nextPollTimeoutMillis = -1;
}



然后通过nativePollOnce 来阻塞

nativePollOnce(ptr, nextPollTimeoutMillis);

这个 nativePollOnce 是个native方法,我们继续往下分析:

在这里我就不一一列出 贴出调用的代码了,就直接将调用流程 贴出来

nativePollOnce  
-> android_os_MessageQueue_nativePollOnce  (android_os_MessageQueue.cpp)
-> NativeMeaageQueue::pollOnce 
->Looper::pollOnce (Looper.cpp)
->Looper::pollInner->epoll_wait 

在调用过程中,可以看到 阻塞时间这个参数 最后传到的epoll_wait这个函数里面

Looper.cpp
int eventCount = epoll_wait(mEpollFd.get(), eventItems, EPOLL_MAX_EVENTS, timeoutMillis);

那就说明,阻塞是由epoll_wait完成的。那么什么是epoll ? 接下来,一点点的讲解。

在这里再向大家声明一个概念:

非阻塞忙轮询:

还是我们点了一个外卖,但是我们么现在特别饿,我们就需要骑手的手机号,然后一分钟给骑手打一个电话,这就是非阻塞忙轮询。

我们请求网络,从数据库读数据,只要处理数据都会涉及I/O,现在我们有多个I/O事件,那么我们该如何处理多个流呢?

多个线程 去处理多个IO流,效率会特别低,这是由CPU设计决定的,所有都会用一个线程去处理I/O

while (true){
    for ( i  -> stream[]) {
        if (i has data){
            read data until unavailable
        }
    }
}

我们可以像这样,用非阻塞忙轮询去处理多个IO,一直遍历每个流,当读到流中有数据的时候,就会处理数据,处理完之后 然后继续轮询。但是当所有流中都没数据的时候,cpu 就会在空转,白白浪费资源。此时应该让出cpu资源,所以有了select 机制。

while (true){
    select(stream[])
    for ( i  -> stream[]) {
        if (i has data){
            read data until unavailable
        }
    }
}

从select 可以知道,有IO事件发生了,然后再轮询流,来处理数据。没数据的时候,则在select处阻塞着。

虽然select可以知道有IO事件发生了,但是不知道是哪几个流,所以只能无差别的轮询所有流。这种无差别轮询显然也是一种资源浪费。

在linux2.6之后,出现了epoll机制

首先看一下epoll的定义

epoll 是linux 内核中的一种可拓展IO事件 处理机制,大量应用程序请时,能够获得较好的性能。

while (true){
   activite_stream[] = epoll_wait();
    for ( i  -> activity_stream[]) {
        read data until unavailable
    }
}

epoll 会将 有哪几个流发生了事件告诉我们,我们就可以对流处理数据,这样操作的每个流都是有数据的。直接将负责度由O(N)降低到了O(1)。

epoll 的用法只有三个方法:

int epoll_create(it size)

创建一个epoll 句柄,size用来告诉内核需要监听的数目一共有多大

int epoll_ctl(int epfd,int op,int fd, struct epoll_event *event);

这是epoll 的时间注册函数

int epoll_wait(int epfd,struct epoll_event * event ,int maxevents,int timeout);

这就是上面提到的阻塞了,参数 events 用来从内核得到事件的集合,maxevents 用来告诉内核 这个events有多大,这个maxevents 的值不能大于创建 epoll_create()时的size,参数timeout是超时事件(毫秒,0 会立即返回,-1将一直阻塞)

epoll 的创建和注册是在一个函数中

void Looper::rebuildEpollLocked() {
    ....
    mEpollFd.reset(epoll_create1(EPOLL_CLOEXEC));
    LOG_ALWAYS_FATAL_IF(mEpollFd < 0, "Could not create epoll instance: %s", strerror(errno));
    epoll_event wakeEvent = createEpollEvent(EPOLLIN, WAKE_EVENT_FD_SEQ);
    int result = epoll_ctl(mEpollFd.get(), EPOLL_CTL_ADD, mWakeEventFd.get(), &wakeEvent);
    ....
}

这个函数是在native 的 Looper的构造方法中调用的。

既然有阻塞,就得有唤醒

MessageQueue.java 
boolean enqueueMessage(Message msg, long when) {
            // We can assume mPtr != 0 because mQuitting is false.
            if (needWake) {
                nativeWake(mPtr);
            }
        }
        return true;
    }

在向队列塞消息的时候,可以看到 在这里有个唤醒操作,这个操作最后是调用到Looper.cpp 的wake方法中

void Looper::wake() {
    uint64_t inc = 1;
    ssize_t nWrite = TEMP_FAILURE_RETRY(write(mWakeEventFd.get(), &inc, sizeof(uint64_t)));
    if (nWrite != sizeof(uint64_t)) {
        if (errno != EAGAIN) {
            mWakeEventFd.get(), nWrite, strerror(errno));
        }
    }
}

唤醒的方法就是通过 linux pipe机制 向 监听的fd中写入数据,就会唤醒了。

总的来说就是,handler 在没有任务执行的时候,就会通过epoll机制阻塞,让出cpu资源,当向队列中发送消息的时候,就会唤醒向监听的文件描述符中写入数据,唤醒epoll , 来继续处理消息。

消息屏障

说完了阻塞机制,我们再说说消息屏障。

消息屏障就是屏障不重要的消息,优先执行重要的消息。

什么才是重要的消息呢? 例如 UI 的绘制,点击事件等。

handler 发送的消息分为三种:

  • 普通消息
  • 屏障消息
  • 异步消息

从MessageQueue中取消息的时候,可以看到这样一种情况:

  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());
}

我们都知道 message的target是用来分发数据的handler ,那tartget 怎么能为空呢?那是因为 target == null 就代表此消息为屏障消息。

屏障消息 就是 开始处理异步消息的标志,handler在收到屏障消息后,就开始处理异步消息,普通消息暂缓处理。

MessageQueue.java
private int postSyncBarrier(long when) {
    // Enqueue a new sync barrier token.
    // We don't need to wake the queue because the purpose of a barrier is to stall it.
    synchronized (this) {
        final int token = mNextBarrierToken++;
        final Message msg = Message.obtain();
        msg.markInUse();
        msg.when = when;
        msg.arg1 = token;

        Message prev = null;
        Message p = mMessages;
        if (when != 0) {
            while (p != null && p.when <= when) {
                prev = p;
                p = p.next;
            }
        }
        if (prev != null) { // invariant: p == prev.next
            msg.next = p;
            prev.next = msg;
        } else {
            msg.next = p;
            mMessages = msg;
        }
        return token;
    }
}

这就是发送一个屏障消息,也是通过时间进行排序,和其他消息唯一不同的就是没有target。

这个方法是在ViewRootImpl中调用的:

   void 
     () {
        if (!mTraversalScheduled) {
            mTraversalScheduled = true;
            //发送一个屏障消息
            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
            //执行一个UI刷新的任务
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
            notifyRendererOfFramePending();
            pokeDrawLockIfNeeded();
        }
    }

然后再看mChoreographer 任务的执行

最后会调用到这里

Choreographer.java
private void postCallbackDelayedInternal(int callbackType,
        Object action, Object token, long delayMillis) {
    synchronized (mLock) {
        .......
        if (dueTime <= now) {
            scheduleFrameLocked(now);
        } else {
            Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
            msg.arg1 = callbackType;
           //将消息设置为异步消息
            msg.setAsynchronous(true);
            mHandler.sendMessageAtTime(msg, dueTime);
        }
    }
}

这是发送一个异步消息,然后再看处理:

MessageQueue.java -> next 方法
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());
}
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 {
        .....
        return msg;
    }

在这里 当 target == null 的时候,也就是消息屏障开启的时候,取出 异步消息,然后返回,进行分发处理。

有开启屏障也就有关闭屏障:

private void removeCallbacksInternal(int callbackType, Object action, Object token) {
    synchronized (mLock) {
        mCallbackQueues[callbackType].removeCallbacksLocked(action, token);
        if (action != null && token == null) {
            mHandler.removeMessages(MSG_DO_SCHEDULE_CALLBACK, action);
        }
    }
}

将屏障消息移除,就可以正常处理普通消息了。

我们连贯一下, ViewRootImpl在处理UI 事件之前,先发送一个屏障消息,告诉handler优先处理异步消息,然后Choreographer发送异步消息(msg.setAsynchronous(true)),异步消息处理完之后,然后再发送一个移除屏障的消息。

Handler就是通过这种机制保障我们UI界面流畅刷新。

总结

至此,这篇文章就水完了,主要是讲了 handler的epoll阻塞机制 和handler的消息屏障。主要是让大家有更深层次的了解,不在局限于表面执行原理。

您的点赞收藏就是对我最大的鼓励! 欢迎关注我,分享Android干货,交流Android技术。 对文章有何见解,或者有何技术问题,欢迎在评论区一起留言讨论!最后给大家分享一些Android相关的视频教程,感兴趣的朋友可以去看看。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,098评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,213评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,960评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,519评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,512评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,533评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,914评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,804评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,563评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,644评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,350评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,933评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,908评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,146评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,847评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,361评论 2 342

推荐阅读更多精彩内容