文章目录

问题

app程序入口中为主线程准备好了消息队列:

Looper.loop 为什么不会阻塞掉 UI 线程 ?_生命周期


而根据 ​​Looper.loop()​​ 源码可知里面是一个死循环在遍历消息队列取消息

Looper.loop 为什么不会阻塞掉 UI 线程 ?_Android 进阶_02

而且并也没看见哪里有相关代码为这个死循环准备了一个新线程去运转,但是主线程却并不会因为 ​​Looper.loop()​​中的这个死循环卡死,为什么呢?

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

概述

要完全彻底理解这个问题,需要准备以下4方面的知识:

  • Process/Thread
  • Android Binder IPC
  • Handler/Looper/MessageQueue消息机制
  • Linux pipe/epoll机制。

首先解答以下三个问题:

  1. Android中为什么主线程不会因为​​Looper.loop()​​ 里的死循环卡死?
  2. 没看见哪里有相关代码为这个死循环准备了一个新线程去运转?
  3. ​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 的小部分:

public void handleMessage(Message msg) {
if (DEBUG_MESSAGES) Slog.v(TAG, ">>> handling: " + codeToString(msg.what));
switch (msg.what) {
case LAUNCH_ACTIVITY: {
Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityStart");
final ActivityClientRecord r = (ActivityClientRecord) msg.obj;

r.packageInfo = getPackageInfoNoCheck(
r.activityInfo.applicationInfo, r.compatInfo);
handleLaunchActivity(r, null);
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
} break;
case RELAUNCH_ACTIVITY: {
Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityRestart");
ActivityClientRecord r = (ActivityClientRecord)msg.obj;
handleRelaunchActivity(r);
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
} break;
case PAUSE_ACTIVITY:
Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityPause");
handlePauseActivity((IBinder)msg.obj, false, (msg.arg1&1) != 0, msg.arg2,
(msg.arg1&2) != 0);
maybeSnapshot();
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
break;
case PAUSE_ACTIVITY_FINISHING:
Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityPause");
handlePauseActivity((IBinder)msg.obj, true, (msg.arg1&1) != 0, msg.arg2,
(msg.arg1&1) != 0);
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
break;
case STOP_ACTIVITY_SHOW:
Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityStop");
handleStopActivity((IBinder)msg.obj, true, msg.arg2);
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
break;
case STOP_ACTIVITY_HIDE:
Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityStop");
handleStopActivity((IBinder)msg.obj, false, msg.arg2);
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
break;
case SHOW_WINDOW:
Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityShowWindow");
handleWindowVisibility((IBinder)msg.obj, true);
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
break;
case HIDE_WINDOW:
Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityHideWindow");
handleWindowVisibility((IBinder)msg.obj, false);
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
break;
case RESUME_ACTIVITY:
Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityResume");
handleResumeActivity((IBinder) msg.obj, true, msg.arg1 != 0, true);
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
break;
case SEND_RESULT:
Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityDeliverResult");
handleSendResult((ResultData)msg.obj);
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
break;

...........
}

看完这 ​​Handler​​​ 里处理消息的内容应该明白了吧, ​​Activity​​​ 的生命周期都有对应的 ​​case​​​ 条件了,​​ActivityThread​​​ 有个 ​​getHandler​​​ 方法,得到这个 ​​handler​​​就可以发送消息,然后 ​​loop​​​ 里就分发消息,然后就发给 ​​handler​​​, 然后就执行到 ​​H(Handler )​​​里的对应代码。所以这些代码就不会卡死~,有消息过来就能执行。举个例子,在 ​​ActivityThread​​​ 里的内部类 ​​ApplicationThread​​​ 中就有很多 ​​sendMessage​​ 的方法:

public final void schedulePauseActivity(IBinder token, boolean finished,
boolean userLeaving, int configChanges, boolean dontReport) {
sendMessage(
finished ? H.PAUSE_ACTIVITY_FINISHING : H.PAUSE_ACTIVITY,
token,
(userLeaving ? 1 : 0) | (dontReport ? 2 : 0),
configChanges);
}

public final void scheduleStopActivity(IBinder token, boolean showWindow,
int configChanges) {
sendMessage(
showWindow ? H.STOP_ACTIVITY_SHOW : H.STOP_ACTIVITY_HIDE,
token, 0, configChanges);
}

public final void scheduleWindowVisibility(IBinder token, boolean showWindow) {
sendMessage(
showWindow ? H.SHOW_WINDOW : H.HIDE_WINDOW,
token);
}

public final void scheduleSleeping(IBinder token, boolean sleeping) {
sendMessage(H.SLEEPING, token, sleeping ? 1 : 0);
}

public final void scheduleResumeActivity(IBinder token, int processState,
boolean isForward, Bundle resumeArgs) {
updateProcessState(processState, false);
sendMessage(H.RESUME_ACTIVITY, token, isForward ? 1 : 0);
}

​ActivityThread​​​ 的 ​​main​​ 方法主要就是做消息循环,一旦退出消息循环,那么你的程序也就可以退出了。

从消息队列中取消息可能会阻塞,取到消息会做出相应的处理。如果某个消息处理时间过长,就可能会影响 ​​UI​​ 线程的刷新速率,造成卡顿的现象。

死循环问题

线程既然是一段可执行的代码,当可执行代码执行完成后,线程生命周期便该终止了,线程退出。而对于主线程,我们是绝不希望会被运行一段时间,自己就退出,那么如何保证能一直存活呢?简单做法就是可执行代码是能一直执行下去的,死循环便能保证不会被退出

例如,​​binder​​​ 线程也是采用死循环的方法,通过循环方式与 ​​Binder​​ 驱动进行读写操作,当然并非简单地死循环,无消息时会休眠。但这里可能又引发了另一个问题,既然是死循环又如何去处理其他事务呢?通过创建新线程的方式

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

线程的阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。

主线程Looper从消息队列读取消息,当读完所有消息时,主线程阻塞。子线程往消息队列发送消息,并且往管道文件写数据,主线程即被唤醒,从管道文件读取数据,主线程被唤醒只是为了读取消息,当消息读取完毕,再次睡眠。因此loop的循环并不会对CPU性能有过多的消耗。

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的回答​

sdk 28 ​​thread.attch()​​ 代码:

final IActivityManager mgr = ActivityManager.getService(); 
try {
mgr.attachApplication(mAppThread, startSeq);
} catch (RemoteException ex) {
throw ex.rethrowFromSystemServer();
}

上面代码完成了 ​​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. 总结

  1. 每个​​App​​ 运行时前首先创建一个进程,该进程是由 ​​Zygote​​ ​​fork​​ 出来的,用于承载各种 ​​Activity/Service​​ 等组件,大多数情况一个 ​​App​​ 就运行在一个进程中
  2. 进程中会起一些线程,比如所谓的主线程​​ActivityThread​​,线程是一段可执行的代码。当可执行代码执行完成后,线程生命周期便该终止了,线程退出。而对于主线程,我们是绝不希望会被运行一段时间,自己就退出,那么如何保证能一直存活呢?简单做法就是可执行代码是能一直执行下去的,死循环便能保证不会被退出
  3. ​ActivityThread​​​ 是应用程序的入口,这里你可以看到写Java程序时司空见惯的 ​​main​​ 方法,而 ​​main​​ 方法正是整个Java程序的入口。​​ActivityThread​​ 的 ​​main​​ 方法主要就是做消息循环,一旦退出消息循环,那么你的程序也就可以退出了。
  4. Android是事件驱动的,在Loop.loop()中不断接收事件、处理事件,而Activity的生命周期都依靠于主线程的Loop.loop()来调度,所以可想而知它的存活周期和Activity也是一致的。当没有事件需要处理时,主线程就会阻塞;当子线程往消息队列发送消息,并且往管道文件写数据时,主线程就被唤醒。

线程的阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。

  1. 主线程中的​​Looper​​ 从消息队列读取消息,当读完所有消息时,主线程阻塞。子线程往消息队列发送消息,并且往管道文件写数据,主线程即被唤醒,从管道文件读取数据,主线程被唤醒只是为了读取消息,当消息读取完毕,再次睡眠。因此​​loop​​​ 的循环并不会对​​CPU​​ 性能有过多的消耗。

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

​Looer.loop()​​​ 方法可能会引起主线程的阻塞,但只要它的消息循环没有被阻塞,能一直处理事件就不会产生 ​​ANR​​ 异常。

4.1 和 ANR 的区别

msg -> 5s -> ANRmsg

ANR:
5秒内没有响应输入事件,比如按键、屏幕触摸
10秒内没有处理广播
本质:消息队列中其他消息耗时,按键或广播消息没有及时处理

ANR 的根本原因不是线程在睡眠,而是消息队列被其他耗时消息阻塞,导致按键或广播消息没有及时处理

参考链接