Android开发教程

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

2021-12-18  本文已影响0人  蜗牛是不是牛

这篇文章 不是带着大家去了解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相关的视频教程,感兴趣的朋友可以去看看。

上一篇下一篇

猜你喜欢

热点阅读