Android:Handler消息机制(四)——为什么主线程不会因Looper.loop()里的死循环卡死

这个问题需要通过三方面来讲:

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

2.为什么主线程一直在死循环却不会占用大量CPU消耗?

3.那究竟是什么导致主线程卡死?

 

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

 

首先理解“线程进入死循环”这个问题, 就是在循环体内具有一段可执行的子程序,由于for(; ;)的调度导致这段子程序持续不断的在执行, 也就是持续的占用CPU资源, 从而导致当前线程的循环体外的子程序无法执行, 导致线程卡死的状态。

 

对于主线程既然是一段可执行的代码,也需要维持自己的执行周期,当可执行代码执行完成后,线程生命周期便该终止了,线程退出。而对于主线程绝不希望会被运行一段时间就自动退出,所以为了何保证程序能一直存活,就需要一个死循环来保证主线程一直处于运行状态,让程序处于运行阶段。 所以这个死循环可以看作是一种“假卡死”状态, 倘若这个循环体结束了意味着主线程该停止运行了, 那么app可以退出了。

 

那既然是死循环又如何去处理其他事务呢?会通过创建新线程的方式来完成,这里简单介绍一下,ActivityThread实际上并非线程,它不像HandlerThread类,ActivityThread并没有真正继承Thread类,只是往往运行在主线程,给人以线程的感觉,其实承载ActivityThread的主线程就是由Zygote fork而创建的进程,以下就是代码ActivityThread.main():

    public static void main(String[] args) {
        ....
        //初始化Looper,创建Looper和MessageQueue对象,用于处理主线程的消息
        Looper.prepareMainLooper();

        //创建ActivityThread对象,并绑定到AMS
        ActivityThread thread = new ActivityThread(); 

        //建立Binder通道,创建新线程,一般的应用程序都不是系统应用,因此设置为false,在这里面会绑定到AMS
        thread.attach(false);

        if (sMainThreadHandler == null) {
       		sMainThreadHandler = thread.getHandler();
    	}
    	if (false) {
            Looper.myLooper().setMessageLogging(new LogPrinter(Log.DEBUG, "ActivityThread"));
    	}

        Looper.loop(); //开启循环,消息循环运行
        throw new RuntimeException("Main thread loop unexpectedly exited");
    }

在创建ActivityThread的时候,会在这里面进行内部的两个重要变量的初始化,就是后续的mAppThread Binder实例以及一个H Handler实例。ActivityThread内的所有message就是来自其他线程send过来的, 例如binder线程的消息也是通过handler方式传递给ActivityThread的queue。

 

ActivityThread对应的Handler是一个内部类H继承于Handler,任何一个与生命周期相关的消息, 都会把target设置为H, 这样才能够确保在for(; ;)中取出的每一个message都能够通过message.target.sendMessage(msg)的方式通知H类的handleMessage()方法,里边通过handler消息机制来进行启动Activity、处理Activity生命周期等方法。Activity的生命周期函数都是在主线程的Looper.loop()循环中,被ActivityThread内部的Handler.handleMessage()调用,当收到不同Message时则采用相应措施,在H.handleMessage(msg)方法中,根据接收到不同的msg,进而调用不同的生命周期处理函数:

比如收到msg=H.LAUNCH_ACTIVITY,则调用ActivityThread.handleLaunchActivity()方法,最终会通过反射机制,创建Activity实例,然后再执行Activity.onCreate()等方法;

比如收到msg=H.PAUSE_ACTIVITY,则调用ActivityThread.handlePauseActivity()方法,最终会执行Activity.onPause()等方法。

 

而当main方法里调用了thread.attach(false),就便会创建一个Binder线程,,在内部进行 AMS(ActivityManagerSevice)和mAppThread Binder进程通讯者的绑定,即是AMS的attachApplication()的调用,在这个函数里面将会进行第一次跨进程通讯,AMS运行在系统进程,而我们的APP是另外一个进程。此时在AMS里面会调用上面绑定的mAppThread binder对象的bindApplication(...)方法,触发BIND_APPLICATION消息,该消息由 H 的sendMessage(...)来进行发送,此时消息会在 Looper 里面的 loop() 进行处理,最后会在 H.handleMessage(...) 处理, 然后进入到对应的函数里面进行 Context 的初始化,Application开始初始化,并且调用它的 onCreate,等其他操作。就是说该Binder线程通过Handler将Message发送给主线程。

 

接下来在AMS的attachApplicationLocked(...)函数里面,在触发了第一次进程通讯后,代码会继续运行,在里面会进行第二次进程通讯,首先是Activity的栈管理者之一ActivityStackSupervisor调用它的attachApplicationLocked(...),并在里面调用 realStartActivityLocked(...),就会正式发起第二次IPC,触发 LAUNCH_ACTIVITY 消息,还是通过 H 发送和处理,最后在处理处调用 performLaunchActivity(...),在这里面会根据一直传下来的信息,new 一个 Actiivity,然后会调用它的onCreate()和onStart()。

所以让ActivityThread进入”假卡死”状态, 一来不需要持续的占用CPU资源, 二来可以保证应用的不退出,ActivityThread的假卡死状态是最适合这种模式的交互方式的设备。

 

二、为什么主线程一直在死循环却不会占用大量CPU消耗?

 

到了这里,其实都会有个疑问,死循环会让主线程一直运行这样会不会特别消耗CPU资源呢?其实不然,如果当前主线程的MessageQueue没有消息时,,需要等待到下一条消息可用为止,也就是处于等待状态时,因为MessageQueue的阻塞机制,会让 主线程会处于阻塞状态, 这个过程涉及到MessageQueue 类中的两个方法: next()和 enqueueMessage(Message, long).

(1) next() :从队列中获取并返回下一个消息. 如果队列为空(无返回值), 则该方法将调用 native void nativePollOnce(long, int), 此操作为阻塞操作程序就会便阻塞在loop的queue.next()中的nativePollOnce()方里,该方法将一直阻塞直到添加新消息为止.

(2)enqueueMessage():将 Message 添加到队列时, 会调用该方法, 该方法不仅将消息插入队列, 而且还会调用native static void nativeWake(long).

 

如果当前线程阻塞的话就会释放掉CPU的占用, 进入阻塞状态.这个阻塞过程主要是通过nativePollOnce和nativeWake来完成的,nativePollOnce 和 nativeWake 的核心过程发生在 native 代码中.nativePollOnce 方法用于等待,而 nativeWake则是唤醒。通过 写入一个 IO 操作到描述符, epoll_wait 等待. 然后, 内核从等待状态中取出 epoll 等待线程, 并且该线程继续处理新消息.

 

nativePollOnce 这里就涉及到Linux pipe/epoll机制,该系统调用可以监视文件描述符中的 IO 事件. handler机制就是使用pipe来实现的。当主线程的MessageQueue没有消息时,就会阻塞在loop的queue.next()中的nativePollOnce()方法里,因为nativePollOnce() 在某个文件描述符上调用 epoll_wait,会让主线程在没有消息处理时就会阻塞在管道的读端。此时主线程会释放CPU资源进入休眠状态,直到下个消息到达或者有事务发生,nativeWake()会通过binder线程往主线程消息队列里添加消息,就会通过往pipe管道写端写入一个字节数据来唤醒主线程从管道读端返回,也就是说queue.next()会调用返回,

 nativePollOnce 大致等同于 Object.wait(), nativeWake 等同于 Object.notify(),只不过它们的实现完全不同: nativePollOnce使用 epoll, 而 Object.wait 使用 futex Linux 调用.

 

这里采用的epoll机制,是一种IO多路复用机制,可以同时监控多个描述符,当某个描述符就绪(读或写就绪),则立刻通知相应程序进行读或写操作,本质同步I/O,即读写是阻塞的。所以主线程的死循环并不是一直运行,在大多数时候都是处于休眠状态,并不会消耗大量CPU资源。因为nativePollOnce()只是表明所有消息的处理已完成, 线程正在等待下一个消息。

 

三、那究竟是什么导致主线程卡死?

 

ANR众所周知就是UI线程做了耗时操作而导致线程进入卡死状态,然后有人都会以为是不是UI线程进入Loop死循环后,就会出现卡死,但上面已经解释过了,loop死循环并不是主线程卡死的原因。

首先UI耗时导致卡死前提是要有输入事件,当一个MessageQueue不是空的,并且Looper正常轮询,线程并没有阻塞,但是该事件执行时间过长(一般5秒),而且与此期间其他的事件(按键按下,屏幕点击等等)都没办法处理,在执行时间过长后在触摸屏幕,然后就会触发ANR异常了。

所以运行死循环并不会导致ANR,原因就是因为耗时操作本身并不会导致主线程卡死, 导致主线程卡死的真正原因是耗时操作之后的触屏操作, 没有在规定的时间内被分发和处理。所以真正会卡死主线程的操作是在进行回调方法onCreate/onStart/onResume等操作时间过长,就有可能发生ANR,而就像上面说的looper.loop()本身不会导致应用卡死。

ANR机制的目的还有一个是为了监测主线程的耗时操作,譬如密集CPU运算、大量IO、复杂界面布局等,因为这些都会降低应用程序的响应能力。所以从理念上也能理解,loop死循环只是简单地处理轻量的消息操作,和ANR并没有关系。

 

猜你喜欢

转载自blog.csdn.net/ZytheMoon/article/details/105850489
今日推荐