Moosphan / Android-Daily-Interview

:pushpin:每工作日更新一道 Android 面试题,小聚成河,大聚成江,共勉之~

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

2019-10-22:Handler中有Loop死循环,为什么没有阻塞主线程,原理是什么?

MoJieBlog opened this issue · comments

2019-10-22:Handler中有Loop死循环,为什么没有阻塞主线程,原理是什么?

我不太明白, 阻塞-挂起-block-suspend, 暂时混为一谈;

主线程挂起

Looper 是一个死循环, 不断的读取MessageQueue中的消息, loop 方法会调用 MessageQueue 的 next 方法来获取新的消息,
next 操作是一个阻塞操作,当没有消息的时候 next 方法会一直阻塞, 进而导致 loop 一直阻塞,
理论上 messageQueue.nativePollOnce 会让线程挂起-阻塞-block 住, 但是为什么, 在发送 delay 10s 的消息, 假设消息队列中, 目前只有这一个消息;
那么为什么在这 10s 内, UI是可操作的, 或者列表页是可滑动的, 或者动画还是可以执行的?
先不讲 nativePollOnce 是怎么实现的阻塞, 我们还知道, 另外一个 nativeWake, 是实现线程唤醒的;
那么什么时候会, 触发这个方法的调用呢, 就是在有新消息添加进来的时候, 可是并没有手动添加消息啊?
display 每隔16.6秒, 刷新一次屏幕;
SurfaceFlingerVsyncChoreographer 每隔16.6秒, 发送一个 vSync 信号;
FrameDisplayEventReceiver 收到信号后, 调用 onVsync 方法, 通过 handler 消息发送到主线程处理, 所以就会有消息添加进来, UI线程就会被唤醒;
事实上, 安卓系统, 不止有一个屏幕刷新的信号, 还有其他的机制, 比如输入法和系统广播, 也会往主线程的 MessageQueue 添加消息;
所以, 可以理解为, 主线程也是随时挂起, 随时被阻塞的;

系统怎么实现的阻塞与唤醒

这种机制是通过pipe(管道)机制实现的;
简单来说, 管道就是一个文件
在管道的两端, 分别是两个打开文件的, 文件描述符, 这两个打开文件描述符, 都是对应同一个文件, 其中一个是用来读的, 别一个是用来写的;
一般的使用方式就是, 一个线程通过读文件描述符, 来读管道的内容, 当管道没有内容时, 这个线程就会进入等待状态,
而另外一个线程, 通过写文件描述符, 来向管道中写入内容, 写入内容的时候, 如果另一端正有线程, 正在等待管道中的内容, 那么这个线程就会被唤醒;
这个等待和唤醒的操作是如何进行的呢, 这就要借助 Linux 系统中的 epoll 机制了, Linux 系统中的 epoll 机制为处理大批量句柄而作了改进的 poll,
是 Linux 下多路复用 IO 接口 select/poll 的增强版本, 它能显著减少程序, 在大量并发连接中, 只有少量活跃的情况下的系统 CPU 利用率;

即当管道中有内容可读时, 就唤醒当前正在等待管道中的内容的线程;

怎么证明, 线程被挂起了

@Override
public void onCreateData(@Nullable Bundle bundle) {

    new Thread() {
        @SuppressLint("HandlerLeak")
        @Override
        public void run() {
            super.run();
            LogTrack.v("thread.id = " + Thread.currentThread().getId());
            Looper.prepare();
            Handler handler = new Handler(Looper.getMainLooper()) {
                @Override
                public void handleMessage(Message msg) {
                    super.handleMessage(msg);
                    LogTrack.v("thread.id = " + Thread.currentThread().getId() + ", what = " + msg.what);
                }
            };
            LogTrack.w("loop.之前");  // 执行了
            Looper.loop();  // 执行了
            LogTrack.w("loop.之后");  // 无法执行
        }
    }.start();

}
commented

不久之前,知乎上看到的答案,很不错,贴出来,不再重复造轮子,有限的时间要花在好的东西上
https://www.zhihu.com/question/34652589/answer/90344494?from=profile_answer_card

commented

可以这样简单的来理解一下,一个Thread对应一个Looper和一个MessageQueue
这个MessageQueue是个一个阻塞队列,类似BlockingQueue,不同之处在于MessageQueue的阻塞方式是通过Pipe机制实现的。
阻塞队列,就是当队列里没有数据时,如果调用获取队首数据的方法时,当前线程会被阻塞(相当于执行了线程的wait方法),如果队列里面有了插入了新数据,则会唤醒被阻塞的方法(相当于执行了线程的notify方法),并返回该数据。再来看MessageQueue,这里的数据指的就是是每一个消息,这个消息则是通过handler来发送的。

综上所述,线程并没有一直死循环的工作,而是在没消息时被暂时挂起了,当有新消息进来的时候,就会又开始工作。

我们的所说的阻塞主线程,其实指的是 ActivityThread 这个线程,它维护为一个叫做 H 的 Handler,Activity 的启动等都是通过它来实现的。
但H既然是个 Handler,那么它里面肯定也有一个Looper维持着 Loop 的死循环,按理说,肯定会阻塞啊?
但其实不是,因为还有个 ApplicationThread 的Binder线程在运行的,它主要 和 AMS 进行交互,比如触摸事件,启动其他组件事件等,它会把这些信息拿到之后,再与 ActivityThread 进行数据传输,所以ActivityThread 的loop循环其实不影响 applicationThread;

这个跟快递分类一样,传送带上有包裹的时候,领导会叫你开始干活(唤醒),没包裹的时候睡觉(nativePollOnce-挂起),让出cpu低成本运行,不会消耗太大的性能

简单一句话是:Android应用程序的主线程在进入消息循环过程前,会在内部创建一个Linux管道(Pipe),这个管道的作用是使得Android应用程序主线程在消息队列为空时可以进入空闲等待状态,并且使得当应用程序的消息队列有消息需要处理时唤醒应用程序的主线程。---这一题是需要从消息循环、消息发送和消息处理三个部分理解Android应用程序的消息处理机制了,
这里我对一些要点作一个总结:
A. Android应用程序的消息处理机制由消息循环、消息发送和消息处理三个部分组成的。
B. Android应用程序的主线程在进入消息循环过程前,会在内部创建一个Linux管道(Pipe),这个管道的作用是使得Android应用程序主线程在消息队列为空时可以进入空闲等待状态,并且使得当应用程序的消息队列有消息需要处理时唤醒应用程序的主线程。
C. Android应用程序的主线程进入空闲等待状态的方式实际上就是在管道的读端等待管道中有新的内容可读,具体来说就是是通过Linux系统的Epoll机制中的epoll_wait函数进行的。 D. 当往Android应用程序的消息队列中加入新的消息时,会同时往管道中的写端写入内容,通过这种方式就可以唤醒正在等待消息到来的应用程序主线程。 E. 当应用程序主线程在进入空闲等待前,会认为当前线程处理空闲状态,于是就会调用那些已经注册了的IdleHandler接口,使得应用程序有机会在空闲的时候处理一些事情。

非原创

准确的讲,阻塞也是阻塞了的,只不过是暂时的(UI线程阻塞,让出CPU),即没消息或消息还未到处理时间。等用户跟app交互后或者队列里的消息到时间了,UI线程会被再次唤醒,继续往下执行。
UI线程这个loop是专门设计成这样,阻塞等消息,消息来了唤醒并执行,如此反复(响应式的),不存在卡死问题。