Handler -- 为什么Looper的loop()不会导致主线程卡死

结论

  1. 初始化后,所有主线程做的事情都是在looper.loop()中完成的,因为主线程不做其他事,所以不会卡死
  2. 基于linux的epoll模型,当主线程没有message消费时,会进入睡眠状态(简单理解),等到有新的可消费的Message时,再转为活跃状态处理Message(类似一个事件回调)。主线程在睡眠状态会让出CPU,并不是一直不停在执行循环。

如有兴趣了解下epoll机制可以看看这篇文章:

NIO相关基础篇

产生此疑问的前提

怎么执行到looper.loop()

首先得分析下ActivityThread的main()方法,我们可以简单认为此方法为一个APP启动的入口。(代码只展示关键步骤)

public static void main(String[] args) {

    Looper.prepareMainLooper();

    ActivityThread thread = new ActivityThread();
    thread.attach(false, startSeq);

    if (sMainThreadHandler == null) {
        sMainThreadHandler = thread.getHandler();
    }

    Looper.loop();

    throw new RuntimeException("Main thread loop unexpectedly exited");
}
  1. 首先Looper.prepareMainLooper()方法生成属于当前线程(主线程)的Looper对象。
  2. sMainThreadHandler = thread.getHandler();获得主线程内置的Handler对象。此Handler对象为ActivityThread的内部类 H 。可以简单看看 H 的源码,会发现 H 能够处理绝大多数的主线程事件,包括了Activity的启动、Service的绑定等等。。
  3. 接下来就是Looper.loop();

进入Looper.loop()

public static void loop() {
    final Looper me = myLooper();

    final MessageQueue queue = me.mQueue;

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

        try {
            msg.target.dispatchMessage(msg);
        } catch (Exception exception) {
            throw exception;
        }

        msg.recycleUnchecked();
    }
}

可以看到第6行 for(;;)是死循环,参考前边的结论2,queue·next()是导致主线程让出CPU的原因,queue.next()利用了epoll机制,闲则睡眠,忙则唤醒。

为什么觉得会卡死

下边看一段典型的死循环 ,结合这个例子来简单探讨下大伙可能觉得会导致程序卡死的原因:

public class A{

    public static void main(String[] args){
        while(true){
            doSomething();
        }
        doSomethingElse();
    }
    
    public static void doSomething(){
        // ...
    }

    public static void doSomethingElse(){
        // ...
    }
}
  1. 上边代码会不停的循环执行doSomething(),会导致doSomethingElse()执行不到(编译甚至都无法通过),并且由于没有停顿,死循环会占用大量的CPU资源(结合任务管理器可以看到程序的CPU急剧升高到接近100%),此时再进行其他操作就会卡顿,因为CPU时间都分配给了死循环的进程。
  2. 另外结合我们的共识:android 的 UI操作都是在主线程的。既然主线程死循环了,那他就只能一直执行死循环内部的逻辑,好像就无暇处理UI更新了(编不下去了。。)。

对于问题1

首先可以考虑如何解决上边的死循环程序一直占用CPU的问题,一种简单的做法是让线程sleep()

while(true){
    doSomething();
    sleep(50);
}
  • sleep()就会使线程休眠,从而会让出CPU,这样CPU就有时间去处理其他事情了。
  • 但是sleep(long time)不太智能,只能休眠固定的时间。结合android主线程要负责更新UI的职责,我们想要的效果应该是当不需要更新UI的时候主线程就睡眠,以让出CPU,一旦有UI更新的需求时就停止睡眠执行UI更新操作。于是android就引入了handler-looper机制。
  • android将每个UI更新的需求包装成一个消息,放入MessageQueue,Looper.loop()就在不断的尝试从MessageQueue中拿消息,以更新UI。然后我们再结合前边说的queue.next()和epoll机制,正是一种闲则睡眠,忙则唤醒的模型。

对于问题2

问题1算是基本解答了问题2,因为更新UI的操作都在死循环内部,没有其他操作,Looper.loop()本身也就不会出现造成UI卡死。

ANR问题

ANR即Application Not Responding,顾名思义就是应用程序无响应。一般都是由于在主线程执行了耗时操作,导致新到来的UI事件的来不及处理,系统检测到这种情况就会ANR。
这种问题就是应该通过编程来避免。核心要点:不要让主线程干耗时的工作。

扩展问题

  1. 正常情况下APP播放动画都是在主线程,但是主线程有无法执行耗时任务的限制,动画不就是耗时任务吗,另外一般播放动画时,app仍然能响应触摸事件,该如何解释?
    解释:动画虽然不是 Handler-Looper 机制,基于Vsync机制,两者都是类似的会有隔一段时间执行以一次循环的特点。最重要的在于明白动画的播放并不是连续的,将动画的播放与屏幕的刷新联系起来。如果屏幕 1s 刷新60次,则1s有60次播放动画的机会(播放动画的一部分),也就是每16.6ms执行一次。但是每一帧中处理动画播放的逻辑一般都是不会超过16.6ms(不然就掉帧了),所以CPU还是有空闲时间来处理其他输入事件的。

  2. 在Activity的onCreate()方法中执行耗时操作会导致ANR,我知道了onCreate()是在主线程中执行的,但是前边分析的主线程只执行Looper.loop()死循环,如何联系起来。
    解释:需要将Activity的启动理解为向MessageQueue添加一个消息,onCreate()方法是在Activity启动时调用 (ApplicationThread.scheduleLaunchActivity()内部,最后通过sendMessage(H.LAUNCH_ACTIVITY,r)来启动acitivty,但新版本sdk源码不同)。所有onCreate()也可以认为是在Looper.loop()中执行的。

你可能感兴趣的:(android,android)