面试整理-Handler机制

312 阅读7分钟

面试可能会问到的问题

  1. Handler的实现原理
  2. 子线程中能不能直接new一个Handler,为什么主线程可以?
  3. Handler导致的内存泄漏原因及其解决方案
  4. 一个线程可以有几个Handler,几个Looper,几个MessageQueue对象
  5. HandlerThread 是什么 & 好处 & 原理 & 使用场景
  6. 子线程中是否可以用MainLooper去创建Handler,Looper和Handle是否是同一线程?
  7. 什么是ThreadLocal?
  8. 消息屏障,同步屏障机制
  9. Looper死循环为什么不会导致应用卡死?

好自己找答案去😄,开玩笑。但有些基础问题讲来也没意思,老掉牙了。抽几个讲讲

HandlerThread 是什么 & 好处 & 原理 & 使用场景

  • 这个类封装了Handler、looper、MessageQueue。主要的作用是在子线程中使用Handler。

  • 优势:减轻主线程的压力,有自己的消息队列不会影响到主线程。

  • 用途:Camera的使用就需要一个HandlerThread,这个是常用的方法不是必须的,不要也可以运行,但是因为处理消息过多会导致阻塞发生卡顿。

子线程中是否可以用MainLooper去创建Handler,Looper和Handle是否是同一线程?

  • 可以创建,Handler里面如果不传的话就需要自己创建一个looper,因为是通过sThreadLocal去拿的,如果子线程是get不到值的。

以下是handler的源码

 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()");
       }
public static @Nullable Looper myLooper() {
    return sThreadLocal.get();
}

handler创建时会先检查有没looper,myLooper最后调用的是ThreadLocal的方法,如果是主线程可以直接获取到looper,如果是子线程就需要先创建一个looper

如果要在子线程使用

class MyThread : Thread() {
    private lateinit var handler: Handler

    override fun run() {
        // 在子线程中创建 Handler,并使用该线程的 Looper
        Looper.prepare()
        handler = object : Handler() {
            override fun handleMessage(msg: Message) {
                // 处理消息
            }
        }
        Looper.loop()
    }
}
  • Looper 和 Handler 并不是同一个线程。Looper 用于管理消息队列,而 Handler 用于处理消息。简单的说这里的Looper是主线程,而Handler只是一个搬运工谁new它的它就跟谁同一个线程,

什么是ThreadLocal

  • 类似于hashMap,但ThreadLocal的key是线程,value是looper。意味着每个线程都有自己的空间,是一种空间换时间的方案。

消息屏障,同步屏障机制

同步消息被过滤了就叫同步屏障,异步消息被过滤了就叫异步屏障。一个名字而已

  • 消息屏障表现在代码里面就是一个判断,而Handle所谓的同步屏障就是处理消息时多加了一个判断msg.target == null。当符合这个判断的时候会进入判断找出一条异步消息,当同步屏障没有消失之前会处理异步消息过滤掉同步消息来做到【异步消息优先执行】的功能

  • 这样设计有什么好处:

  1. view更新时需要优先保证ui的刷新再执行其他,在ViewRootImpt.scheduleTraversals()方法中就使用了同步屏障
  2. 当发了一条消息立马移除时也是使用了同步屏障

想象一下如果发送一条1s的延时消息,然后for循环插入一百条消息,for循环结束移除消息,这个时候无论中间有多少条消息也都是先保障移除消息先被执行的

Looper死循环为什么不会导致应用卡死?

 Looper死循环不会导致应用卡死,是因为Looper的内部实现中使用了阻塞式的等待机制。没有消息的时候会进入阻塞状态,有消息的时候才会唤醒。真正卡死的是处理message处理不过来导致没办法响应出现卡顿甚至ANR。

你要这么回答,面试官就没办法怎么继续问呢cpu是不是一直在运行了😏,因为阻塞嘛。补损耗资源


如果继续继续问这个阻塞机制是怎么样的?

  • 核心是Loop的queue.next()中的nativePollOnce()方法 里面主要用到的机制是Linux pipe(管道) 和 epoll,在底层消息会被封装成一个结构体并通过管道写入到 pipe 的写端,然后 Looper 会通过 epoll 监听读端是否有消息到来,如果有就从 pipe 读取消息并交给 Handler 进行处理。如果处理完消息便阻塞线程,直到下一个消息的到来。

  • epoll 是 Linux 内核提供的一种高效的 I/O 事件通知机制。所以能监听到管道的I/O事件。但是epoll只能知道事件不知道具体内容

符:epoll/pipe的使用

  • epoll的使用
  1. 创建 epoll 实例,即调用 epoll_create() 函数;
  2. epoll 实例中注册需要监听的文件描述符,即调用 epoll_ctl() 函数;
  3. 调用 epoll_wait() 函数,阻塞等待文件描述符的 I/O 事件;
  4. 如果有文件描述符发生 I/O 事件,epoll_wait() 函数将返回这些文件描述符的相关信息。

在使用 epoll 时,将文件描述符插入到监听队列中,是通过 epoll_ctl() 函数来完成的,该函数会将文件描述符加入到 epoll 实例的红黑树中,并同时将该文件描述符相关的事件注册到内核的事件表中。当文件描述符上有 I/O 事件发生时,内核会将该事件插入到该文件描述符相关的等待队列中,然后唤醒应用程序,这样就能监听到该事件。

  • pipe的使用

管道的概念就是一个进程的输入是一个进程的输出,所以只需要两个地址。下面就是传入了一个数组。在c语言中经常使用这种套路传一个地址赋值,类似于安卓的里面获取view位置传的location[2]。

#include <unistd.h>
int pipe(int fildes[2]); // int[0] 表示读; int[1] 表示写
使用的时候父进程调用:
write(fd[1], "hello world", 12); // 表示将 `"hello world"` 这个字符串中的前12个字节写入到 `fd[1]` 所对应的写入端口
子进程可以这样获取到:
read(fd[0], buf, 256); // 256 是指读取的最大字节数
printf("child process received: %s\n", buf);

下面是一个epoll监听pipe的例子:

其实在代码中能很清晰的看到epoll对象是怎么跟pipe对象产生联系的,在编程的世界里一旦产生联系就能为所欲为😁

#include <sys/epoll.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>

#define MAX_EVENTS 10
#define READ_SIZE 10

int main() {
    int pipe_fd[2];
    int epoll_fd;
    int nfds, i, len;
    char buffer[READ_SIZE + 1];
    struct epoll_event ev, events[MAX_EVENTS];

    // 创建 pipe 和 epoll 文件描述符
    pipe(pipe_fd);
    epoll_fd = epoll_create1(0);

    // 添加 pipe 的读端到 epoll 中进行监听
    ev.events = EPOLLIN;
    ev.data.fd = pipe_fd[0];
    epoll_ctl(epoll_fd, EPOLL_CTL_ADD, pipe_fd[0], &ev);

    // 开始循环监听 epoll 事件
    while (1) {
        nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);

        for (i = 0; i < nfds; i++) {
            // 如果是 pipe 的读端事件,则读取 pipe 中的数据
            if (events[i].data.fd == pipe_fd[0]) {
                len = read(pipe_fd[0], buffer, READ_SIZE);
                buffer[len] = '\0';
                printf("Received %d bytes: %s\n", len, buffer);
            }
        }
    }

    // 关闭文件描述符
    close(epoll_fd);
    close(pipe_fd[0]);
    close(pipe_fd[1]);
    return 0;
}

在这个示例中,我们首先创建了一个管道 pipe_fd,并使用 epoll_create1 创建了一个 epoll 文件描述符 epoll_fd。然后,我们将 pipe_fd 的读端添加到 epoll 中进行监听,使用 epoll_ctlEPOLL_CTL_ADD 命令将其添加到 epoll 中。

接着,在 while 循环中,我们调用 epoll_wait 等待 epoll 事件发生,如果有事件发生,我们就循环处理每一个事件,如果事件对应的文件描述符是 pipe_fd 的读端,我们就调用 read 函数从 pipe 中读取数据并输出到屏幕上。

最后,我们在程序结束时关闭了所有文件描述符。这个示例只是一个简单的演示如何使用 epoll 监听 pipe,实际上在 Android Handler 和 Looper 的实现中还涉及到更复杂的细节和处理逻辑。