文章目录
- 进程
- 线程
- ActivityThread
- 死循环问题
- 参考链接
问题
app程序入口中为主线程准备好了消息队列:
而根据 Looper.loop()
源码可知里面是一个死循环在遍历消息队列取消息
而且并也没看见哪里有相关代码为这个死循环准备了一个新线程去运转,但是主线程却并不会因为 Looper.loop()
中的这个死循环卡死,为什么呢?
举个例子,像 Activity
的生命周期这些方法这些都是在主线程里执行的吧,那这些生命周期方法是怎么实现在死循环体外能够执行起来的?
概述
要完全彻底理解这个问题,需要准备以下4方面的知识:
- Process/Thread
- Android Binder IPC
- Handler/Looper/MessageQueue消息机制
- Linux pipe/epoll机制。
首先解答以下三个问题:
- Android中为什么主线程不会因为
Looper.loop()
里的死循环卡死? - 没看见哪里有相关代码为这个死循环准备了一个新线程去运转?
-
Activity
的生命周期这些方法这些都是在主线程里执行的吧,那这些生命周期方法是怎么实现在死循环体外能够执行起来的?
1. Android中为什么主线程不会因为 Looper.loop() 里的死循环卡死?
这里涉及线程,先说说说 Android 中每个应用所对应的 进程/线程:
进程
每个app运行时前首先创建一个进程,该进程是由
Zygote
fork
出来的,用于承载各种 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
时间片。
ActivityThread
ActivityThread
是应用程序的入口,这里你可以看到写Java程序时司空见惯的 main
方法,而 main
方法正是整个Java程序的入口。ActivityThread
的 main
方法主要就是做消息循环,一旦退出消息循环,那么你的程序也就可以退出了。
Android是事件驱动的,在Loop.loop()中不断接收事件、处理事件,而Activity的生命周期都依靠于主线程的Loop.loop()来调度,所以可想而知它的存活周期和Activity也是一致的。当没有事件需要处理时,主线程就会阻塞;当子线程往消息队列发送消息,并且往管道文件写数据时,主线程就被唤醒。
ActivityThread
并不是一个 Thread
,就只是一个 final
类而已。我们常说的主线程就是从这个类的 main
方法开始,main
方法很简短,一眼就能看全,我们看到里面有 Looper
了,那么接下来就找找 ActivityThread
对应的 Handler
贴出 handleMessage 的小部分:
看完这 Handler
里处理消息的内容应该明白了吧, Activity
的生命周期都有对应的 case
条件了,ActivityThread
有个 getHandler
方法,得到这个 handler
就可以发送消息,然后 loop
里就分发消息,然后就发给 handler
, 然后就执行到 H(Handler )
里的对应代码。所以这些代码就不会卡死~,有消息过来就能执行。举个例子,在 ActivityThread
里的内部类 ApplicationThread
中就有很多 sendMessage
的方法:
ActivityThread
的 main
方法主要就是做消息循环,一旦退出消息循环,那么你的程序也就可以退出了。
从消息队列中取消息可能会阻塞,取到消息会做出相应的处理。如果某个消息处理时间过长,就可能会影响 UI
线程的刷新速率,造成卡顿的现象。
死循环问题
线程既然是一段可执行的代码,当可执行代码执行完成后,线程生命周期便该终止了,线程退出。而对于主线程,我们是绝不希望会被运行一段时间,自己就退出,那么如何保证能一直存活呢?简单做法就是可执行代码是能一直执行下去的,死循环便能保证不会被退出
例如,binder
线程也是采用死循环的方法,通过循环方式与 Binder
驱动进行读写操作,当然并非简单地死循环,无消息时会休眠。但这里可能又引发了另一个问题,既然是死循环又如何去处理其他事务呢?通过创建新线程的方式。
真正会卡死主线程的操作是在回调方法 onCreate/onStart/onResume
等操作时间过长,会导致掉帧,甚至发生 ANR
,looper.loop
本身不会导致应用卡死。
线程的阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。
主线程Looper从消息队列读取消息,当读完所有消息时,主线程阻塞。子线程往消息队列发送消息,并且往管道文件写数据,主线程即被唤醒,从管道文件读取数据,主线程被唤醒只是为了读取消息,当消息读取完毕,再次睡眠。因此loop的循环并不会对CPU性能有过多的消耗。
2. 没看见哪里有相关代码为这个死循环准备了一个新线程去运转?
事实上,会在进入死循环之前便创建了新 binder
线程,在代码 ActivityThread.main()
中:
thread.attach(false)
便会创建一个 Binder
线程(具体是指 ApplicationThread
,Binder
的服务端,用于接收系统服务 AMS
发送来的事件),该 Binder
线程通过 Handler
将 Message
发送给主线程,具体过程可查看 startService 流程分析,这里不展开说,简单说 Binder
用于进程间通信,采用 C/S
架构。关于 binder
感兴趣的朋友,可查看我回答的另一个知乎问题:为什么Android要采用Binder作为IPC机制? - Gityuan的回答
sdk 28 thread.attch()
代码:
上面代码完成了 AMS
与App进程通讯的绑定,在 system_server
就创建了 App
对应的 Binder
进程,具体参看上面问题3的图片和 AMS
源码
另外,ActivityThread 实际上并非线程,不像 HandlerThread
类,ActivityThread
并没有真正继承Thread类,只是往往运行在主线程,该人以线程的感觉,其实承载 ActivityThread
的主线程就是由 Zygote
fork
而创建的进程。
主线程的死循环一直运行是不是特别消耗 CPU
资源呢? 其实不然,这里就涉及到 Linux
pipe/epoll
机制,简单说就是在主线程的 MessageQueue
没有消息时,便阻塞在 loop
的 queue.next()
中的nativePollOnce()
方法里,此时主线程会释放 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运行过程的理解:
4. 总结
- 每个
App
运行时前首先创建一个进程,该进程是由 Zygote
fork
出来的,用于承载各种 Activity/Service
等组件,大多数情况一个 App
就运行在一个进程中 - 进程中会起一些线程,比如所谓的主线程
ActivityThread
,线程是一段可执行的代码。当可执行代码执行完成后,线程生命周期便该终止了,线程退出。而对于主线程,我们是绝不希望会被运行一段时间,自己就退出,那么如何保证能一直存活呢?简单做法就是可执行代码是能一直执行下去的,死循环便能保证不会被退出 -
ActivityThread
是应用程序的入口,这里你可以看到写Java程序时司空见惯的 main
方法,而 main
方法正是整个Java程序的入口。ActivityThread
的 main
方法主要就是做消息循环,一旦退出消息循环,那么你的程序也就可以退出了。 - Android是事件驱动的,在Loop.loop()中不断接收事件、处理事件,而Activity的生命周期都依靠于主线程的Loop.loop()来调度,所以可想而知它的存活周期和Activity也是一致的。当没有事件需要处理时,主线程就会阻塞;当子线程往消息队列发送消息,并且往管道文件写数据时,主线程就被唤醒。
线程的阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。
- 主线程中的
Looper
从消息队列读取消息,当读完所有消息时,主线程阻塞。子线程往消息队列发送消息,并且往管道文件写数据,主线程即被唤醒,从管道文件读取数据,主线程被唤醒只是为了读取消息,当消息读取完毕,再次睡眠。因此loop
的循环并不会对CPU
性能有过多的消耗。
真正会卡死主线程的操作是在回调方法 onCreate/onStart/onResume
等操作时间过长,会导致掉帧,甚至发生ANR
,looper.loop
本身不会导致应用卡死。
Looer.loop()
方法可能会引起主线程的阻塞,但只要它的消息循环没有被阻塞,能一直处理事件就不会产生 ANR
异常。
4.1 和 ANR 的区别
msg -> 5s -> ANRmsg
ANR:
5秒内没有响应输入事件,比如按键、屏幕触摸
10秒内没有处理广播
本质:消息队列中其他消息耗时,按键或广播消息没有及时处理
ANR 的根本原因不是线程在睡眠,而是消息队列被其他耗时消息阻塞,导致按键或广播消息没有及时处理
参考链接
- 为什么Looper.loop()死循环不会导致ANR
- Looper中的死循环
- Looper.loop为什么不会阻塞掉UI线程?来,我们从源码里面找到答案
- 知乎 – Android中为什么主线程不会因为Looper.loop()里的死循环卡死?
- 腾讯Android面试:Handler中有Loop死循环,为什么没有阻塞主线程,原理是什么