消息机制全解析(下)

3、一问一答:常见问题汇总

经过上面的学习,到了最激动人心的时刻了,揭秘下Handler常见的问题都如何回答。一开始觉得不知道如何回答的问题,你现在一定能轻松理解

3.1 Handler 引起的内存泄露原因以及最佳解决方案

因为Handler一般是作为Activity的内部类,可以发送延迟执行的消息,如果在延迟阶段,我们把Activity关掉,此时因为该Activity还被Handler这个内部类所持有,导致Activity无法被回收,没有真正退出并释放相关资源,因此就造成内存泄漏。

工程上常用的方法是将 Handler 定义成静态的内部类,在内部持有 Activity 的弱引用,并在Acitivity的onDestroy()中调用handler.removeCallbacksAndMessages(null)及时移除所有消息。如果和面试官说了这两个方法,那你就100分过关了,但更进一步是建议将Handler抽离出来作为BaseHandler,然后每个Activity需要用到Handler的时候,就去继承BaseHandler。最佳解决方案具体代码:

// 这个是BaseHandler
public abstract class BaseHandler<T> extends Handler {
    private final WeakReference<T> mWeakReference; //弱引用

    protected BaseHandler(T t) {
        mWeakReference = new WeakReference<T>(t);
    }

    protected abstract void handleMessage(T t, Message msg);

    @Override
    public void handleMessage(Message msg) {
        super.handleMessage(msg);
        if (mWeakReference == null) {
            return;
        }

        T t = mWeakReference.get();
        if (t != null) {
            handleMessage(t, msg);
        }
    }
}

//然后在某个Activity中使用
 private static class H extends BaseHandler<XuruiActivity> { //静态的内部类哦
   
        public H(XuruiActivity activity) {
            super(activity);
        }

        @Override
        protected void handleMessage(XuruiActivity activity, Message msg) {
            //do something
        }
    }
//同时Activity的onDestroy函数取消掉所有消息
@Override
protected void onDestroy() {
    mMyHandler.removeCallbacksAndMessages(null);
    super.onDestroy();
}

3.2 为什么我们能在主线程直接使用 Handler,而不需要创建 Looper ?

详情对应2.1小节,ActivityThread是主线程操作的管理者,在 ActivityThread.main() 方法中调用了 Looper.prepareMainLooper() ,该方法调用prepare()创建Looper。因此主线程不是不需要创建Looper,而是系统帮我们做了。

3.3 Handler、Thread和HandlerThread的差别

又是这种考区别的题目,不过还算是比较常见的三个知识点:

  1. Handler:本文所学的知识,是Android的一种异步消息机制,负责发送和处理消息,可实现子线程和主线程的消息通讯;
  2. Thread:Java的一个多线程类,是Java进程中最小执行运算单位,用于给子类继承,创建线程/
  3. HandlerThread:从名字看就知道是由前面两者结合起来的。可以理解为“一个继承自Thread的Handler类”,因此本质上和父类一样是Thread,但其内部直接实现了Looper,我们可以直接在HandlerThread里面直接使用Handler消息机制。减少了手动调用Looper.prepare()和Looper.loop()这些方法。

3.4 子线程中怎么使用 Handler?

这个题目就可以结合上面两个题目来拓展理解了。子线程中使用 Handler 需要先执行两个操作:Looper.prepare() 和 Looper.loop(),看到这里你应该要记得这两个函数执行顺序是不能变的哦。同时可以直接使用HandlerThread类即可。

3.5 为什么在子线程中创建 Handler 会抛异常?

不能在还没有调用 Looper.prepare() 方法的线程中创建 Handler。 因为抛出异常的地方,在Handler的构建函数,判断 mLooper 对象为null的时候, 会抛出异常

3.6 Handler 里藏着的 Callback 能干什么?

详情对应2.4小节,当从消息队列获取到信息后,需要分配给对应的Handler去处理,总共有3种优先级。

  1. handleCallback(msg):Message里自带的callback优先级最高;对应Handler的post方法;
  2. mCallback.handleMessage(msg):也就是Handler.Callback 写法;
  3. handleMessage(msg):重写handlerMessage()方法,优先级最低;

而Handler.Callback处于第二优先级,当一条消息被 Callback 处理并返回true,那么 Handler 的 handleMessage(msg) 方法就不会被调用了;但如果 Callback 处理后返回false,那么这个消息就先后被Handler.Callback和handleMessage(msg)都处理过。

3.7 Handler 的 send 和 post 的区别?

基于上道题继续展开,post方法,它会把传入的 Runnable 参数赋值给 Message 的 callback 成员变量。当 Handler 进行分发消息时,msg.callback 会最优先执行。

  • post是属于sendMessage的一种赋值callback的特例
  • post和sendMessage本质上没有区别,两种都会涉及到内存泄露的问题
  • post方式配合lambda表达式写法更精简

3.8 创建 Message 实例的最佳方式

详情对应2.3小节,为了节省开销,Android 给 Message 设计了回收机制,所以我们在使用的时候尽量复用 Message ,减少内存消耗:

通过 Message 的静态方法 Message.obtain(); 通过 Handler 的公有方法 handler.obtainMessage()。

3.9 Message 的插入以及回收是如何进行的,如何实例化一个 Message 呢?

插入对应2.5.1小节注释2,Message 往 MessageQueue 插入消息时,会根据 when 字段(相对时间)来判断插入的顺序.

消息回收对应2.4小节loop()函数注释5,在消息执行完成之后,会进行回收消息,回收消息可见2.3小节recycleUnchecked()函数,只是 Message 的成员变量设置为0或者null;

实例化 Message 的时候,也是件2.3小节,本文建议多次了,尽量使用 Message.obtain 方法,这是从缓存消息池链表里直接获取的实例,可以避免 Message 的重复创建。

3.10 妙用Looper机制,或者你知道Handler机制的其他用途吗?

  • 将 Runnable post 到主线程执行;
  • 利用 Looper 判断当前线程是否是主线程;
public boolean isMainThread() {
    return Looper.getMainLooper() == Looper.myLooper();
}

3.11 Looper.loop()死循环一直运行是不是特别消耗CPU资源呢?不会造成应用卡死吗?

详情对应2.4和2.5小节。这还涉及linux多进程通讯方式:Pipe管道通讯。Android应用程序的主线程在进入消息循环过程前,会在内部创建一个Linux管道。首先在loop()方法中,调用queue的next()方法获取下一个消息。具体看2.5.2小节,next()源码分析说过,MessageQueue没有消息时,便阻塞在nativePollOnce()方法里,此时主线程会释放CPU资源进入休眠状态,因此并不特别消耗CPU资源。

直到等待时长到了或者有新的消息时,通过往pipe管道写端写入数据来唤醒主线程工作。这里采用的epoll机制是一种IO多路复用机制,可以同时监视多个描述符。当一个描述符号准备好(读或写)时,立即通知相应的程序进行读或写操作,其实质是同步 I/O,即读写是阻塞的。其实主线程大多数时候都是处于这种休眠状态,并不会消耗大量CPU资源,更不会造成应用卡死。

3.12 MessageQueue 中如何等待消息?为何不使用 Java 中的 wait/notify 来实现阻塞等待呢?

直接回答在 MessageQueue 的 nativePollOnce 函数阻塞,直到等待时长到了或者有新的消息时才重新唤醒MessageQueue。其实在 Android 2.2 及其以前,确实是使用wait/notify来实现阻塞和唤醒,但是现在MessageQueue源码涉及很多native的方法,因此Java层的wait/notify自然不过用了,而Pipe管道通讯是很底层的linux跨进程通讯机制,满足native层开发需求。

3.13 你知道延时消息的原理吗?

首先是信息插入:会根据when属性(需要处理消息的相对时间)进行排序,越早的时间的Message插在链表的越前面;

在取消息处理时,如果时间还没到,就休眠到指定时间;如果当前时间已经到了,就返回这个消息交给 Handler 去分发,这样就实现处理延时消息了。

3.14 handler postDelay这个延迟是怎么实现的?

3.15 如何保证在msg.postDelay情况下保证消息次序?

详情对应2.5.1小节,和上一题有所联系。handler.postDelay不是延迟一段时间再把Message放到MessageQueue中,而是直接进入MessageQueue,根据when变量(相对时间)的大小排序在消息池的链表里找到合适的插入位置,如此也保证了消息的次序的准确性。也就是本质上以MessageQueue的时间顺序排列和唤醒的方式结合实现的。

3.16 更新UI的方式有哪些

这个题目放到这一节确实比较靠前,但因为本节介绍了其中的两个。所以也提一下。

  • Activity.runOnUiThread(Runnable)
  • View.post(Runnable),View.postDelay(Runnable, long)
  • Handler
  • AsyncTask
  • Rxjava
  • LiveData

3.17 线程、Handler、Looper、MessageQueue 的关系?

这里还是有必要说明一下,一个线程对应一个 Looper (可见2.1小节prepare()函数注释1的判断),同时对应一个 MessageQueue,对应多个 Handler。

3.18 多个线程给 MessageQueue 发消息,如何保证线程安全?

见2.5.1 enqueueMessage()在插入Message的时候使用synchronized机制加锁。

3.19 View.post 和 Handler.post 的区别?

//TODO:

3.20 你知道IdleHandler吗?

看看next()源码:

Message next() {
    ···
    for (;;) {
        //1:阻塞操作,当等待nextPollTimeoutMillis时长,或者消息队列被唤醒,都会返回 
        nativePollOnce(ptr, nextPollTimeoutMillis);
        ···
        synchronized (this) {
           //获取消息
            ···
        }

        // 此时没有信息需要处理就跑到这里
        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(); //1
            } catch (Throwable t) {
                Log.wtf(TAG, "IdleHandler threw exception", t);
            }

            if (!keep) {
                synchronized (this) {
                    mIdleHandlers.remove(idler);
                }
            }
        }
        ...
    }
}

IdleHandler 是通过 MessageQueue.addIdleHandler 来添加到 MessageQueue 的,前面提到当 MessageQueue.next 当前没有需要处理的消息时就会进入休眠,而在进入休眠之前呢,会执行注释1,此时如果返回true,则调用该方法后继续保留,下次队列又空闲的时候继续调用。如果返回false,就会在注释2将当前的idler删除。

3.21 子线程中能不能直接new一个Handler,为什么主线程可以?

子线程不可以直接new一个Handler,因为Handler的构造方法中,会通过Looper.myLooper()获取looper对象,如果为空,就会抛出异常

主线程在入口处ActivityThread的main方法中通过Looper.prepareMainLooper()获取到对象,并通过looper.loop()开启循环,在子线程中若要使用handler,可以先通过Looper.prepare获取到looper对象,并使用looper.loop()开启循环

3.22 .Handler怎么做到的一个线程对应一个Looper,如何保证只有一个 MessageQueue ThreadLocal在Handler机制中的作用

synchronized采取的是“以时间换空间”的策略,本质上是对关键资源上锁,让大家排队操作。 而ThreadLocal采取的是“以空间换时间”的思路, 它一个线程内部的数据存储类,通过它可以在制定的线程中存储数 据,数据存储以后,只有在指定线程中可以获取到存储的数据, 对于其他线程就获取不到数据,可以保证本线程任 何时间操纵的都是同一个对象。比如对于Handler,它要获取当前线程的Looper,很显然Looper的作用域就是线程, 并且不同线程具有不同的Looper。 ThreadLocal本质是操作线程中ThreadLocalMap来实现本地线程变量的存储的 ThreadLocalMap是采用数组的方式来存储数据,其中key(弱引用)指向当前ThreadLocal对象,value为设的值 通过 ThreadLocal计算出Hash key,通过这个哈 ThreadLocal对象,value为设的值

3.23 Handler消息机制中,一个looper是如何区分多个Handler的当 Activity有多个Handler的时候,怎么样区分当前消息由哪个Handler处理处 理message的时候怎么知道是去哪个callback处理的

每个Handler会被添加到 Message 的target字段上面,Looper 通过调用 Message.target.handleMessage() 来让 Handler 处理消息