Handler的一问一答

作者: 葛糖糖 | 来源:发表于2019-01-10 21:01 被阅读0次

    对于handler,我想我就不用废话了,开发必不可少,面试必定会问,可真的能够对handler深入掌握么?又能在面试中回答的面面俱到么?(大佬请放下你手中的杠略过吧),而我肯定不行的,所以就实际一点,以一问一答的形势,慢慢地解开一点Handler的不再神秘的面纱.那么开始吧!!!

    第一问,请简单的描述一下Handler机制或者Android消息机制.

    • Android 的消息机制也就是 handler 机制,创建 handler 的时候会创建一个 looper ( 通过 looper.prepare() 来创建 ),looper 一般为主线程 looper.

    • handler 通过 send 发送消息 (sendMessage) ,当然 post 一系列方法最终也是通过 send 来实现的,在 send 方法中handler 会通过 enqueueMessage() 方法中的 enqueueMessage(msg,millis )向消息队列 MessageQueue 插入一条消息,同时会把本身的 handler 通过 msg.target = this 传入.

    • Looper 是一个死循环,不断的读取MessageQueue中的消息,loop 方法会调用 MessageQueue 的 next 方法来获取新的消息,next 操作是一个阻塞操作,当没有消息的时候 next 方法会一直阻塞,进而导致 loop 一直阻塞,当有消息的时候,Looper 就会处理消息 Looper 收到消息之后就开始处理消息: msg.target.dispatchMessage(msg),当然这里的 msg.target 就是上面传过来的发送这条消息的 handler 对象,这样 handler 发送的消息最终又交给他的dispatchMessage方法来处理了,这里不同的是,handler 的 dispatchMessage 方法是在创建 Handler时所使用的 Looper 中执行的,这样就成功的将代码逻辑切换到了主线程了.

    • Handler 处理消息的过程是:首先,检查Message 的 callback 是否为 null,不为 null 就通过 handleCallBack 来处理消息,Message 的 callback 是一个 Runnable 对象,实际上是 handler 的 post 方法所传递的 Runnable 参数.其次是检查 mCallback 是 否为 null,不为 null 就调用 mCallback 的handleMessage 方法来处理消息.


    第二问,请画出Handler机制或者Android消息机制图解.


    第三问,关于UI线程中Looper对象,MessageQueue对象,Handler对象,Message对象的个数问题.

    在UI线程中只有一个Looper对象,只有一个MessageQueue对象。但可以有很多个handler对象。可以有很多个Message对象。


    第四问,怎么保证只有一个Looper对象的?怎么保证只有一个MessageQueue对象的?

    在Handler机制中使用Looper对象的地方有三个,若这三个Looper对象是同一个就证明了在UI线程只有一个Looper对象。

    1. 在ActivityThread类中使用Looper.prepareMainLooper()创建Looper对象

    prepareMainLooper方法的原码:

    public static void prepareMainLooper() {
            prepare(false);//创建Looper对象,并保存到ThreadLocal中
            synchronized (Looper.class) {
                if (sMainLooper != null) {
                    throw new IllegalStateException("The main Looper has already been prepared.");
                }
                sMainLooper = myLooper();//得到Looper对象,并保存到Looper类的静态字段中
            }
     }
    

    下面看prepare方法的原码:

    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));
    }
    

    在第一次调用sThreadLocal.get()方法得到的一定是null,所以此时的重点是创建Looper对象,并放入sThreadLocal中储存起来。
    这里要明确两点:

    1. sThreadLocal是Looper类的静态字段,所以只有一个sThreadLocal对象。
    2. prepare方法在UI线程被调用,所以只有在Ui线程才能从sThreadLocal对象中获取到looper对象。

    下面看myLooper方法的原码:

    public static @Nullable Looper myLooper() {
          return sThreadLocal.get();
    }
    

    这个方法很简单,目的就是得到UI线程中的Looper对象。注意这个方法是静态方法,得到的Looper对象就是在UI线程中创建的Looper对象。

    2. 在hanlder类中使用Looper对象

    我们知道在创建Handler对象时使用的是无参构造,但无参构造方法调用了下面的构造方法:

    public Handler(Callback callback, boolean async) {
        if (FIND_POTENTIAL_LEAKS) {
            final Class<? extends Handler> klass = getClass();
            if ((klass.isAnonymousClass() || klass.isMemberClass() || klass.isLocalClass()) &&
                    (klass.getModifiers() & Modifier.STATIC) == 0) {
                Log.w(TAG, "The following Handler class should be static or leaks might occur: " +
                    klass.getCanonicalName());
            }
        }
    
        mLooper = Looper.myLooper();
        if (mLooper == null) {
            throw new RuntimeException(
                "Can't create handler inside thread that has not called Looper.prepare()");
        }
        mQueue = mLooper.mQueue;
        mCallback = callback;
        mAsynchronous = async;
    }
    

    由源码可知,Handler类中有mLooper字段和mQueue字段。mLooper字段赋值时通过Looper.myLooper()方法,我们从上面知道这个方法返回的值就是UI线程中创建的Looper对象,所以此时的Looper对象和Ui线程中创建的Looper对象是同一个。

    3. 在Looper的loop方法中使用Looper对象处理消息

    在Looper类的loop方法中获取Looper对象的代码是:
    final Looper me = myLooper();
    很明显通过myLooper方法得到的Looper对象就是UI线程中创建的Looper对象。

    我们知道在在Looper的prepare方法中创建了Looper对象,并放入到ThreadLocal中,那么我们来看一下Looper的构造方法:

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

    在Looper的构造方法中创建了MessageQueue对象,并赋值给mQueue字段。因为Looper对象只有一个,那么Messagequeue对象肯定只有一个。

    下面我们再多学习一点,找到使用MessageQueue使用的地方。
    MessageQueue被两个地方使用,一是在handler的sendMessage中发送消息。二是在looper类的loop方法中取出消息。

      1. 在看Handler的构造方法中我们知道,在构造方法中通过Looper.myPrepare方法得到mLooper对象,又通过mLooper.mQueue得到Messagequeue对象。所以此时这个MessageQueue对象就是在Looper的构造方法中创建的对象。
      1. 在Looper的loop方法中也是首先通过Looper.myPrepare()方法得到Looper对象,然后得到MessageQueue对象。

    第五问,looper对象只有一个,在分发消息时怎么区分不同的handler?

    关于这个问题主要看三点:

    • 一是Message类中有个字段target,这个字段是Handler类型的。
    • 二在Handler的enqueueMessage方法中有这么一句代码:
      msg.target = this;
      即把handler对象与Message绑定在了一起。
    • 三在Looper类的looper方法中分发消息的代码是:msg.target.dispatchMessage(msg);

    此时我们就明白了:在发送消息时handler对象与Message对象绑定在了一起。在分发消息时首先取出Message对象,然后就可以得到与它绑定在一起的Handler对象了。


    第六问,为什么发送消息在子线程,而处理消息就变成主线程了,在哪儿跳转的?

    发送消息使用的是Handler的sendMessage方法,这个方法最终调用的是enqueueMessage方法,enqueueMessage方法的原码是:

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

    这个方法中调用的是MessageQueue的enqueueMessage方法,只要queue对象是UI线程中的MessageQueue对象,那么就能被Ui线程中的Looper从消息队列中取出来,然后就在主线程执行了。

    我们知道Handler对象中的mQueue就是UI线程中的消息队列对象,所以在处理消息时就是主线程了。


    第七问,能不能在子线程中创建Handler对象?怎么在子线程中和主线程通信?

    这个问题并不是简单的能不能的问题,考察的是对Handler的深入理解。答案肯定是可以的,但是仅仅使用无参构造是不可以的,还需要做其他的操作。

    仅仅使用无参构造为什么不能创建Handler对象?

    我们知道Handler的无参构造最终调用了是另一个构造方法,下面还是看这个构造方法的原码:

    public Handler(Callback callback, boolean async) {
        mLooper = Looper.myLooper();
        if (mLooper == null) {
            throw new RuntimeException(
                "Can't create handler inside thread that has not called Looper.prepare()");
        }
    }
    

    在上面代码中我们看到,如果得到的Looper对象是null就会抛出异常。为什么在子线程中Looper.myLooper方法会返回null呢?原因就是ThreadLocal的特性了。Looper.myLooper方法的原码是从ThreadLocal中得到Looper对象,而在Looper.prepare方法中Looper对象是在UI线程中放入到ThreadLocal中的,所以在子线程中是得不到Looper对象的。在handler中没有looper对象就会抛出异常。

    为什么在handler中没有looper对象就会抛出异常?

    我们知道handler发送消息最终调用的是MessageQueue的enqueueMessage方法,如果没有Looper对象,肯定得不到MessageQueue对象,在发送消息时一定会抛出nullPointException。所以系统在前面就进行了拦截,只要没有Looper对象就不让代码继续执行。

    怎么在子线程中创建Handler对象?

    在UI线程之所以可以直接创建创建Handler对象,是因为在Ui线程已经有了Looper对象,所以只要我们在子线程中创建Looper对象后就可以创建handler对象了。使用示例如下:

    new Thread(new Runnable() {
           @Override
            public void run() {
                 Looper.prepare();//创建looper对象
                 Looper.loop();//开启子线程中的消息循环
                 Handler handler = new Handler(){//创建Handler对象
                     @Override
                      public void handleMessage(Message msg) {
                            super.handleMessage(msg);
                      }
                  };
            }
    }).start();
    

    此时就在子线程中维护了一套消息循环系统。
    注意:这套消息循环系统与UI线程中的Handler机制没有任何关系,这儿的消息不能与Ui线程中的消息队列进行通信。但子线程中的消息循环系统也有很大的作用,当需要处理很多消息时可以让消息按序列执行。在图片加载框架Picasso中就在子线程中维护了一套消息循环系统,感兴趣的小伙伴可以自行查看Picasso的原码。

    怎么在子线程中得到主线程中handler对象?

    其实handler对象没有主线程和子线程之分,有区分的是Looper对象,如果Looper对象是主线程中的,那么handler就是主线程中的。
    Handler有下面一个构造方法:

    public Handler(Looper looper) {
        this(looper, null, false);
    }
    

    这个构造方法中接收一个Looper对象。只要我们能到子线程中得到主线程的Looper对象,那么就可以实现在子线程中得到主线程中的handler对象了。
    在Looper类中有getMainLooper方法,这个方法的源码是:

    public static Looper getMainLooper() {
        synchronized (Looper.class) {
            return sMainLooper;
        }
    }
    

    这个方法返回的是在主线程中创建过的Looper对象。所以这个方法无论在哪儿调用得到的都是主线程中的Looper对象,此时我们就可以在子线程中创建主线程的handler对象了,代码如下:

    new Thread(new Runnable() {
         @Override
         public void run() {
             Handler handler = new Handler(Looper.getMainLooper());//得到UI线程中的handler对象
             handler.post(new Runnable() {
                @Override
                 public void run() {
                    //这儿写逻辑代码
                 }
             });
         }
     }).start();
    

    注:使用这种方法可以轻松的从子线程跳转到UI线程,完全不依赖于Activity或Application。图片加载框架Picasso,网络请求控件Volley都是通过这种方法在工具类内部实现了更新UI。


    第八问,handler为什么会造成内存泄漏?如何避免?

    1. App第一次启动的时候会在主线程中创建Looper,Looper会一个一个处理MessageQueue中的Message,而主线程中的Looper是贯穿APP整个生命周期的.
    2. 当在Activity中创建一个Handler的时候会关联到MessageQueue(见上面handler构造代码),在消息分发的时候会把Handler和Message绑定在一起,Looper会调用Handler#handleMessage(Message) 处理Message.
    3. 在Java中非静态内部类和匿名内部类会持有外部应用,所以此时的handler持有的Activity会在Looper中存在不会被回收,就会造成内存泄漏.

    如何避免?

    若在Activity中使用Handler时候可以采用静态内部类的形势。静态内部类不包含对其外部类的隐式引用,因此Activity不会泄漏。如果需要从Handler内部调用外部Activity的方法,让handler持有weakreference<Activity>,这样就不会意外泄漏上下文。或者在Activity生命周期终结的时候手动remove掉还未处理的Message.


    第九问,主线程中Looper中的轮询死循环为何没有阻塞主线程?

    • 线程是一段可执行的代码,当可执行代码执行完成后,线程生命周期便该终止了,线程退出。而对于主线程,我们是绝不希望会被运行一段时间,自己就退出,那么如何保证能一直存活呢?简单做法就是可执行代码是能一直执行下去的,死循环便能保证不会被退出,例如,binder线程也是采用死循环的方法,通过循环方式不同与Binder驱动进行读写操作,当然并非简单地死循环,无消息时会休眠。但这里可能又引发了另一个问题,既然是死循环又如何去处理其他事务呢?通过创建新线程的方式。

    • 真正会卡死主线程的操作是在回调方法onCreate/onStart/onResume等操作时间过长,会导致掉帧,甚至发生ANR,looper.loop本身不会导致应用卡死。

    • 主线程的死循环一直运行是不是特别消耗CPU资源呢? 其实不然,这里就涉及到Linux pipe/epoll机制,简单说就是在主线程的MessageQueue没有消息时,便阻塞在loop的queue.next()中的nativePollOnce()方法,此时主线程会释放CPU资源进入休眠状态,直到下个消息到达或者有事务发生,通过往pipe管道写端写入数据来唤醒主线程工作。这里采用的epoll机制,是一种IO多路复用机制,可以同时监控多个描述符,当某个描述符就绪(读或写就绪),则立刻通知相应程序进行读或写操作,本质同步I/O,即读写是阻塞的。 所以说,主线程大多数时候都是处于休眠状态,并不会消耗大量CPU资源。


    第十问,next()操作是如何实现阻塞和唤醒的?

    通过上面那一问,我们知道next()方法是通过调用底层代码的nativePollOnce()方法,这个方法涉及到Linux pipe/epoll机制(epoll是一种当文件描述符的内核缓冲区非空的时候,发出可读信号进行通知,当写缓冲区不满的时候,发出可写信号通知的机制).


    第十一问,Handler.postDelayed()是先delay一定的时间,然后再放入messageQueue中,还是先直接放入MessageQueue中,然后在里面wait delay的时间?

    一步一步跟一下Handler.postDelayed()的调用路径:

    • Handler.postDelayed(Runnable r, long delayMillis)
    • Handler.sendMessageDelayed(getPostMessage(r), delayMillis)
    • Handler.sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis)
    • Handler.enqueueMessage(queue, msg, uptimeMillis)
    • MessageQueue.enqueueMessage(msg, uptimeMillis)

    最后发现Handler没有自己处理Delay,而是交给了MessageQueue处理的.
    整个调用流程如下:

    1. postDelay()一个10秒钟的Runnable A、消息进队,MessageQueue调用nativePollOnce()阻塞,Looper阻塞;
    2. 紧接着post()一个Runnable B、消息进队,判断现在A时间还没到、正在阻塞,把B插入消息队列的头部(A的前面),然后调用nativeWake()方法唤醒线程;
    3. MessageQueue.next()方法被唤醒后,重新开始读取消息链表,第一个消息B无延时,直接返回给Looper;
    4. Looper处理完这个消息再次调用next()方法,MessageQueue继续读取消息链表,第二个消息A还没到时间,计算一下剩余时间(假如还剩9秒)继续调用nativePollOnce()阻塞;
    5. 直到阻塞时间到或者下一次有Message进队;

    这样,基本上就能保证Handler.postDelayed()发布的消息能在相对精确的时间被传递给Looper进行处理而又不会阻塞队列了。

    MessageQueue会根据post delay的时间排序放入到链表中,链表头的时间小,尾部时间最大。因此能保证时间Delay最长的不会block住时间短的。当每次post message的时候会进入到MessageQueue的next()方法,会根据其delay时间和链表头的比较,如果更短则,放入链表头,并且看时间是否有delay,如果有,则block,等待时间到来唤醒执行,否则将唤醒立即执行。

    所以handler.postDelay并不是先等待一定的时间再放入到MessageQueue中,而是直接进入MessageQueue,以MessageQueue的时间顺序排列和唤醒的方式结合实现的。使用后者的方式,我认为是集中式的统一管理了所有message,而如果像前者的话,有多少个delay message,则需要起多少个定时器。前者由于有了排序,而且保存的每个message的执行时间,因此只需一个定时器按顺序next即可。


    第十二问,Message消息有优先级么?如何优先执行一个Message?

    通过十一问,我们可以更改延迟时间来更改Message的执行时间.如果是相同时间的Message可以采用同步屏障,把Message设置为异步消息,优先执行.

    相关文章

      网友评论

        本文标题:Handler的一问一答

        本文链接:https://www.haomeiwen.com/subject/khkzrqtx.html