当前位置: 首页 > article >正文

Android Handler消息机制完全解析-IdleHandler和epoll机制(四)

Android 消息机制Handler完全解析(一)

Android 消息机制Handler完全解析(二)

Android Handler消息机制-消息屏障(三)
经过前面的学习相信大家对Handler已经有了比较全面且深入的认识,我在写第一篇的时候就说过Handler相关的知识远比我们想象的要多,到这里大家是不是略有体会呢?今天和大家一起学习的是IdleHandler和epoll机制,这两个可能平时用到的不多,尤其是epoll,但是它的原理以及思想我们要清楚,有些面试可能会问到,有一句话叫"有剑不用和没有剑是两码事"。废话不多说我们开始学习今天的内容,通过本篇博客你将学习到以下内容

1.IdleHandler是什么

2.为什么要有IdleHandler即产生的背景或者有什么好处

3.IdleHandler的原理

4.IdleHandler如何使用

5.什么是阻塞和非阻塞轮询

6.epoll机制在handler消息机制中是怎么工作的

1.IdleHandler是什么,IdleHandler的作用

首先什么是IdleHandler,IdleHandler是MessageQueue中的一个接口

public final class MessageQueue {
    public static interface IdleHandler {
        boolean queueIdle();
    }
}

Idle有空闲的意思,IdleHandler在什么情况下会执行呢?当线程的MessageQueue空闲的时候(MessageQueue中没有Message或者下一次要执行的Message的时间还未到)就会去执行IdleHandler中指定的任务,它常用来做一些不是特别紧急的任务,比如资源的回收、延时加载等。

2.IdleHandler原理

IdleHandler平时项目中用到的不多,今天主要来看看它的原理,即它是怎么做到MessageQueue空闲的时候才执行我们指定的任务的。前几篇在Handler消息机制的时候我们知道针对MessageQueue中消息主要有两个操作分别是enqueueMessage(向MessageQueue插入消息)和next(从MessageQueue中取消息),因为它是当MessageQueue空闲的时候执行所以肯定是取消息的时候发现当前没有要执行的消息,而去执行IdleHandler中的任务,我们来看看MessageQueue#next方法

public final class MessageQueue {
    private final ArrayList<IdleHandler> mIdleHandlers = new ArrayList<IdleHandler>();
    private IdleHandler[] mPendingIdleHandlers;
    
    public void addIdleHandler(@NonNull IdleHandler handler) {
        if (handler == null) {
            throw new NullPointerException("Can't add a null IdleHandler");
        }
        synchronized (this) {
            mIdleHandlers.add(handler);
        }
    }
    
    Message next() {
        ...
        int pendingIdleHandlerCount = -1; // -1 only during first iteration
        int nextPollTimeoutMillis = 0;
        for (;;) {
            synchronized (this) {
               ...
               // 当mMessages为null或者有延时消息但是执行的时机还未到,则统计下当前IdleHandler的个数
                if (pendingIdleHandlerCount < 0
                        && (mMessages == null || now < mMessages.when)) {
                    pendingIdleHandlerCount = mIdleHandlers.size();
                }
                // 如果没有IdleHandler则继续下一次轮询
                if (pendingIdleHandlerCount <= 0) {
                    // No idle handlers to run.  Loop and wait some more.
                    mBlocked = true;
                    continue;
                }
                
                if (mPendingIdleHandlers == null) {
                    mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)];
                }
                // 将mIdleHandlers列表转换为数组
                mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);
            }
            
            // 遍历执行IdleHandler
            for (int i = 0; i < pendingIdleHandlerCount; i++) {
                final IdleHandler idler = mPendingIdleHandlers[i];
                mPendingIdleHandlers[i] = null; // release the reference to the handler
    
                boolean keep = false;
                try {
                    keep = idler.queueIdle();
                } catch (Throwable t) {
                    Log.wtf(TAG, "IdleHandler threw exception", t);
                }
                // 如果queueIdle方法返回false则执行之后将其从mIdleHanders中移除
                if (!keep) {
                    synchronized (this) {
                        mIdleHandlers.remove(idler);
                    }
                }
            }
    
            // 将空闲处理程序计数重置为0,这样我们就不会再次运行它们
            pendingIdleHandlerCount = 0;
        }
    }
}

可以看到这里首先定义了一个存放IdleHandler的列表mIdleHandlers,然后定义了一个addIdleHandler方法,此方法就是供我们开发者调用的,调用之后会把当前传递过来的IdleHandler对象添加到mIdleHandlers,接下来看下IdleHandler在next方法中什么时机下执行,在第22行的if语句判断可以看到当mMessages为null或者有延时消息但是执行的时机还未到时会将mIdlerHandlers.size()即IdleHandler的个数赋值给pendingIdleHandlerCount,如果pendingIdleHandlerCount>0说明当前有需要执行的IdleHandler,然后将mIdleHandlers转换为数组,并对其进行遍历执行。

这里有一点需要注意第47行keep = idler.queueIdle();会将queueIdle()方法的返回值赋值给keep,然后在第52行的if语句如果queueIdle方法的返回值为false则执行之后会将此IdleHandler对象移除。如果返回true的话则不移除,下次调用next方法如果条件满足仍然会执行

3.IdleHandler使用

IdleHandler的原理其实比较简单,接下来看看它的使用,这里就举个最简单的例子,在Activity中的onCreate方法中添加如下代码,运行就会打印相应日志

Looper.getMainLooper().getQueue().addIdleHandler(new MessageQueue.IdleHandler() {
    @Override
    public boolean queueIdle() {
        Log.d(TAG, "getMainLooper");
        return false;
    }
});

关于IdleHandler的使用场景遵循一个原则:在不影响其他任务,在消息队列空闲状态下执行

1.系统源码中 比如在ActivityThread中,就有一个名叫GcIdler的内部类,实现了IdleHandler接口。

它在queueIdle方法被回调时,会做强行GC的操作

2.一些第三方框架Glide 和 LeakCanary 等很多第三方框架也使用到 IdleHandler
如果大家想深入了解可以在自己查下资料,网上有很多资料。

4.常见面试题

1.IdleHandler 主要是在 MessageQueue 出现空闲的时候被执行,那么何时出现空闲?

MessageQueue 是一个基于消息触发时间的列表,所以列表出现空闲存在两种场景。

(1)MessageQueue为空,没有消息;

(2)MessageQueue 中最近需要处理的消息,是一个延迟消息(when>currentTime),需要滞后执行

2.IdleHandler 有什么用?

IdleHandler是Handler提供的一种在消息队列空闲时,执行任务的时机,当 MessageQueue 当前没有立即需要处理的消息时,会执行 IdleHandler;它常用来做一些不是特别紧急的任务,比如资源的回收、延时加载等。

3.MessageQueue 提供了 add/remove IdleHandler 的方法,是否需要成对使用

不是必须;IdleHandler.queueIdle() 的返回值,可以移除加入 MessageQueue 的 IdleHandler;

4.当 mIdleHanders 一直不为空时,为什么不会进入死循环?

只有在pendingIdleHandlerCount 为 -1 时,才会尝试执行 mIdleHander,pendingIdlehanderCount 在 next() 中初始时为 -1,执行一遍后被置为 0,所以不会重复执行

5.是否可以将一些不重要的启动服务,搬移到 IdleHandler 中去处理?

不建议;IdleHandler 的处理时机不可控,如果 MessageQueue 一直有待处理的消息,那么 IdleHander 的执行时机会很靠后

6.IdleHandler 的 queueIdle() 运行在那个线程?

queueIdle() 运行的线程,只和当前MessageQueue的Looper所在的线程有关;子线程一样可以构造Looper, 并添加IdleHandler

5.epoll机制

epoll机制涉及到linux底层的一些设计,因此大家不要深入了解,了解其原理即可。首先来看下百度百科的定义
epoll是Linux内核为处理大批量文件描述符而作了改进的poll,是Linux下多路复用IO接口select/poll的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率。什么鬼,感觉每个字都认识怎么连到一起就不知道啥意思了呢?想了解epoll机制首先要搞清楚两个概念:“阻塞"和"非阻塞轮询”。我举一个现实生活中的例子,通过这个例子你可能很容易理解"阻塞"和"非阻塞轮询"这两个概念。

假如你女朋友要来你家玩,由于之前没有来过你家,你不知道她什么时候到小区,所以你需要每隔一分钟给她打个电话问她到哪里了,这种方式就是非阻塞轮询。还有一种方式就是你女朋友告诉你到了小区之后给你打电话,因此你可以去睡觉了,因为你女朋友到了小区后会主动通知你。

非阻塞轮询:你需要每个一分钟给她打个电话问她到哪里了

阻塞:你女朋友告诉你到了小区之后给你打电话
到这里大家是不是对非阻塞轮询和阻塞有了进一步的认识?非阻塞轮询中的轮询就是每隔一定的时间间隔去执行相应的操作即使是无用功也要执行,就好比你女朋友到你家有2个小时的路程,你从她一出门就每隔一分钟打一个电话问她到了没有。而阻塞不一样,阻塞就是你知道你女朋友要来你家,来到你家小区之后你要做的动作是去小区门口接她,因为你女朋友到了小区之后会给你打电话所以你不用担心,你可以放心的去睡觉等待这个事件的到来。

再来看两段伪代码,进一步理解这两个概念,假如现在有n个I/O事件,我们怎么同时处理多个流呢?

非阻塞轮询

while(true) {
    for (i in stream[]) {
        read stream data
    }
}

通过以上这种方式如果所有的流中都没有数据,那么只会造成CPU的空转,只会白白浪费CPU

while(true) {
    active_stream[] = epoll_wait()
    for(i in active_stream[]) {
        read stream data
    }
}

这个epoll_wait是干嘛的呢?就是说如果当前没有I/O流需要处理它会进入阻塞状态,等待事件的到来,后面的for循环不会执行,当有I/O流需要执行时它会把哪个流发生了怎样的I/O事件通知给active_stream,此时我们对这些流的处理都是有意义的,这样是不是大大提升了效率。说了这么多,epoll机制在Handler消息机制中是怎么体现的呢?我们来看下MessageQueue的构造方法、next方法、enqueueMessage方法的源码

public final class MessageQueue {
    private long mPtr; // used by native code
    private native static long nativeInit();
    @UnsupportedAppUsage
    private native void nativePollOnce(long ptr, int timeoutMillis); /*non-static for callbacks*/
    private native static void nativeWake(long ptr);
    
    MessageQueue(boolean quitAllowed) {
        mQuitAllowed = quitAllowed;
        mPtr = nativeInit();
    }

    Message next() {
        final long ptr = mPtr;
        ...
        int nextPollTimeoutMillis = 0;
        for (;;) {
           ...
            //阻塞操作,当等待nextPollTimeoutMillis时长,或者消息队列被唤醒
            nativePollOnce(ptr, nextPollTimeoutMillis);
            synchronized (this) {
                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);
                    } 
                } 
            // While calling an idle handler, a new message could have been delivered
            // so go back and look again for a pending message without waiting.
            nextPollTimeoutMillis = 0;
        }
    }
        
    boolean enqueueMessage(Message msg, long when) {
        ...
            // We can assume mPtr != 0 because mQuitting is false.
            if (needWake) {
                // 唤醒
                nativeWake(mPtr);
            }
        }
        return true;
    }
}

上面对next方法的源码做了简化,可以看到在MessageQueue定义了一些native相关的方法,并且在MessageQueue中的构造方法中调用了nativeInit方法,并将返回值赋值给mPtr这个值可以理解为epoll的一个句柄或者标签。然后第18行可以看到调用了nativePollOnce传递了两个参数一个是ptr,一个是nextPollTimeoutMillis即阻塞的时间,进入阻塞之后它下面的流程就不会执行从而不会造成CPU的浪费(此时没有可执行的消息,下面的代码流程没有意义,对比下上面的现实生活中的例子)。除了等待nextPollTimeoutMillis时长之外,在enqueueMessage中如果需要唤醒会执行nativeWake方法进行唤醒停止阻塞。
好了,今天关于IdleHandler以及epoll机制的讲解就到这里了,这也是关于Handler的第四篇文章了,大家觉得有帮助的话帮忙点点赞。


http://www.kler.cn/a/370217.html

相关文章:

  • 2. CSS 中的单位
  • Pix2Pix:图像到图像转换的条件生成对抗网络深度解析
  • Docker使用 使用Dockerfile来创建镜像
  • 记录一下OpenCV Contrib 编译踩的坑
  • 大模型最新研究进展分析(市场规模、行业应用、技术趋势、当前挑战)
  • 回归算法、聚类算法、决策树、随机森林、神经网络
  • Spring Boot的核心优势及其应用详解
  • 基于SSM(spring+springmvc+mybatis)+MySQL开发的新闻推荐系统
  • STM32之外部中断旋转编码器
  • 20241028在荣品PRO-RK3566开发板的预置Android13下用iperf3测试AP6256的WIFI网速
  • Git创建和拉取项目分支的应用以及Gitlab太占内存,如何配置降低gitlab内存占用进行优化
  • 分体式智能网关在现代电力物联网中的优势有哪些?
  • Java SPI 机制详解
  • gaussdb hccdp实验练习03 GaussDB数据库应用程序开发实验
  • 2024年10月24日Github流行趋势
  • 磁盘空间不足导致postgreSQL启动失败
  • Axure使用动态面板制作新闻栏目高级交互
  • 【ChatGP】让ChatGPT解释和简化复杂的技术概念
  • 配置console口密码和远程登录
  • 面试官:Kafka 为什么那么快?
  • Android Input的流程和原理
  • 前端零基础入门到上班:【Day1】什么是前端?
  • 【Android】Kotlin教程(2)
  • MacOS上Homebrew 安装、配置、更改国内镜像源及使用教程
  • GEE APP:为土地分类图数据在地图上添加一个可视化图例
  • 算法备案全攻略:深度合成等五类算法的备案流程详解