《你知道android的MessageQueue.IdleHandler吗?》

AI权益加码!Claude Code、Cursor等20+工具免费用! 购周边限时加赠Coding Plan Lite,畅享主流AI工具!学习进阶更高效! 阅读详情

本文来自于腾讯Bugly公众号(weixinBugly),未经作者同意,请勿转载,原文地址:http://mp.weixin.qq.com/s/KpeBqIEYeOzt_frANoGuSg

yangu

导语 干货!干货!或许可以是一种处理问题的新思路哟~~   

前言

我们知道android是基于Looper消息循环的系统,我们通过Handler向Looper包含的MessageQueue投递Message, 不过我们常见的用法是这样吧?

new Handler(Looper.getMainLooper()).post(new Runnable() {
@Override
    public void run() {/do something } 
});

一般我们比较少接触MessageQueue, 其实它内部的IdleHandler接口有很多有趣的用法,首先看看它的定义

/**
     * Callback interface for discovering when a thread is going to block
     * waiting for more messages.
     */
    public static interface IdleHandler {
        /**
         * Called when the message queue has run out of messages and will now
         * wait for more.  Return true to keep your idle handler active, false
         * to have it removed.  This may be called if there are still messages
         * pending in the queue, but they are all scheduled to be dispatched
         * after the current time.
         */
        boolean queueIdle();
    }

简而言之,就是在looper里面的message暂时处理完了,这个时候会回调这个接口,返回false,那么就会移除它,返回true就会在下次message处理完了的时候继续回调,让我们看看它有哪些有趣的用法吧~~

一.提供一个android没有的声明周期回调时机

如果有这种需求,想要在某个activity绘制完成去做一些事情,那这个时机是什么时候呢?有同学可能觉得onResume()是一个合适的机会,不是可是这个onResume() 真的是各种绘制都已经完成才回调的吗?No, too naive ~~

这里写图片描述

你看谷老师说了,onStart是用户可见,onResume是用户可交互,谷老师可没说onResume是绘制完成吧~那么android那些耗时的measure, layout, draw是在什么时候执行的呢?它们跟onResume()又有何关系呢?让我们先来看看源码吧~

1. ActivityThread.java

我们知道app的进程其实是ActivityThread, 那么activity的生命周期自然是它来执行了,

final void handleResumeActivity(IBinder token,
boolean clearHide, boolean isForward, boolean reallyResume) {
//省略部分代码..

//call activity的onResume
ActivityClientRecord r = performResumeActivity(token, clearHide);

//省略部分代码..
View decor = r.window.getDecorView();
decor.setVisibility(View.INVISIBLE);
ViewManager wm = a.getWindowManager();
WindowManager.LayoutParams l = r.window.getAttributes();
a.mDecor = decor;
l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
l.softInputMode |= forwardBit;
if (a.mVisibleFromClient) {
a.mWindowAdded = true;
//这里就是关键代码了
wm.addView(decor, l);

performResumeActivity就是回调onResume了, 我们继续看wm.addView方法, 这个ViewManager是一个接口,其实现者是WindowManagerImpl

2.WindowManagerImpl.java

@Override
    public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
        applyDefaultToken(params);
        mGlobal.addView(view, params, mDisplay, mParentWindow);
    }

这个mGlobal是WindowManagerGlobal对象,我们继续

3.WindowManagerGlobal.java

public void addView(View view, android.view.ViewGroup.LayoutParams params, 
  Display display, Window parentWindow) {
 //我们跳过不相关代码.. 
  root = new ViewRootImpl(view.getContext(), display); 
  view.setLayoutParams(wparams); 
  this.mViews.add(view); 
  this.mRoots.add(root);
  this.mParams.add(wparams); 
try { 
  root.setView(view, wparams, panelParentView); 
}catch (RuntimeException var15) { //省略... } } }

这里我们new 出了ViewRootImpl对象, 我们知道这个对象就是android view的根对象了,负责view绘制的measure, layout, draw的巨长的方法 performTraversals就是这个类的,我们继续看setView方法

4.ViewRootImpl.java

public void setView(View view, LayoutParams attrs, View panelParentView) {
//省略部分...

                this.requestLayout();

//省略部分..
                    switch(res) {
                    case -9:
                        throw new InvalidDisplayException("Unable to add window " + this.mWindow + " -- the specified display can not be found");
                    case -8:
                        throw new BadTokenException("Unable to add window " + this.mWindow + " -- permission denied for this window type");
                    case -7:
                        throw new BadTokenException("Unable to add window " + this.mWindow + " -- another window of this type already exists");
                    case -6:
                        return;
                    case -5:
                        throw new BadTokenException("Unable to add window -- window " + this.mWindow + " has already been added");
                    case -4:
                        throw new BadTokenException("Unable to add window -- app for token " + attrs.token + " is exiting");
                    case -3:
                        throw new BadTokenException("Unable to add window -- token " + attrs.token + " is not for an application");
                    case -2:
                    case -1:
                        throw new BadTokenException("Unable to add window -- token " + attrs.token + " is not valid; is your activity running?");
                    default:
                        throw new RuntimeException("Unable to add window -- unknown error code " + res);
                    }

    }

这个函数调用了关键方法requestLayout(), 我们继续跟踪,顺便说下,后面一连串的BadTokenException就是我们常常遇到的dialog相关抛出的,也有些特殊场景也会出这个异常,可以到这里查看线索

public void requestLayout() {
        if(!this.mHandlingLayoutInLayoutRequest) {
            this.checkThread();
            this.mLayoutRequested = true;
            this.scheduleTraversals();
        }

    }

调用了scheduleTraversals, 从名字就能看出来了吧

void scheduleTraversals() {
        if(!this.mTraversalScheduled) {
            this.mTraversalScheduled = true;
            this.mTraversalBarrier = this.mHandler.getLooper().postSyncBarrier();
            this.mChoreographer.postCallback(2, this.mTraversalRunnable, (Object)null);
            this.scheduleConsumeBatchedInput();
        }

    }

它往Choreographer里面post了一个runnable, 这个Choreographer是android负责帧率刷新相关的东西,我们暂时可以不关注它,可以理解为往主线程post一个消息是一样的,顺便说下这个Choreographer可以做帧率检测相关的东西,,可以用于卡顿检测什么的。。。

final class TraversalRunnable implements Runnable {
        TraversalRunnable() {
        }

        public void run() {
            ViewRootImpl.this.doTraversal();
        }
    }
void doTraversal() {
        if(this.mTraversalScheduled) {
            this.mTraversalScheduled = false;
            this.mHandler.getLooper().removeSyncBarrier(this.mTraversalBarrier);
            if(this.mProfile) {
                Debug.startMethodTracing("ViewAncestor");
            }

            Trace.traceBegin(8L, "performTraversals");

            try {
                this.performTraversals();
            } finally {
                Trace.traceEnd(8L);
            }

            if(this.mProfile) {
                Debug.stopMethodTracing();
                this.mProfile = false;
            }
        }

    }

我们看这个runnable果然是去执行了那个巨长无比的函数performTraversals函数, 现在我们可以总结下流程了
这里写图片描述
结论:所以如果我们想在界面绘制出来后做点什么,那么在onResume里面显然是不合适的,它先于measure等流程了, 有人可能会说在onResume里面post一个runnable可以吗?还是不行,因为那样就会变成这个样子
这里写图片描述
所以你的行为一样会在绘制之前执行,这个时候我们的主角IdleHandler就发挥作用了,我们前面说了,它是在looper里面message暂时执行完毕了就会回调,顾名思义嘛,Idle就是队列为空的意思,那么我们的onResume和measure, layout, draw都是一个个message的话,这个IdleHandler就提供了一个它们都执行完毕的回调了,大概就是这样
这里写图片描述
说了这么多,那么现在获取到这个时机有什么用呢? look!!

这个是我们地图的公交详情页面, 进入之后产品要求左边的页卡需要展示,可以看到左边的页卡是一个非常复杂的布局,那么进入之后的效果可以明显看到头部的展示信息是先显示空白再100毫秒左右之后才展示出来的,原因就是这个页卡的内容比较复杂,用数据向它填充的时候花了较长时间,代码如下

long time = System.currentTimeMillis();
        detailView.populate(route);
        //省略部分不相关代码
        drawerLayout.openDrawer(GravityCompat.START);
        Log.i("yangu", "cost time " + (System.currentTimeMillis() - time));

这里写图片描述

可以看到这个detailView就是这个侧滑的页卡了,填充里面的数据花了90ms,如果这个时间是用在了界面view绘制之前的话,就会出现以上的效果了,view先是白的,再出现,这样就体验不好了,如果我们把它放到IdleHandler里面呢?代码如下

Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() {
            @Override
            public boolean queueIdle() {
                long time = System.currentTimeMillis();
                detailView.populate(route);
                //省略部分不相关代码
                drawerLayout.openDrawer(GravityCompat.START);
                Log.i("yangu", "cost time " + (System.currentTimeMillis() - time));
                return false;
            }
        });

效果是这样的
这里写图片描述
看出不同了吗?顶部的页卡先展示出来了,这样体验是不是会更好一些呢。虽然只有短短90ms,不过我们做app也应该关注这种细节优化的,是吧~ 这个做法也提供了一种思路,android本身提供的activity框架和fragment框架并没有提供绘制完成的回调,如果我们自己实现一个框架,就可以使用这个IdleHandler来实现一个onRenderFinished这种回调了。

二.可以结合HandlerThread, 用于单线程消息通知器

我们先思考一个问题,如果有一个model数据管理模块,怎么设计?比如地图的收藏模块的model部分。就是下面这个图的小星星

这里写图片描述

它原来的model设计大概是这个样子的

public class FavoriteModel {
    private static FavoriteModel ourInstance = new FavoriteModel();

    public static FavoriteModel getInstance() {
        return ourInstance;
    }

    private FavoriteModel() {
    }

    //收藏点数据
    private List<Favorite> mData = new ArrayList<>();

    //同步锁
    public Object lock;

    //添加收藏点
    public void add(Favorite f) {
        synchronized (lock){
            doSomeThing();
        }
    }

    //删除一个收藏点
    public void delete(Favorite f) {
        synchronized (lock){
            doSomeThing();
        }
    }

    //获取当前收藏列表
    public List<Favorite> get() {
        return mData;
    }

    //同步服务器收藏数据
    public void sync() {
        synchronized (lock){
            doSomeThing();
        }
    }
}

由于这个model是单例的,而且是多线程可以访问的,所以它的增删改查都加上了锁,而且由于外部访问需要遍历有哪些收藏点,所以外部遍历列表也需要加锁,大概是这样的

public void visit(List<Favorite> favorites){
        synchronized (FavoriteModel.getInstance().lock){
            for (Favorite favorite : favorites) {
                doSomeThing();
            }
        }
    }

因为是多线程可访问的,如果遍历不加锁的话,其他线程删除了一个收藏,就会crash的,原来的这样设计有几个不好的地方

1.外部使用者需要关系锁的使用,增加了负担,不用还不安全

2.如果在主线程加锁的话,可能另一个线程执行操作会阻塞主线程造成anr

总之,多线程代码就是容易出错,而且真的出错的时候查起来太费劲了,目前收藏夹模块就有N多bug,所以我想用单线程来解决这个问题,由于model层的访问需要数据库和网络等,所以需要异步线程,那么单线程队列+异步线程,首先想到的就是HandlerThread, 大概架构如下
这里写图片描述
现在,我们把原来多线程的逻辑改到了单线程里面,各种收藏的model共用一个HandlerThread,这样我们增删改查都不用加锁了,出错几率大大减小,而且这种model的设计有点类似插件的意思,可以很方便的增加其他收藏

ok, 那么跟我们的主题IdleHandler有什么关系呢?思考这样一个问题,地图上的小星星需要实时更新,也就是model的任何变化都需要显示到地图上,那么收藏的小星星就应该作为model的观察者,以前的做法是向收藏model注册监听,在每一个增删改查操作后都对观察者回调,大概是这样

//添加收藏点
    public void add(Favorite f) {
        synchronized (lock){
            doSomeThing();
            notifyObserver();
        }
    }

    //删除一个收藏点
    public void delete(Favorite f) {
        synchronized (lock){
            doSomeThing();
            notifyObserver();
        }
    }

这样有一个小小的问题,就是如果有一个操作生成10个快速连续的增删改查操作,那么我们的UI就会收到10次回调,而这种场景下我们其实只需要最后一次回调就够了,中间操作其实不用刷新UI的

那么现在改成单线程模型,我们又该如何处理这个问题呢?当然我们也能在每个post到异步线程的runnable里面去回调观察者,但这样未免不够优雅,所以这个时候IdleHandler不就又可以发挥作用了吗?它是在消息暂时处理完的时候回调的呀,不是很符合我们的时机么,对吧

public AbsFavoriteModel() {
        if (sThread == null) {
            sThread = new HandlerThread("favorite-model");
            sThread.start();
        }
        mHandler = new Handler(sThread.getLooper());

        try {
            Field field =                            Looper.class.getDeclaredField("mQueue");
            field.setAccessible(true);
            MessageQueue queue = (MessageQueue) field.get(sThread.getLooper());
            queue.addIdleHandler(new MessageQueue.IdleHandler() {
                @Override
                public boolean queueIdle() {
                    if (mListeners != null){
                        for (DataChangeListener<T> mListener : mListeners) {
                            mListener.onDataChange(new ArrayList<>(mData));
                        }
                    }
                    return true;
                }
            });
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

就是这个样子了,这里为什么不用第一个场景下的Looper.myQueue().addIdleHandler()呢?注意这个地方Looper.myQueue()如果在主线程调用就会使用主线程looper了,所以我选择反射这个HandlerThread的looper来设置它,这个IdleHandler我们返回了true, 表示我们要长期监听消息队列,因为返回false,下次就没有回调了哦。

好了,结论是这个地方IdleHandler用作了一个消息的触发器,是不是挺有意思的呢?

结语

如果你没有用过它,从今天开始试试吧,这篇文章只是我个人的一点小思路,说不定这个IdleHandler有很多其他的用法呢~~如果喜欢的话请点个赞哟,有任何不正确的地方也请随时指出


更多精彩内容欢迎关注腾讯 Bugly的微信公众账号:

腾讯 Bugly是一款专为移动开发者打造的质量监控工具,帮助开发者快速,便捷的定位线上应用崩溃的情况以及解决方案。智能合并功能帮助开发同学把每天上报的数千条 Crash 根据根因合并分类,每日日报会列出影响用户数最多的崩溃,精准定位功能帮助开发同学定位到出问题的代码行,实时上报可以在发布后快速的了解应用的质量情况,适配最新的 iOS, Android 官方操作系统,鹅厂的工程师都在使用,快来加入我们吧!

Android开发系列:IdleHandler闲时加载 在之前的文章里,我们讲过关于handler的一些使用和原理。今天讲一个系统预留的一个handler,IdleHandler,有了它,可以让我们在系统闲时进行一些预加载或者事务处理。 阅读详情

相关推荐

深入源码分析Handler 消息机制 、Looper、MessageQueue 消息同步屏障、IdleHandler、Message 复用

Handler 线程通信 基本使用 在Android 中Handler来实现,大多数都是用来实现,子线程中发送消息,到主线程中更新UI,下面是基本使用 // 步骤1:在主线程中 通过匿名内部类 创建Handler类对象 mHandler = new Handler(){ // 通过复写handlerMessage(),处理其他线程发来的消息 ...

薛瑄的博客 2065

androidMessageQueue.IdleHandler

2019独角兽企业重金招聘Python工程师标准>>> ...

weixin_34111819的博客 208

详细分析androidMessageQueue.IdleHandler

主要介绍了androidMessageQueue.IdleHandler用法,很有参考价值,欢迎大家在下方留言区讨论。

Android学习总结之扩展基础篇(一)

idleHandle和intentService的知识点

2301_80329517的博客 766

Android 学习使用空闲线程addIdleHandler

Android 中的 Handler 用于处理 UI 更新和一些耗时操作。 偶然看到了 addIdleHandler ,记录下探索过程。 1.使用 使用也很简单,在 Activity 的 onCreate 中初始化即可。 拿到当前的 MessageQueue 后调用即可,两种写法都能拿到。 推荐 写法 2 (为什么 ? 因为别人是这样写的 =.=l)。 return false 表示仅回调执行一次; return true 表示一直回调执行; 后面说明。 // 写法 1 getMainLooper(

weixin_44021334的博客 4220

AndroidIdleHandler的简单理解和使用

IdleHandler 说白了,就是 Handler 机制提供的一种,可以在 Looper 事件循环的过程中,当出现空闲的时候,允许我们执行任务的一种机制。,当然要注意这里执行的代码同样不能太耗时,因为它是同步执行的,如果太耗时肯定会影响后面的 message 执行。MessageQueue 是一个基于消息触发时间的优先级队列,所以队列出现空闲存在两种场景。,也就是当下次队列空闲的时候,不会继续回调它的 queueIdle 方法了。这个获取消息队列下一个待执行消息的方法中,我们跟一下具体的逻辑。

JMW1407的博客 5547

知道androidMessageQueue.IdleHandler吗?

https://mp.weixin.qq.com/s/KpeBqIEYeOzt_frANoGuSg? 我们知道android是基于Looper消息循环的系统,我们通过Handler向Looper包含的MessageQueue投递Message,不过我们常见的用法是这样吧? 一般我们比较少接触MessageQueue, 其实它内部的IdleHandler接口有很多有趣的用法,首先看看...

leochen‘s Rock 375

知道 AndroidMessageQueue.IdleHandler 吗?

知道 AndroidMessageQueue.IdleHandler 吗?

历史上的今天 6870

从电竞战队依赖明星选手看技术团队如何避免单点故障与构建系统韧性

在软件工程与系统架构中,单点故障(SPOF)是一个核心风险概念,指系统中某个关键组件的失效会导致整个服务中断。其原理在于系统设计未能实现充分的冗余与解耦,将可用性过度绑定于特定的人、服务或技术栈。管理单点故障的技术价值在于提升系统的整体可用性、可维护性与团队抗风险能力,是构建高韧性技术架构的基石。典型的应用场景包括核心服务依赖特定维护人员、关键业务逻辑绑定独有技术栈,以及重要流程严重耦合某个外部API。本文借由近期电竞领域关于战队依赖核心选手的讨论,深入剖析技术团队中类似的“人员单点故障”与“知识单点故障”

weixin_34377919的博客 391

MessageQueueIdleHandler

IdleHandler是什么? IdleHandler是包含在MessageQueue类中的一个接口,内部只包含一个方法,在消息队列空闲(没有消息或者第一个需要处理的消息在将来执行)时被回调 publicstaticinterfaceIdleHandler{//当消息队列空闲时会被回调,返回值表示是否会被重复执行//返回true,会在mIdleHandlers...

从技术到艺术 688

Android IdleHandler 原理解析与应用场景

IdleHandlerAndroid MessageQueue 机制中的一个接口,允许在主线程空闲时执行任务。本文详细解析 IdleHandler 的工作原理,包括 MessageQueue 结构、触发时机及其使用方法。同时,我们探讨了 IdleHandler 的应用场景,如延迟初始化、资源回收和数据预加载等,并分析了其优缺点。合理使用 IdleHandler 可以优化应用性能,提高用户体验。本文将帮助开发者深入理解 IdleHandler 并在实际开发中灵活运用。

帅次的博客 3437

MessageQueue.IdleHandler接口使用方法以及原理分析

转载自https://bbs.51cto.com/thread-1094228-1.html <h2 style="float: left;font-size:16px;vertical-align:middle">&nbsp;&nbsp;<strong><a title="MessageQueue.IdleH...

changqijihua的专栏 1450

Android | IdleHandler的使用分析

Android 提供的一种机制,用于在MessageQueue消息队列没有消息或下一次消息执行的时间还未到(系统空闲)时执行一些任务。是:当消息队列空闲时调用。如果返回true,该保持活动状态,等待下一个空闲周期。如果返回false,它将被移除,不会在下一次空闲周期调用。//是否是空闲状态//添加IdleHandler//移除IdleHandlerfor (;;) {for (;;) {if (msg!

小马快跑的博客 986

Android 异步操作】Handler 机制 ( MessageQueue 空闲任务 IdleHandler 机制 )

一、MessageQueue 空闲任务 IdleHandler 机制、 二、MessageQueue 中空闲任务 IdleHandler 相关源码、

让 学习 成为一种 习惯 ( 韩曙亮 の 技术博客 ) 1276

深入了解IdleHandler,用来做优化或者轻量级任务都是极好的

IdleHandler 是 Handler 提供的一种充分利用CPU的机制, 主要是在 MessageQueue 出现空闲的时候被执行,

fumeidonga的博客 1378

利用空闲时间提升性能:深入解析 Android IdleHandler 机制

IdleHandlerAndroid消息机制中用于在主线程空闲时执行任务的接口。其核心原理是通过MessageQueue在消息队列空闲时调用queueIdle()方法执行轻量级任务。适用于启动优化、预加载等场景,但需注意执行时机不可控、避免耗时操作和内存泄漏等问题。使用时需权衡任务必要性与性能影响,确保只处理非紧急任务。

csdn_silent的博客 2199
上一篇: Android 7.0中ContentProvider实现原理
下一篇: 《Android 创建线程源码与OOM分析》
腾讯Bugly
博客等级 码龄12年 1875粉丝 201原创
评论 2
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值