搞懂Handler去面试!

本文深入解析Android中Handler机制的工作原理,包括与线程的关联、消息的存储与管理、分发与处理流程,以及常见使用误区和内存泄漏问题。

 

如果你没有听说过或者使用过Handler,那么你一定不是一个合格的Android开发。Handler作为我们最常使用的跨线程UI工具,你可能只知道它的使用,却没有了解过它的实现原理。可能有人会想,原理这种东西理解与不理解它,并不影响我们对它的使用。话是没错,可能原理这种东西对于大部分人最直接的作用就是面试了,对于Handler,它可是面试中的常客,与之相关提问最多的就是Handler的原理了。有人会说,这我熟悉啊,然后张口就来:

“Handler机制由四部分组成,分别是Handler、Looper、MessageQueue、Message。Handler负责消息的发送和处理,Looper负责不断从MessageQueue中取出消息,MessageQueue负责Message的存储与管理,Message是消息的载体,可以承载需要的数据”

这要是搁几年前面试的时候,你这么说没什么问题,毕竟那时移动端很火,各行各业急需懂得开发App的人才,标准和要求没那么高,可能面试你的人都不懂它的原理甚至都不懂技术。但是,随着市场对移动端人才需求的不断饱和与移动开发市场的降温,这样的回答在今天可就不再那么管用了,一是对人才标准和要求方面的提高,二是如今的面试官不可能再出现像几年前那样的情况。当再往下问深点时,可能就触到你的知识盲区,GG了。

所以,就算是为了面试需要,请你务必搞懂这些原理。对于想在技术上有所进阶的人,就更该了。

那么本篇博文就带着以下对于Handler的疑问进行展开(下面都是针对Handler原理问题的变种,本质上还是离不开原理)

1、Handler如何与线程相关联?

2、消息是如何存储和管理的?

3、消息又是如何分发与处理的?

4、如果一个线程存在多个Handler,那么消息是怎样分发到对应的Handler而不出错的?

5、Handler处理消息的方式都有哪些?

6、线程的切换是怎么回事?

上面这些问题可能是你在使用Handler的过程中会产生的疑问,又或是在面试的时候被提问过,今天我们就Handler的使用过程搞懂这些问题背后的原理。

一个Handler是如何使用的呢?

 class MyHandler:Handler(){
        override fun handleMessage(msg: Message?) {
            super.handleMessage(msg)
            /.../
        }
    }
myHandler.sendEmptyMessage()
myHandler.post()

继承Handler,复写handleMessage方法处理消息,再调用send系列或post系列的方法发送一个消息,这应该是Handler最常见的使用方式了,然而你会发现看不到Looper、MessageQueue的身影,因为这些细节都被Handler封装了起来,让我们只需关注它如何使用而不需要知道背后的细节,今天我们就反其道而行之,从它的创建入手,看看Handler机制是怎么实现的。

一、Handler的创建

Handler一共有七个构造方法,但是最后都会指向其中两个,一个带Looper参数的,一个不带的。

那么我们先看看不带Looper参数的那个

public Handler(Callback callback, boolean async) {
        
        /.../

        mLooper = Looper.myLooper();
        if (mLooper == null) {
            throw new RuntimeException(
                "Can't create handler inside thread " + Thread.currentThread()
                        + " that has not called Looper.prepare()");
        }
        mQueue = mLooper.mQueue;
        mCallback = callback;
        mAsynchronous = async;
    }

可以看到,在这个构造方法内通过Looper.myLooper()方法获得一个Looper对象,对其进行为空判断,并抛出一个异常提示当前线程不能创建Handler,因为在此之前没有调用Looper.prepare()方法。

那prepare()这个方法是干什么的呢

public static void prepare() {
        prepare(true);
    }

    private static void prepare(boolean quitAllowed) {
        if (sThreadLocal.get() != null) {
            throw new RuntimeException("Only one Looper may be created per thread");
        }
        sThreadLocal.set(new Looper(quitAllowed));
    }

prepare()方法调用了私有的prepare方法并转递了一个true参数,这个参数表示当前Looper是否可退出,在这个私有的prepare()方法内它首先去当前线程的本地存储区去获得一个Looper对象,如果对象不为空,则提示异常并告知一个线程只能存在一个Looper对象,如果为空则创建一个Looper对象并设置到当前线程的本地存储区。

private Looper(boolean quitAllowed) {
        mQueue = new MessageQueue(quitAllowed);
        mThread = Thread.currentThread();
    }

Looper的构造方法里创建了一个MessaQueue并标记了当前线程。

那么再来看看Handler带Looper参数的构造函数

public Handler(Looper looper, Callback callback, boolean async) {
        mLooper = looper;
        mQueue = looper.mQueue;
        mCallback = callback;
        mAsynchronous = async;
    }

这个构造方法只是把传进来的参数赋值给本地,并没有什么特别的。

ok,通过Handler的构造函数可以得知,Handler的构建离不开Looper,而Looper是与线程绑定且一个线程只能拥用有一个Looper,所以Handler与线程是通过Looper来关联的,Looper在那个线程创建,Handler就在哪个线程执行。

二、消息的存储与管理

通常我们使用Handler发送一个消息,会调用send系列或post系列的方法,但其实它们最后调用的都是同一个方法。

而这个enqueueMessage()方法又会调用MessageQueue的enqueueMessage()方法将消息入队。

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

到这里,显而易见消息存储与管理是通过MessageQueue来完成。

三、消息的分发与处理

在子线程中使用Handler不仅需要Looper.prepa(),还需调用Looper.loop()方法才能正常使用。

public static void loop() {
        final Looper me = myLooper();
        if (me == null) {
            throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
        }
        final MessageQueue queue = me.mQueue;

        /.../

        for (;;) {
            Message msg = queue.next(); // might block
            if (msg == null) {
                // No message indicates that the message queue is quitting.
                return;
            }

            /.../
            
            try {
                msg.target.dispatchMessage(msg);
                dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;
            } finally {
                if (traceTag != 0) {
                    Trace.traceEnd(traceTag);
                }
            }
            /.../

            msg.recycleUnchecked();
        }
    }

loop()方法内部开始了一个死循环,并不断从MessageQueue中取出消息,没有时则阻塞,当Looper调用quit()方法时,MessageQueue的queue.next()返回为null,loop()方法结束死循环,Looper退出。

当有Message时,尝试执行msg.target.dispatchMessage(msg)方法,msg.target是什么?记不记得前面Handler的enqueueMessage方法,其实msg.target就是Handler对象本身

private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
        //将Handler引用指向msg.target
        msg.target = this;
        if (mAsynchronous) {
            msg.setAsynchronous(true);
        }
        return queue.enqueueMessage(msg, uptimeMillis);
    }

这样Message就被分发到对应的Handler的dispatchMessage进行处理

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

咦?这msg.callback和mCallBack又是什么,消息不是被handleMessage()处理就完了么,怎么还有其他方法会处理?别慌,其实这些东西你也见过,而且还用过。

msg.callback其实就是handler.post()方法里的Runnable,在使用post系列方法的时候,Runnable会经getPostMessage(Runnable r)

封装成一个Message对象

private static Message getPostMessage(Runnable r) {
        Message m = Message.obtain();
        m.callback = r;
        return m;
    }

handleCallback()方法则是在Handler绑定的线程执行Runnable的run方法

 private static void handleCallback(Message message) {
        message.callback.run();
    }

那mCallback呢?mCallback是一个接口,内有一个handleMessage方法,你会注意到,这个方法有一个布尔返回值,如果不需dispatchMessage方法再往下执行时返回ture。这个接口通过Handler的构造方法传入

public interface Callback {
        
        public boolean handleMessage(Message msg);
    }

最后就是Handler的handleMessage()方法了,通常继承Handler时需要复写这个方法。

上面就是Handler处理Message的三种实现了,你会发现,这相当于三个方法拥有不同的优先执行权,Runnable最高,CallBack接口次之,最后是handleMessage()。对于这个优先级可以干什么,有兴趣的可以自行谷哥度娘。

 

扩展

1、Handler的内存泄漏

其实在Handler的构造方法里,就明确表示过Handler应该是静态的,否则可能会发生内存泄漏

 Log.w(TAG, "The following Handler class should be static or leaks might occur: " +
                    klass.getCanonicalName());

比如我要通过Handler的消息设置进度条

companion object{
         class MyHandler(activity: MainActivity) : Handler() {

             private var mWeakReference =WeakReference(activity)

             override fun handleMessage(msg: Message) {
                super.handleMessage(msg)
                 val activity = mWeakReference.get()
                 activity?.let { it.progress_bar.progress = msg.what }
            }
        }
    }

2、Message的最佳获取方式?

因为我们需要频繁的使用Message,所以频繁的创建会过多消耗资源,所以Message内部维护着一个消息池供我们使用,它会循环利用Message,这个消息池的大小为50。

private static final int MAX_POOL_SIZE = 50;

我们可以通过Handler.obtainMessage()或Message.obtain()方法来获取一个Message。

3、子线程吐司的正确方式

正如我们在子线程使用Handler一样,在吐司前后调用Looper.preare()、Looper.loop()方法即可(这个我没研究过,貌似是Toast的底层也是通过Handler实现的,如果有知道的可以评论告诉我)

最后

相信看完这篇文章,以后再有人问起你Handler的问题,基本不会再出现触及知识盲区的情况而卡壳了(除非真的很底层的东西),如果有人在面试遇到过关于Handler的其他问题,希望可以留言评论,和大家分享一下。

下载方式:https://pan.quark.cn/s/a4b39357ea24 布线问题(分支限界算法)是计算机科学和电子工程领域中一个广为人知的议题,它主要探讨如何在印刷电路板上定位两个节点间最短的连接路径。 在这一议题中,电路板被构建为一个包含 n×m 个方格的矩阵,每个方格能够被界定为可通行或不可通行,其核心任务是定位从初始点到最终点的最短路径。 分支限界算法是处理布线问题的一种常用策略。 该算法与回溯法有相似之处,但存在差异,分支限界法仅需获取满足约束条件的一个最优路径,并按照广度优先或最小成本优先的原则来探索解空间树。 树 T 被构建为子集树或排列树,在探索过程中,每个节点仅被赋予一次成为扩展节点的机会,且会一次性生成其全部子节点。 针对布线问题的解决,队列式分支限界法可以被采用。 从起始位置 a 出发,将其设定为首个扩展节点,并将与该扩展节点相邻且可通行的方格加入至活跃节点队列中,将这些方格标记为 1,即从起始方格 a 到这些方格的距离为 1。 随后,从活跃节点队列中提取队首节点作为下一个扩展节点,并将与当前扩展节点相邻且未标记的方格标记为 2,随后将这些方格存入活跃节点队列。 这一过程将持续进行,直至算法探测到目标方格 b 或活跃节点队列为空。 在实现上述算法时,必须定义一个类 Position 来表征电路板上方格的位置,其成员 row 和 col 分别指示方格所在的行和列。 在方格位置上,布线能够沿右、下、左、上四个方向展开。 这四个方向的移动分别被记为 0、1、2、3。 下述表格中,offset[i].row 和 offset[i].col(i=0,1,2,3)分别提供了沿这四个方向前进 1 步相对于当前方格的相对位移。 在 Java 编程语言中,可以使用二维数组...
源码来自:https://pan.quark.cn/s/a4b39357ea24 在VC++开发过程中,对话框(CDialog)作为典型的用户界面组件,承担着与用户进行信息交互的重要角色。 在VS2008SP1的开发环境中,常常需要满足为对话框配置个性化背景图片的需求,以此来优化用户的操作体验。 本案例将系统性地阐述在CDialog框架下如何达成这一功能。 首先,需要在资源设计工具中构建一个新的对话框资源。 具体操作是在Visual Studio平台中,进入资源视图(Resource View)界面,定位到对话框(Dialog)分支,通过右键选择“插入对话框”(Insert Dialog)选项。 完成对话框内控件的布局设计后,对对话框资源进行保存。 随后,将着手进行背景图片的载入工作。 通常有两种主要的技术路径:1. **运用位图控件(CStatic)**:在对话框界面中嵌入一个CStatic控件,并将其属性设置为BST_OWNERDRAW,从而具备自主控制绘制过程的权限。 在对话框的类定义中,需要重写OnPaint()函数,负责调用图片资源并借助CDC对象将其渲染到对话框表面。 此外,必须合理处理WM_CTLCOLORSTATIC消息,确保背景图片的展示不会受到其他界面元素的干扰。 ```cppvoid CMyDialog::OnPaint(){ CPaintDC dc(this); // 生成设备上下文对象 CBitmap bitmap; bitmap.LoadBitmap(IDC_BITMAP_BACKGROUND); // 获取背景图片资源 CDC memDC; memDC.CreateCompatibleDC(&dc); CBitmap* pOldBitmap = m...
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值