安卓资源收集Android开发经验谈Android开发

从设计角度理解Handler通信机制

2017-11-18  本文已影响80人  蓝灰_q

Handler通信机制大家都很熟悉了,但是我们不应满足于知道如何使用这套机制,本文试图从背后的设计思路着手,更深入地理解Handler的设计原理。
我们将从线程分工、建立通信、添加和处理、处理时机和内存泄露原理五个方面,尝试学习Handler的设计原理。

线程分工

App中的View都是由ViewRootImpl负责的,刷新View都是从ViewRootImpl发起的,在ViewRootImpl中,有这样一个前置的判断函数

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

这个函数起到的作用就是,只有主线程ActivityThread能刷新View。
背后的设计原理是,为了系统界面表现流畅,View的刷新时间必须在16ms之内,这就不能使用复杂的多线程锁同步机制来刷新UI,需要硬性限制只能在主线程ActivityThread中操作UI,其他工作线程不得操作UI。(仅在通过ViewRootImpl操作View时限制为主线程,如果直接操作window,如Toast,就不要求必须是主线程)

所以从线程分工的角度看,主线程和子线程之间需要有一套通信机制,Android提供的就是Handler通信机制。

建立通信

我们知道主线程ActivityThread角色非常重要,必须保持流畅,不能阻塞,所以不能用共享数据和锁同步的方式来建立通信,那样开销太大,也太危险。
我们很容易想到使用队列,在ActivityThread中维持一个队列,把任务排进队列里,ActivityThread可以通过这个队列来管理任务,这样可以避免锁开销。
但是,使用队列需要解决两个问题:
1.工作线程需要向队列写数据
2.必须保证队列的线程安全(而且不能用锁同步,否则没有意义)
这两个问题看起来是相互矛盾的,但是Android巧妙地采用了ThreadLocal解决了这个问题。
我们知道,Thread本身也是一个对象,这就可以在Thread的对象实例里维持一个数据集合,具体是一个ThreadLocal.ThreadLocalMap集合(其实是一个数组,每个元素Entry有key和value两个数据,key是ThreadLocal对象),所以每个Thread实例都自己的ThreadLocal对象,是其他线程访问不到的。

这样问题2就解决了,队列放到ThreadLocal里,只有所在的线程能操作这个队列,这样可以保证队列的线程安全。

至于问题1,因为其他的线程不能操作主线程的队列,所以就需要一个主线程的对象来做中转,这个对象不能是ThreadLocal的,这样可以被其他线程访问,这个对象还需要能引用主线程队列,这样可以向队列里插数据。
这个对象,就是android.os.Handler,他引用了当前线程的Looper,从而可以操作Looper的消息队列MessageQueue。

添加和处理

对于消息队列的添加和处理,就到了我们最常使用的环节了,添加是很简单的,handler直接把message添加进messagequeue即可,具体有send Message和post Runnable两种函数,不过最终都是包装为message。

为了处理队列中的消息,主线程会持续不断地运转Looper,依次处理队列中的任务,但是从职责分工上,Looper自己不处理任务,需要把消息交给某个对象处理,Android的做法是,在handler添加消息时,让这个message引用这个handler(target属性),在Looper需要处理这个消息时,就把这个message交给引用的这个handler来处理,然后就会进入我们熟悉handler的handleMessage回调。
(如果message是post的runnable对象,就不需要handler处理,这时的做法是让message的另一个属性callback指向这个runnable,Looper直接回调这个runnable)

处理时机

基本流程上,都是通过MessageQueue的next函数来排队处理的。

处理任务的时机可以分为即时、排队和闲时。
对于主线程来说,可以即时发起任务。
对于子线程来说,因为handler机制本质是一个消息队列,只能排队处理任务,排队时可以定义延迟执行的时机。
在某些情况下,不紧迫的任务不想占用队列,又想排进队列,就可以用消息队列的IdleHandler添加闲时任务,在队列不忙的时候处理。

关于Handler对于延迟消息的处理,其实需要解决三个问题:队列怎么管理延迟消息和非延迟消息、如何判断延迟消息的时间、如何与手机状态(休眠)关联
1.队列怎么管理延迟消息和非延迟消息:在enqueuMessage时,会按msg.when的时间排序;在Looper用next处理消息时,根据when去计算需要等待的时间,这个时间赋给nextPollTimeoutMillis,用于阻塞一定的时间。
2.如何判断延迟消息的时间:延迟消息会有个msg.when,这个时间与系统启动后的时间SystemClock.uptimeMillis()做对比,得出是否可以执行,这个时间差会赋给nextPollTimeoutMillis,用于控制阻塞时间。
3.如何与手机状态(休眠)关联:android在息屏之后很容易进入休眠,休眠时SystemClock.uptimeMillis是不更新的,android会定期去唤醒手机接收消息,(这会导致息屏后延迟消息的时间不准)

内存泄露原理

Handler最典型的问题是内存泄露,这跟Google官方给的例子(匿名内部类)误导也有关系。
我们可以梳理一下Handler匿名内部类出现内存泄露的原理:
匿名内部类的外部类是Activity(举例);
匿名内部类在编译时会把外部类Activity的实例作为构造参数传进来;
匿名内部类就引用了Activity实例;
匿名内部类的Handler实例向MessageQueue传入了一个延迟消息,这个消息的target会引用handler;
这样message就引用了handler,间接引用了Activity;
message是messageQueue里的,messageQueue是Looper里的,Looper是ActivityThread主线程里的,主线程是始终在运行Looper的,符合JVM对GCRoot规定中的第一种“虚拟机栈中的引用”。
所以,Activity到GCRoot是可达的强引用,哪怕Activity已退出,只要message还没有得到处理,就会导致内存泄露。

参考

Handler延时处理消息的流程

上一篇下一篇

猜你喜欢

热点阅读