Android消息机制疑问解析

对于这个内容的blog网上有很多撰写的,但是发觉有些点看了一次两次都会记不牢靠,因为这些东西并没有成为自己来整理的东西,所以写下这篇来进行记录

简介

Android消息机制主要是Handler的运行机制,以及它附带的Looper和MessageQueue,他们三一起工作形成一个整体。
Q:Android为什么提供这个功能?
A:Android规定访问UI只能在主线程进行,如果在子线程访问UI那么会抛出异常,这个异常是ViewRootImpl如下代码抛出的。

void checkThread() {
        if (mThread != Thread.currentThread()) {
            throw new CalledFromWrongThreadException(
                    "Only the original thread that created a view hierarchy can touch its views.");
        }
    }

由于ViewRootImpl负责对ui的操作,所以我们要在主线程中访问ui操作,但是又延伸一个问题。

Q:对UI的操作必须在主线程才可以访问吗?

A:不一定,这个我们要了解ViewRootImpl的运行原理以及创建校验机制,下面的代码在Activity居然可以成功执行。

@Override
    protected void onResume() {
        super.onResume();
        new Thread(new Runnable() {
            @Override
            public void run() {
                button.setText("onResume change");
            }
        }).start();
    }

解释:

  • ViewRootImpl在文章View体系中我们可以知道它负责View的绘制过程,而上述的代码肯定是在没有执行checkThread方法的情况下更新的ui。

  • ViewRootImpl和View的创建是在WindowManager里面来进行创建的,而在Activity的onResume方法里面才把View添加显示到Window上面。我们这样理解,在onResume方法执行之前或者里面的时候都在通过异步的创建使用ViewRootImpl,所以我们的代码在onResume里面执行的时候并没有执行到ViewRootImpl里面的checkThread方法。

  • 那么ui是如何更新的呢?这里我们有一个理解既然View的ui操作是ViewRootImpl来进行的为啥没有它的存在的前提下我们把ui还是更新了。因为View自己处理了自己的invalidate方法完成了更新。可以这样说,ViewRootImpl也是调用View的绘制方法来更新的,而这里越过了ViewRootImpl来检测线程的一个后续的过程而已,具体过程如下:

    • ImageView.setText
    • View.invalidate
    • View.invalidateInternal
    • ViewGroup.invalidateChild
    • ViewParent.invalidateChildInParent //这里会不断Loop去取上一个结点的mParent
    • ViewRootImpl.invalidateChildInParent //DecorView的mParent是ViewRootImpl
    • ViewRootImpl.checkThread //在这里执行checkThread,如果非UI线程则抛出异常

参考链接查看

Q:Android为什么系统提供在具体的线程执行某个功能?
A:因为Android的主线程的控件不是线程安全的,如果多线程并发访问会出现线程安全问题,而且加上锁会使代码变得复杂,特别是会降低效率。所以使用主线程来更新的方式,提供Handler这样的实现来做线程切换更新ui。

消息机制原理分析

我们这里记录对Handler消息机制的实现来进行分析,对Looper对象和MessageQueue对象进行独立分析以及理清他们相互之间的联系。首先我们抛出一个结论:一个线程会有一个Looper对象,它来创建MessageQueue并且构成循环消息队列。如何保证实现的一个线程一个Looper对象的勒?系统把它存放在ThreadLocal里面,下面我们一步步了解他们各自的实现原理。

ThreadLocal实现原理

这里我们以主线程的的消息机制实现为例看到Looper的使用ThreadLocal来看ThreadLocal的实现原理,首先在应用程序入口ActivityThread的main方法里面开始看起:

public static void main(String[] args) {
        //.....
        Looper.prepareMainLooper();
        //......
        Looper.loop();
        throw new RuntimeException("Main thread loop unexpectedly exited");
    }

上面通过Looper.prepareMainLooper()方法来创建Looper对象,我们看他的里面如何实现的。
在perpareMainLooper方法中会调用Looper.prepare方法:

private static void prepare(boolean quitAllowed) {
        if (sThreadLocal.get() != null) {
            throw new RuntimeException("Only one Looper may be created per thread");
        }
        sThreadLocal.set(new Looper(quitAllowed));
    }

看到ThreadLocal的set方法:

public void set(T value) {
        Thread t = Thread.currentThread();//得到当前线程
        ThreadLocalMap map = getMap(t);//得到当前线程持有的ThreadLocalMap对象
        if (map != null)
            map.set(this, value);//把我们上面的Looper对象放入ThreadLocalMap中
        else
            createMap(t, value);//线程的ThreadLocalMap为空就创建再放入Looper
    }

ThreadLocalMap对象是啥?跟到它对应的源码,原来他就是ThreadLocal的一个静态内部类,实现了一个Map可以存储键为ThreadLocal的对象。里面的算法实现set和get我们现在不去看。其中getMap传递了当前的线程作为参数,其实ThreadLocalMap就作为Thread的成员变量来保存的。一个线程保存一个唯一的ThreadLocalMap对象,而在这个ThreadLocalMap里面存储的键值对的值就是我们那个唯一的Looper对象。

MessageQueue工作原理

MessageQueue主要的是插入和读取两个操作。MessageQueue是一个单链表的结构实现。

插入对应函数enqueueMessage方法,具体的代码我们就不贴了,注意其中首先会有一个when时间的判断,如果最后一条Message的时间比我们插入的时间要大,那么我们后面插入的要先执行,即它会插入到链表的末端,否则就会在链表中根据时间插入相应的位置。

读取操作对应这next方法,这里注意它会如果没有消息它会在nativePollOnce方法这里阻塞。

Looper工作原理

Looper的主要工作是进行消息循环。它会不停的从MessageQueue里面查看是否有消息,如果有消息就立刻处理,没有的话就会阻塞。它在创建的时候会创建一个MessageQueue对象,并且把线程对象保存起来。

我们知道使用Handler要有当前所在线程有Handler的存在,我们创建一个子线程然后调用Looper.prepare和Looper.loop然后在中间也可以使用Handler了。

Looper除了prepare方法外,还提供了prepareMainLooper方法,它主要是给主线程创建Looper使用,本质也是调用perpare来实现的。由于主线程的Looper比较特殊,它可以通过getMainLooper来获得,在任何地方可以获得主线程的Looper对象。

Looper提供了quit和quitSafely方法来退出一个Looper,二者区别是quit会直接退出Looper,而quitSafely只是设定一个退出标志,然后把消息队列消息处理完就安全退出。如果我们在子线程自己来手动创建使用Looper,那么可以调用退出方法退出,否则子线程就一直处于等待状态。注意上面使用prepareMainLooper方法创建Looper的时候会调用prepare的时候传递参数quitAllowed为false。在调用退出的时候不允许,根据传递的参数来判断。因为如果主线程不循环了,那么应用就直接关闭了呀。
那么问题来了为什么主线程的没有成为等待状态阻塞呢?

A:对于线程里面的代码执行完毕以后,线程的生命周期就该终止了,对于主线程我们上面也有提及不会允许这种情况发生。我们可以让这段代码一直可以执行下去,死循环即可。真正会卡死主线程的是在生命周期方法onCreate/onStart/onResume里面的操作时间超时导致ANR,looper.loop方法本身不会导致应用卡死。并且我们的ui刷新没16ms一次和各种事件接收以后会丢给Looper,它会接收消息然后处理。
那么又有一个问题,主线程都在循环了,它如何去处理别的事务的呢?(这里别的事务指的是和系统后台交互等)系统通过创建新的线程去完成这个事情。

A:在ActivityThread的main方法中,调用ActivityThread.attach(false)方法来创建一个Binder线程,该线程通过主线程的Handler把消息发给Looper里面去处理。里面主线程一直循环不会特别消耗CPU的资源,因为它使用了Linux pipe/epoll机制,就是通过nativePollOnce()方法使线程休眠,当有事务来的时候唤醒。

Q:Activity的生命周期方法是如何在死循环里面执行的?
A:这里涉及Activity的启动过程,大概的就通过ipc远程调用了后然后拿到主线程的Handler发送消息给looper对象里面,然后处理ActivityThread的Handler H来回调各个生命周期对应的方法处理。

以上问题参考链接知乎讲解

Q:这里我临时又有一个疑惑的问题,既然上面了解到生命周期方法会在Activity的handler里面回调,这个消息都是发送给一个线程的同一个Looper对象的MessageQueue里面,为啥我自己在Activity里面建立一个Handler收不到这个消息?
A:这里我们要注意我们的Message对象,它在回调处理的时候通过msg.tartget.dispatchMessage来回调到发送taget Handler对象的里面,所以我们在一个线程里面只有一个Looper对象,但是我们的Handler可以有多个,他们独立工作,通过Message来分发给不同的发送对象。

Handler工作原理

Handler的工作主要是发送和接受消息。发送消息通过post或者send一系列方法,最终都是调用enqueueMessage方法把消息插入到MessageQueue中。这里注意会对when这个时间进行比较,然后对单链表进行插入操作。这个时候MessageQueue的next方法就会取出这个消息,通过msg.tart.dispatchMessage方法调用Handler的接收消息回调方法。

参考源码分析
鸿洋
《Android开发艺术探索》

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值