登录

去注册

登录

注册

去登录

注册

每日一问 Looper.loop为什么不会阻塞掉UI线程?

xiaoyang   2019-07-14   收藏

今天问一个老生常谈的问题,很期待大家的回答。

9

要完全彻底理解这个问题,需要准备以下4方面的知识:Process/Thread,Android Binder IPC,Handler/Looper/MessageQueue消息机制,Linux pipe/epoll机制。

总结一下楼主主要有3个疑惑:

1.Android中为什么主线程不会因为Looper.loop()里的死循环卡死?

2.没看见哪里有相关代码为这个死循环准备了一个新线程去运转?

3.Activity的生命周期这些方法这些都是在主线程里执行的吧,那这些生命周期方法是怎么实现在死循环体外能够执行起来的?


(1) Android中为什么主线程不会因为Looper.loop()里的死循环卡死?

这里涉及线程,先说说说进程/线程,进程:每个app运行时前首先创建一个进程,该进程是由Zygote fork出来的,用于承载App上运行的各种Activity/Service等组件。进程对于上层应用来说是完全透明的,这也是google有意为之,让App程序都是运行在Android Runtime。大多数情况一个App就运行在一个进程中,除非在AndroidManifest.xml中配置Android:process属性,或通过native代码fork进程。

线程:线程对应用来说非常常见,比如每次new Thread().start都会创建一个新的线程。该线程与App所在进程之间资源共享,从Linux角度来说进程与线程除了是否共享资源外,并没有本质的区别,都是一个task_struct结构体,在CPU看来进程或线程无非就是一段可执行的代码,CPU采用CFS调度算法,保证每个task都尽可能公平的享有CPU时间片

有了这么准备,再说说死循环问题:

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

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


2) 没看见哪里有相关代码为这个死循环准备了一个新线程去运转?

事实上,会在进入死循环之前便创建了新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"); 
  }

thread.attach(false);便会创建一个Binder线程(具体是指ApplicationThread,Binder的服务端,用于接收系统服务AMS发送来的事件),该Binder线程通过Handler将Message发送给主线程,具体过程可查看 startService流程分析,这里不展开说,简单说Binder用于进程间通信,采用C/S架构。关于binder感兴趣的朋友,可查看我回答的另一个知乎问题:
为什么Android要采用Binder作为IPC机制? - Gityuan的回答

另外,ActivityThread实际上并非线程,不像HandlerThread类,ActivityThread并没有真正继承Thread类,只是往往运行在主线程,该人以线程的感觉,其实承载ActivityThread的主线程就是由Zygote fork而创建的进程。

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


(3) Activity的生命周期是怎么实现在死循环体外能够执行起来的?

ActivityThread的内部类H继承于Handler,通过handler消息机制,简单说Handler机制用于同一个进程的线程间通信。

Activity的生命周期都是依靠主线程的Looper.loop,当收到不同Message时则采用相应措施:
在H.handleMessage(msg)方法中,根据接收到不同的msg,执行相应的生命周期。

比如收到msg=H.LAUNCH_ACTIVITY,则调用ActivityThread.handleLaunchActivity()方法,最终会通过反射机制,创建Activity实例,然后再执行Activity.onCreate()等方法;
再比如收到msg=H.PAUSE_ACTIVITY,则调用ActivityThread.handlePauseActivity()方法,最终会执行Activity.onPause()等方法。 上述过程,我只挑核心逻辑讲,真正该过程远比这复杂。

主线程的消息又是哪来的呢?当然是App进程中的其他线程通过Handler发送给主线程,请看接下来的内容:

最后,从进程与线程间通信的角度,通过一张图加深大家对App运行过程的理解:



system_server进程是系统进程,java framework框架的核心载体,里面运行了大量的系统服务,比如这里提供ApplicationThreadProxy(简称ATP),ActivityManagerService(简称AMS),这个两个服务都运行在system_server进程的不同线程中,由于ATP和AMS都是基于IBinder接口,都是binder线程,binder线程的创建与销毁都是由binder驱动来决定的。

App进程则是我们常说的应用程序,主线程主要负责Activity/Service等组件的生命周期以及UI相关操作都运行在这个线程; 另外,每个App进程中至少会有两个binder线程 ApplicationThread(简称AT)和ActivityManagerProxy(简称AMP),除了图中画的线程,其中还有很多线程,比如signal catcher线程等,这里就不一一列举。

Binder用于不同进程之间通信,由一个进程的Binder客户端向另一个进程的服务端发送事务,比如图中线程2向线程4发送事务;而handler用于同一个进程中不同线程的通信,比如图中线程4向主线程发送消息。

结合图说说Activity生命周期,比如暂停Activity,流程如下:
  1. 线程1的AMS中调用线程2的ATP;(由于同一个进程的线程间资源共享,可以相互直接调用,但需要注意多线程并发问题)
  2. 线程2通过binder传输到App进程的线程4;
  3. 线程4通过handler消息机制,将暂停Activity的消息发送给主线程;
  4. 主线程在looper.loop()中循环遍历消息,当收到暂停Activity的消息时,便将消息分发给ActivityThread.H.handleMessage()方法,再经过方法的调用,最后便会调用到Activity.onPause(),当onPause()处理完后,继续循环loop下去。
回复
蔡徐坤打篮球 : @Forever 

我不是gityuan,也不是大神,只是在一些android常见的问题进行深入的理解,力求知其然知其所以然,至于的你问题参考问题3中的“结合图说说Activity生命周期,比如暂停Activity”,你  ...查看更多

2019-07-14 回复
Forever : @蔡徐坤打篮球 

大神,我想问一下,是不是谁主动传递信息,谁就是binder的服务端?另外一个接受信息的就是binder的Client端?还有大神你这个名字有点皮啊,你是不是gityuan本尊啊?  ...查看更多

2019-07-13 回复
蔡徐坤打篮球 : @小学生,学习中 

这里有你想要的一切 http://gityuan.com/android/

2019-07-12 回复
小学生,学习中 : @蔡徐坤打篮球 

大神,源码这个庞然大物,有点无从下嘴啊,有什么引导性的意见吗

2019-07-12 回复
蔡徐坤打篮球 : @pigmandy 

sdk 28 thread.attch()代码: final IActivityManager mgr = ActivityManager.getService(); try { mgr.at  ...查看更多

2019-07-12 回复
pigmandy : @蔡徐坤打篮球 

//建立Binder通道 (创建新线程) thread.attach(false); 这里有创建新线程吗,好像没找到什么关键代码

2019-07-12 回复
陈小缘 : @蔡徐坤打篮球 

赞!

2019-07-12 回复
蔡徐坤打篮球 : @蔡徐坤打篮球 

这是我多年前记下gityaun大神的回答

2019-07-12 回复
4

本周3/3,开心又坚持一周。

回复
1

从源码loop方法可以看到,说明方法中有判断结束loop循环

/**
* Run the message queue in this thread. Be sure to call
* {@link #quit()} to end the loop.
*/  

因为Looper.loop中for死循环是不断在MessageQueue中取得消息,发送给绑定的handler处理

try {
msg.target.dispatchMessage(msg);
dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;
}

如果没有消息就返回ActivityThread.main()方法中,一句话说就是有消息就处理,没有消息就返回等待

Message msg = queue.next(); // might block
if (msg == null) {
// No message indicates that the message queue is quitting.
return;
}


回复
0

哈哈,自身太差了回答的都比大家晚,这是一道陷阱题, Looper.loop 会阻塞 UI 线程, Looper.loop 开启死循环后如果 MQ 中有消息就处理消息,没有消息就进入阻塞同时释放 CPU 资源,当 MQ 中有消息时,依靠 Liunx 的 pipe/epoll 机制唤醒主线程处理 MQ 中的消息。

回复
0

主线程中的所有操作,都是通过Handler发送到主线程的消息队列中执行的,Looper.loop当没有消息的时候阻塞,是为了避免线程中代码运行完了就结束了

回复
0

 之前在学习Hanlder源码的时候,刚好涉及到 Looper.loop 方面的知识,这里进行一下回答


首先,在ActivityThread.main 方法中,可以找到Looper相关的初始化代码,在这段代码里面做了两件事, 1、初始化当前线程的Looper
2、开启循环

```
public static void main(String[] args) {
//省略掉部分不相关代码
//..........
//prepareMainLooper 方法在当前线程初始化了一个消息队列不允许退出Looper
Looper.prepareMainLooper();
//..........
Looper.loop();

throw new RuntimeException("Main thread loop unexpectedly exited");
}

```
进入loop方法
```
public static void loop() {
final Looper me = myLooper();
if (me == null) {
throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
}
//1、获取到消息队列
final MessageQueue queue = me.mQueue;

//省略部分不相关的代码
//..................

//开启死循环
for (;;) {
//2、拿到队列中的消息
Message msg = queue.next(); // might block
if (msg == null) {
// No message indicates that the message queue is quitting.
return;
}

//省略部分不相关的代码
//..................
try {
//3、执行队列中的消息
msg.target.dispatchMessage(msg);
dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;
} finally {
if (traceTag != 0) {
Trace.traceEnd(traceTag);
}
}
//省略部分不相关的代码
//..................
msg.recycleUnchecked();
}
```
loop()方法中,代码非常简单,去除掉一些无用的日志打印和不相关的代码,剩余的就非常简单了,分三步走
1、获取到looper中的 MessageQueue
2、开启一个死循环,从MessageQueue 中不断的取出消息
3、执行取出来的消息 msg.target.dispatchMessage(msg);(顺便说一下,Handler的handleMessage()方法就是在这一步执行的,有兴趣的可以自己看看,这里就不细说了)

在第二步里面,会发生阻塞,如果消息队列里面没有消息了,会无限制的阻塞下去,主线程休眠,释放CPU资源,直到有消息进入消息队列,唤醒线程。从这里就可以看出来,loop死循环本身大部分时间都处于休眠状态,并不会占用太多的资源,真正会造成线程阻塞的反而是在第三步里的 msg.target.dispatchMessage(msg)方法,因此如果在生命周期或者handler的Handler的handleMessage执行耗时操作的话,才会真正的阻塞UI线程;

到这里,已经从java层解释了Looper.loop为什么不会阻塞掉UI线程;最后,再看一下queue.next()方法,毕竟代码留个尾巴不看实在太憋屈了

```
Message next() {

// mPtr保存了NativeMessageQueue的指针,调用nativePollOnce进行等待
final long ptr = mPtr;
if (ptr == 0) {
return null;
}
int pendingIdleHandlerCount = -1; // -1 only during first iteration
int nextPollTimeoutMillis = 0;
for (;;) {
if (nextPollTimeoutMillis != 0) {
Binder.flushPendingCommands();
}
//nativePollOnce 两个参数,nextPollTimeoutMillis 表示的是等待时间,-1的时候表示无限制等待
//在这里可以看出,如果消息队列里面没有消息,就会一直等待,直到队列里面加入新的消息,唤醒线程
nativePollOnce(ptr, nextPollTimeoutMillis);

//线程被唤醒后,开始从队列中取出消息
synchronized (this) {
// Try to retrieve the next message. Return if found.
final long now = SystemClock.uptimeMillis();
Message prevMsg = null;
Message msg = mMessages;

if (msg != null) {
if (now < msg.when) {
//如果下一条消息执行时间还未到,则计算出剩余需要阻塞的时间,给nativePollOnce方法,让他阻塞指定的时间后,继续执行
nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
} else {
// 在这里,直接取出方法
mBlocked = false;
if (prevMsg != null) {
prevMsg.next = msg.next;
} else {
mMessages = msg.next;
}
msg.next = null;
if (DEBUG) Log.v(TAG, "Returning message: " + msg);
msg.markInUse();
return msg;
}
} else {
// 队列里面没有消息了,再次无限期阻塞
nextPollTimeoutMillis = -1;
}
//省略部分不相关的代码
//..................
// Process the quit message now that all pending messages have been handled.
if (mQuitting) {
dispose();
return null;
}
//省略部分不相关的代码
//..................

}
}
```
从上面代码可以看出来,在调用next方法的时候,如果有当前消息可以被立刻执行,就会直接返回,如果消息需要延迟执行,则会接着阻塞一段时间,到消息可以被执行的时候再继续线程执行消息,如果队列里面没消息了,就会无限期的阻塞下去,直到新的消息进入队列唤醒线程;

至于什么时候唤醒阻塞,就需要看看enqueueMessage(Message msg, long when) 方法了,在这个方法里面,会将新的消息放到消息队列里面,并且判断如果此时线程处于阻塞状态,就会调用nativeWake()方法唤醒线程,继续执行next()方法,取出队列中的消息

最后总结一下:

loop()开启死循环后,会命令MessageQueue通过 next()方法 取出之前储存的消息,如果有立刻被拿出来执行msg.target.dispatchMessage(msg);如果此时MessageQueue中已经没有消息了(大部分时候都没有),MessageQueue就会无限期的阻塞下去nativePollOnce(ptr, nextPollTimeoutMillis),释放cpu资源,这时候并不会造成UI线程卡顿,直到有新的消息存入队列enqueueMessage(Message msg, long when),唤醒之前阻塞的线程 nativeWake(mPtr),继续执行next()方法;


我只是从java层对问题进行了解答,时间仓促,可能有不完善的地方,对于有错误的地方,欢迎指正,共同学习进步



回复
桑小年 : @鸿洋 

第一次回答没注意,下次就知道了

2019-07-15 回复
桑小年 : @陈小缘 

ok,懂了,谢啦,这次就不编辑了,下次回答就知道了

2019-07-15 回复
鸿洋 : @桑小年 

暂时不能重新编辑呢,会尽快替换为markdown的编辑器。

2019-07-14 回复
陈小缘 : @桑小年 

只能先复制到记事本,然后慢慢复制进输入框了,代码部分就点开那个 <_ 的图标来插入

2019-07-12 回复
桑小年 : @桑小年 

没有注意到不能用MarkDown,排版完全乱掉了,怎么重新编辑??

2019-07-12 回复

删除留言

确认删除留言,会导致相关评论丢失?

取消 确定