Framework源码面试六部曲:3.Handler面试集合

前言

今天在电脑上翻出了很久之前整理笔记Framework源码面试,Flutter,以及一部分面试专题。拿出来温习一下。

公众号:初一十五a

今天先讲Framework源码篇
1.Framework源码面试:Activity启动流程

2.Framework源码面试:Binder面试

3.Framework源码面试:Handler面试

4.Framework源码面试:事件分发机制

5.Framework源码面试:onMeasure测量原理

6.Framework源码面试:Android屏幕刷新机制

1.Handler怎么在主线程和子线程进行数据交互的原理?

主线程和子线程通过handler交互,交互的载体是通过Message这个对象,实际上我们在子线程发送的所有消息,都会加入到主线程的消息队列中,然后主线程分发这些消息,这个就很容易做到俩个线程信息的交互。

看到这里,你可能有疑问了,我从子线程发送的消息,怎么就加到了主线程的消息队列里呢???

大家可以看看你自己的代码,你的handler对象是不是在主线程初始的?子线程发送消息,是不是通过这个handler发送的?

这就很简单了,handler只需要把发送的消息,加到自身持有的Looper对象的MessageQueue里面(mLooper变量)就ok了

所以,你在哪个线程里面初始化Handler对象,在不同的线程中,使用这个对象发送消息;都会在你初始化Handler对象的线程里分发消息。

2.Handler中主线程的消息队列是否有数量上限?为什么?

这问题整的有点鸡贼,可能会让你想到,是否有上限这方面?而不是直接想到到上限数量是多少?

解答Handler主线程的消息队列肯定是有上限的,每个线程只能实例化一个Looper实例(上面讲了,Looper.prepare只能使用一次),不然会抛异常,消息队列是存在Looper()中的,且仅维护一个消息队列

重点:每个线程只能实例化一次Looper()实例、消息队列存在Looper

拓展MessageQueue类,其实都是在维护mMessage,只需要维护这个头结点,就能维护整个消息链表

3.Handler中有Loop死循环,为什么没有卡死?为什么没有发生ANR

先说下ANR:5秒内无法响应屏幕触摸事件或键盘输入事件;广播的onReceive()函数时10秒没有处理完成;前台服务20秒内,后台服务在200秒内没有执行完毕;ContentProviderpublish在10s内没进行完。所以大致上Loop死循环和ANR联系不大,问了个正确的废话,所以触发事件后,耗时操作还是要放在子线程处理,handler将数据通讯到主线程,进行相关处理。

线程实质上是一段可运行的代码片,运行完之后,线程就会自动销毁。当然,我们肯定不希望主线程被over,所以整一个死循环让线程保活。

为什么没被卡死:在事件分发里面分析了,在获取消息的next()方法中,如果没有消息,会触发nativePollOnce方法进入线程休眠状态,释放CPU资源,MessageQueue中有个原生方法nativeWake方法,可以解除nativePollOnce的休眠状态,ok,咱们在这俩个方法的基础上来给出答案。

  • 当消息队列中消息为空时,触发MessageQueue中的nativePollOnce方法,线程休眠,释放CPU资源

  • 消息插入消息队列,会触发nativeWake唤醒方法,解除主线程的休眠状态

    • 当插入消息到消息队列中,为消息队列头结点的时候,会触发唤醒方法
    • 当插入消息到消息队列中,在头结点之后,链中位置的时候,不会触发唤醒方法

综上:消息队列为空,会阻塞主线程,释放资源;消息队列为空,插入消息时候,会触发唤醒机制

  • 这套逻辑能保证主线程最大程度利用CPU资源,且能及时休眠自身,不会造成资源浪费

本质上,主线程的运行,整体上都是以事件(Message)为驱动的。

4.为什么不建议在子线程中更新UI?

多线程操作,在UI的绘制方法表示这不安全,不稳定。

假设一种场景:我会需要对一个圆进行改变,A线程将圆增大俩倍,B改变圆颜色。A线程增加了圆三分之一体积的时候,B线程此时,读取了圆此时的数据,进行改变颜色的操作;最后的结果,可能会导致,大小颜色都不对。。。

5.可以让自己发送的消息优先被执行吗?原理是什么?

这个问题,我感觉只能说:在有同步屏障的情况下是可以的。

同步屏障作用:在含有同步屏障的消息队列,会及时的屏蔽消息队列中所有同步消息的分发,放行异步消息的分发。

在含有同步屏障的情况,我可以将自己的消息设置为异步消息,可以起到优先被执行的效果。

6.子线程和子线程使用Handler进行通信,存在什么弊端?
子线程和子线程使用Handler通信,某个接受消息的子线程肯定使用实例化handler,肯定会有Looper操作,Looper.loop()内部含有一个死循环,会导致线程的代码块无法被执行完,该线程始终存在。

如果在完成通信操作,我们一般可以使用: mHandler.getLooper().quit() 来结束分发操作

说明下quit()方法进行几项操作

  • 清空消息队列(未分发的消息,不再分发了)
  • 调用了原生的销毁方法 nativeDestroy(猜测下:可能是一些资源的释放和销毁)
  • 拒绝新消息进入消息队列
  • 它可以起到结束loop()死循环分发消息的操作

拓展quitSafely()可以确保所有未完成的事情完成后,再结束消息分发。

7.Handler中的阻塞唤醒机制?

这个阻塞唤醒机制是基于 Linux 的 I/O 多路复用机制 epoll实现的,它可以同时监控多个文件描述符,当某个文件描述符就绪时,会通知对应程序进行读/写操作.

MessageQueue 创建时会调用到 nativeInit,创建新的 epoll 描述符,然后进行一些初始化并监听相应的文件描述符,调用了epoll_wait方法后,会进入阻塞状态;nativeWake触发对操作符的 write 方法,监听该操作符被回调,结束阻塞状态。

8.什么是IdleHandler?什么条件下触发IdleHandler

IdleHandler的本质就是接口,为了在消息分发空闲的时候,能处理一些事情而设计出来的

具体条件:消息队列为空的时候、发送延时消息的时候

9.消息处理完后,是直接销毁吗?还是被回收?如果被回收,有最大容量吗?

Handler存在消息池的概念,处理完的消息会被重置数据,采用头插法进入消息池,取的话也直接取头结点,这样会节省时间

消息池最大容量为50,达到最大容量后,不再接受消息进入

10.不当的使用Handler,为什么会出现内存泄漏?怎么解决?

先说明下,Looper对象在主线程中,整个生命周期都是存在的,MessageQueue是在Looper对象中,也就是消息队列也是存在在整个主线程中;我们知道Message是需要持有Handler实例的,Handler又是和Activity存在强引用关系

存在某种场景:我们关闭当前Activity的时候,当前Activity发送的Message,在消息队列还未被处理,Looper间接持有当前activity引用,因为俩者直接是强引用,无法断开,会导致当前Activity无法被回收

思路:断开俩者之间的引用、处理完分发的消息,消息被处理后,之间的引用会被重置断开

解决:使用静态内部类弱引Activity、清空消息队列

Handler的作用:
当我们需要在子线程处理耗时的操作(例如访问网络,数据库的操作),而当耗时的操作完成后,需要更新UI,这就需要使用Handler来处理,因为子线程不能做更新UI的操作。Handler能帮我们很容易的把任务(在子线程处理)切换回它所在的线程。简单理解,Handler就是解决线程和线程之间的通信的。

Handler连环之说说Handler的作用,以及每个类让他们的角色

使用的handler的两种形式

1.在主线程使用handler;
2.在子线程使用handler。

Handler的消息处理主要有五个部分组成

Message

Handler

Message Queue

Looper

ThreadLocal

首先简要的了解这些对象的概念

  1. Message:Message是在线程之间传递的消息,它可以在内部携带少量的数据,用于线程之间交换数据。Message有四个常用的字段,what字段,arg1字段,arg2字段,obj字段。what,arg1,arg2可以携带整型数据,obj可以携带object对象。
  2. Handler:它主要用于发送和处理消息的发送消息一般使用sendMessage()方法,还有其他的一系列sendXXX的方法,但最终都是调用了sendMessageAtTime方法,除了sendMessageAtFrontOfQueue()这个方法 而发出的消息经过一系列的辗转处理后,最终会传递到Handler的handleMessage方法中。
  3. Message Queue:MessageQueue是消息队列的意思,它主要用于存放所有通过Handler发送的消息,这部分的消息会一直存在于消息队列中,等待被处理。每个线程中只会有一个MessageQueue对象。
  4. Looper:每个线程通过Handler发送的消息都保存在,MessageQueue中,Looper通过调用loop()的方法,就会进入到一个无限循环当中,然后每当发现Message Queue中存在一条消息,就会将它取出,并传递到Handler的handleMessage()方法中。每个线程中只会有一个Looper对象。

ThreadLocal:MessageQueue对象,和Looper对象在每个线程中都只会有一个对象,怎么能保证它只有一个对象,就通过ThreadLocal来保存。Thread Local是一个线程内部的数据存储类,通过它可以在指定线程中存储数据,数据存储以后,只有在指定线程中可以获取到存储到数据,对于其他线程来说则无法获取到数据。

Handler连环泡之 说说 Looper 死循环为什么不会导致应用卡死?

​ 线程默认没有Looper的,如果需要使用Handler就必须为线程创建Looper。我们经常提到的主线程,也叫UI线程,它就是ActivityThreadActivityThread被创建时就会初始化Looper,这也是在主线程中默认可以使用Handler的原因。

首先我们看一段代码

 new Thread(new Runnable() {
        @Override
        public void run() {
            Log.e("qdx", "step 0 ");
            Looper.prepare();

            Toast.makeText(MainActivity.this, "run on Thread", Toast.LENGTH_SHORT).show();

            Log.e("qdx", "step 1 ");
            Looper.loop();

            Log.e("qdx", "step 2 ");

        }
    }).start();

​ 我们知道Looper.loop();里面维护了一个死循环方法,所以按照理论,上述代码执行的应该是 step 0 –>step 1 也就是说循环在Looper.prepare();与Looper.loop();之间。

​ 在子线程中,如果手动为其创建了Looper,那么在所有的事情完成以后应该调用quit方法来终止消息循环,否则这个子线程就会一直处于等待(阻塞)状态,而如果退出Looper以后,这个线程就会立刻(执行所有方法并)终止,因此建议不需要的时候终止Looper

​ 执行结果也正如我们所说,这时候如果了解了ActivityThread,并且在main方法中我们会看到主线程也是通过Looper方式来维持一个消息循环


    public static void main(String[] args) {
        Looper.prepareMainLooper();//创建Looper和MessageQueue对象,用于处理主线程的消息
        ActivityThread thread = new ActivityThread();
        thread.attach(false);//建立Binder通道 (创建新线程)

        if (sMainThreadHandler == null) {
            sMainThreadHandler = thread.getHandler();
        }

        Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
        Looper.loop();

        //如果能执行下面方法,说明应用崩溃或者是退出了...
        throw new RuntimeException("Main thread loop unexpectedly exited");
      }

那么回到我们的问题上,这个死循环会不会导致应用卡死,即使不会的话,它会慢慢的消耗越来越多的资源吗?

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

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

Handler连环泡之 说说 Looper 死循环为什么不会导致应用卡死?

事实上,会在进入死循环之前便创建了新binder线程,在代码ActivityThread.main()中:

public static void main(String[] args) {
//创建Looper和MessageQueue对象,用于处理主线程的消息
 Looper.prepareMainLooper();

 //创建ActivityThread对象
 ActivityThread thread = new ActivityThread(); 

 //建立Binder通道 (创建新线程)
 thread.attach(false);

 Looper.loop(); //消息循环运行
 throw new RuntimeException("Main thread loop unexpectedly exited");
}

Activity的生命周期都是依靠主线程的Looper.loop,当收到不同Message时则采用相应措施:一旦退出消息循环,那么你的程序也就可以退出了。 从消息队列中取消息可能会阻塞,取到消息会做出相应的处理。如果某个消息处理时间过长,就可能会影响UI线程的刷新速率,造成卡顿的现象。

thread.attach(false)方法函数中便会创建一个Binder线程(具体是指ApplicationThread,Binder的服务端,用于接收系统服务AMS发送来的事件),该Binder线程通过Handler将Message发送给主线程。「Activity 启动过程」

​ 比如收到msg=H.LAUNCH_ACTIVITY,则调用ActivityThread.handleLaunchActivity()方法,最终会通过反射机制,创建Activity实例,然后再执行Activity.onCreate()等方法;

再比如收到msg=H.PAUSE_ACTIVITY,则调用ActivityThread.handlePauseActivity()方法,最终会执行Activity.onPause()等方法。

公众号:初一十五a

作者:初一十五不吃饭

%s 个评论

要回复文章请先登录注册