Handler后传篇二: 该如何理解Handler的"异步"?

2,090 阅读7分钟

提到异步, 大家首先想到的一定是多线程, 多线程当中的并发, 指的是通过CPU调度计算, 让用户看上去是同时执行的, 实际上从CPU操作层面上并不是真正的同时。CPU通过调度计算, 在同一时间, 只会有一条线程在执行任务, 只不过调度计算速度很快, 随机选择执行的线程, 所以我们看起来像是同时在执行, 下面我们来举个例子: 

private static class ThreadOne extends Thread {

    private static final String ThreadName = "ThreadOne";

    @Override
    public void run() {
        super.run();

        for (int i = 0; i < 100; i++) {
            Log.i(TAG, "run: " + ThreadName + "执行了任务" + i);
        }
    }
}
private static class ThreadTwo extends Thread {

    private static final String ThreadName = "ThreadTwo";

    @Override
    public void run() {
        super.run();

        for (int i = 0; i < 100; i++) {
            Log.i(TAG, "run: " + ThreadName + "执行了任务" + i);
        }
    }
}
private void threadTest() {

    final ThreadOne threadOne = new ThreadOne();
    final ThreadTwo threadTwo = new ThreadTwo();

    threadOne.start();
    threadTwo.start();
}

例子很简单, 创建两个线程, 调用 start()方法启动线程

上面的例子运行结果如下: (部分结果截图)



不同的手机运行结果可能不同, 但是我们还是能够得出结论: 

多线程的异步, 是多条路径都在执行, 互不干扰, 所以他们执行的顺序是未知的, 这得看CPU的调度计算,正常情况下, 你无法控制哪条线程先执行哪条线程后执行, 或者哪条线程先结束哪条线程后结束。

平时我们提到 Handler, 我们有的时候也会说是异步分发, 但是这里的"异步"跟多线程的异步是一样的吗?? 我们看一下下面的小例子: 

private void test() {

    Log.i(TAG, "onCreate: 事件分发前");
    sHandler.sendEmptyMessage(0);
    try {
        Thread.sleep(2000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    Log.i(TAG, "test: 事件分发后");
}
private static Handler sHandler = new Handler() {
    @Override
    public void handleMessage(Message msg) {
        super.handleMessage(msg);
        Log.i(TAG, "handleMessage: 事件处理回调");
    }
};

我们在onCreate()方法中执行了 test() 方法, 通过 hander 分发事件以后, 让主线程 slee 2秒, 

运行上面的程序以后, 打印结果如下: 



发现问题了吗?? 多线程的情况下, 每一条线程中的代码是交替执行的, 但是上面的这个Handler 例子, 并没有出现交替执行的情况, 而是等到了我们 test()方法执行完成了以后, 才去执行了 Handler 的处理回调, 这看上去并不是一个异步操作, 而是一个同步操作, 只不过Handler 回调的代码放到了最后执行, 这是为什么呢? 下面我们来分析一下: 

首先, 我们得了解 Handler 的运行原理, 我们的主线程会对应配备一个消息队列MessageQueue, 并且只有一个消息队列, Handler 是消息的发送接收者和处理这, Message是消息的载体, 大致的一个运行情况如下: 



这里面, 消息的发送和处理并不是连续发生的, 并不是我发送了消息以后立即就去执行处理消息, 而是现将消息发送到线程中对应的消息队列的尾部, 当消息队列中前面的消息执行完了以后, 才会轮到他来执行.

再者, 在android中, 所有的事件的运行都是基于事件驱动的, activity 中的声明周期也不例外, 在 app 程序的入口类 ThreadActivity 中定义了一个内部类 H, 这个H就是一个Handler, activity 的声明周期也是通过 handler 进行分发的, 感兴趣的可以去看看源码, 这里不是我们的重点, 

这里, 我通过几张图来分析一下我们上面的Handler 例子:

现在假设我们的 test() 方法是消息A, 在 test()方法中通过Handler 发送的消息是消息B, 为了更直观一点, 我们假设消息A是带血槽的(原谅我中了王者的毒):


前面我们说过, onCreate()方法也是通过Handler 分发出来的, 也就是程序通过Handler 将我们的消息发送到主线程的消息队列 MessageQueue 中, test()方法在 onCreate 中执行的, 我们只看test()方法, 也就是消息A在将要被执行是的状态是这样的: 


这时消息A开始执行, 当第一个血槽执完以后, 变成下面这样: 


这时候, 血槽二开始执行, 在这是通过 handler 分发事件B, 也就是 test()方法中的sHandler.sendEmptyMessage(0);

现在MessageQueue 中的状态有变成了下面这样: 


因为我们的Handler 是在主线程中创建的,也就是说Handler 跟主线程绑定的, 所以通过Handler 发送出来的消息, 会先存到消息队列MessageQueue 中, 这是消息A没有执行完, 等到消息A全部执行完了, 我们的消息B才能得到执行, 这也是为什么我们上面的那个例子打印顺序的原因.

通过上面的分析, 我们可以知道, Handler 中所谓的"异步", 其实并不是真正的异步, 只不过是消息的发送和处理不止先后执行的, 而且我们的程序也不用阻塞着等到 handle 回调执行完成以后再继续执行, 而是继续向下执行, 等当前消息执行完成了, 再从消息队列中取出下一个消息执行.最终的执行也并不是异步的,而是同步的, 只不过消息B放到了事件A的后面, 当时间A执行完了才能轮到事件B执行.

而且, 在这里我们需要注意一个问题, Handler 最终的回调也是在主线程的, 我们不能再他的回调方法中执行耗时操作, 不单单是 handler.sendMessage()方法, 我们调用 handler.post()方法的时候他也是这样的.

说到这里, 让我想起来之前在学习 view 的绘制过程中遇到的一个问题.

我们知道, view 的测量绘制操作是在 ViewRootImpl 这个类里面的, 这个类是在Activity 的onResume()方法后才创建的, 所以我们在 onCreat()方法中去获取 view 的宽高, 得到的都是0. 如果想在 onCreate()方法中获取宽高, 其中的一个方法就是通过 view.post()方法去获取,我们可以看下这个方法的源码: 

public boolean post(Runnable action) {
    final AttachInfo attachInfo = mAttachInfo;
    if (attachInfo != null) {
        return attachInfo.mHandler.post(action);
    }

    // Postpone the runnable until we know on which thread it needs to run.
    // Assume that the runnable will be successfully placed after attach.
    getRunQueue().post(action);
    return true;
}

AttachInfo 是View 的一个内部类, 在 onCreate()方法中 view 还没有被创建, 所以这里是AttachInfo 是 null 的, 这是他会调用下面这个方法:

getRunQueue().post(action);

在调用这个方法之后, 他会把传入的Runnable 对象放入到运行队列 RunQueue 中, 这里只是放入运行队列, 而并没有去执行, 真正的执行是在哪里呢?在ViewRootImpl的 performTraversals()这个方法里面, 这里为了便于分析, 我简化一下这个方法: 

performTraversals() {
    // 省略.....
    getRunQueue().executeActions(mAttachInfo.mHandler);
    
    performMeasure();
    
    performLayout();

    performDraw();

}

这里面会真正的去执行运行队列中的消息, 但是问题来了, 我们在这个方法里面, 是先调用的运行队列, 再调用的 performMeasure()方法, 按道理来说, 我们的 view.post() 里面也拿不到 view 的宽高, 但是我们是可以拿到的, 其实这个原因就是我们上面分析的那样, 当执行performTraversals()这个方法时,它是阻塞在消息队列头部的, 然后调用 getRunnQueue().executeAction()方法会把运行队列里面的消息发送到消息队列里面, 但是它是排在 performTraversals()这个方法之后的, 只有当performTraversals()方法执行完了, 才能轮到运行队列里面的消息执行.