Handler二十七问|你真的了解我吗?(上)

简介: 对于handler,你会想到什么呢?

前言


对于handler,你会想到什么呢?


面试必考?项目常用?体系庞大?


既然它如此重要,不知对面的你了解它多深呢?今天就和大家一起打破砂锅问到底,看看Handler这口砂锅的底到底在哪里。


Handler二十七问,送上。


大纲


23.png


Handler被设计出来的原因?有什么用?


一种东西被设计出来肯定就有它存在的意义,而Handler的意义就是切换线程。


作为Android消息机制的主要成员,它管理着所有与界面有关的消息事件,常见的使用场景有:


  • 跨进程之后的界面消息处理。


比如Activity的启动,就是AMS在进行进程间通信的时候,通过Binder线程 将消息发送给ApplicationThread的消息处理者Handler,然后再将消息分发给主线程中去执行。


  • 网络交互后切换到主线程进行UI更新


当子线程网络操作之后,需要切换到主线程进行UI更新。


总之一句话,Hanlder的存在就是为了解决在子线程中无法访问UI的问题。


为什么建议子线程不访问(更新)UI?


因为Android中的UI控件不是线程安全的,如果多线程访问UI控件那还不乱套了。

那为什么不加锁呢?


  • 会降低UI访问的效率。本身UI控件就是离用户比较近的一个组件,加锁之后自然会发生阻塞,那么UI访问的效率会降低,最终反应到用户端就是这个手机有点卡。
  • 太复杂了。本身UI访问时一个比较简单的操作逻辑,直接创建UI,修改UI即可。如果加锁之后就让这个UI访问的逻辑变得很复杂,没必要。


所以,Android设计出了 单线程模型 来处理UI操作,再搭配上Handler,是一个比较合适的解决方案。


子线程访问UI的 崩溃原因 和 解决办法?


崩溃发生在ViewRootImpl类的checkThread方法中:


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


其实就是判断了当前线程 是否是 ViewRootImpl创建时候的线程,如果不是,就会崩溃。


而ViewRootImpl创建的时机就是界面被绘制的时候,也就是onResume之后,所以如果在子线程进行UI更新,就会发现当前线程(子线程)和View创建的线程(主线程)不是同一个线程,发生崩溃。


解决办法有三种:


  • 在新建视图的线程进行这个视图的UI更新,主线程创建View,主线程更新View。
  • ViewRootImpl创建之前进行子线程的UI更新,比如onCreate方法中进行子线程更新UI。
  • 子线程切换到主线程进行UI更新,比如Handler、view.post方法。


MessageQueue是干嘛呢?用的什么数据结构来存储数据?


看名字应该是个队列结构,队列的特点是什么?先进先出,一般在队尾增加数据,在队首进行取数据或者删除数据。


Hanlder中的消息似乎也满足这样的特点,先发的消息肯定就会先被处理。但是,Handler中还有比较特殊的情况,比如延时消息。


延时消息的存在就让这个队列有些特殊性了,并不能完全保证先进先出,而是需要根据时间来判断,所以Android中采用了链表的形式来实现这个队列,也方便了数据的插入。


来一起看看消息的发送过程,无论是哪种方法发送消息,都会走到sendMessageDelayed方法


public final boolean sendMessageDelayed(@NonNull Message msg, long delayMillis) {
        if (delayMillis < 0) {
            delayMillis = 0;
        }
        return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
    }
    public boolean sendMessageAtTime(@NonNull Message msg, long uptimeMillis) {
        MessageQueue queue = mQueue;
        return enqueueMessage(queue, msg, uptimeMillis);
    }


sendMessageDelayed方法主要计算了消息需要被处理的时间,如果delayMillis为0,那么消息的处理时间就是当前时间。


然后就是关键方法enqueueMessage


boolean enqueueMessage(Message msg, long when) {
        synchronized (this) {
            msg.markInUse();
            msg.when = when;
            Message p = mMessages;
            boolean needWake;
            if (p == null || when == 0 || when < p.when) {
                msg.next = p;
                mMessages = msg;
                needWake = mBlocked;
            } else {
                needWake = mBlocked && p.target == null && msg.isAsynchronous();
                Message prev;
                for (;;) {
                    prev = p;
                    p = p.next;
                    if (p == null || when < p.when) {
                        break;
                    }
                    if (needWake && p.isAsynchronous()) {
                        needWake = false;
                    }
                }
                msg.next = p; 
                prev.next = msg;
            }
            if (needWake) {
                nativeWake(mPtr);
            }
        }
        return true;
    }


不懂得地方先不看,只看我们想看的:


  • 首先设置了Message的when字段,也就是代表了这个消息的处理时间
  • 然后判断当前队列是不是为空,是不是即时消息,是不是执行时间when大于表头的消息时间,满足任意一个,就把当前消息msg插入到表头。
  • 否则,就需要遍历这个队列,也就是链表,找出when小于某个节点的when,找到后插入。


好了,其他内容暂且不看,总之,插入消息就是通过消息的执行时间,也就是when字段,来找到合适的位置插入链表。


具体方法就是通过死循环,使用快慢指针p和prev,每次向后移动一格,直到找到某个节点p的when大于我们要插入消息的when字段,则插入到p和prev之间。或者遍历到链表结束,插入到链表结尾。


所以,MessageQueue就是一个用于存储消息、用链表实现的特殊队列结构。


延迟消息是怎么实现的?


总结上述内容,延迟消息的实现主要跟消息的统一存储方法有关,也就是上文说过的enqueueMessage方法。


无论是即时消息还是延迟消息,都是计算出具体的时间,然后作为消息的when字段进程赋值。


然后在MessageQueue中找到合适的位置(安排when小到大排列),并将消息插入到MessageQueue中。


这样,MessageQueue就是一个按照消息时间排列的一个链表结构。


MessageQueue的消息怎么被取出来的?


刚才说过了消息的存储,接下来看看消息的取出,也就是queue.next方法。


Message next() {
        for (;;) {
            if (nextPollTimeoutMillis != 0) {
                Binder.flushPendingCommands();
            }
            nativePollOnce(ptr, nextPollTimeoutMillis);
            synchronized (this) {
                // Try to retrieve the next message.  Return if found.
                final long now = SystemClock.uptimeMillis();
                Message prevMsg = null;
                Message msg = mMessages;
                if (msg != null && msg.target == null) {
                    do {
                        prevMsg = msg;
                        msg = msg.next;
                    } while (msg != null && !msg.isAsynchronous());
                }
                if (msg != null) {
                    if (now < msg.when) {
                        nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
                    } else {
                        // Got a message.
                        mBlocked = false;
                        if (prevMsg != null) {
                            prevMsg.next = msg.next;
                        } else {
                            mMessages = msg.next;
                        }
                        msg.next = null;
                        msg.markInUse();
                        return msg;
                    }
                } else {
                    // No more messages.
                    nextPollTimeoutMillis = -1;
                }
            }
        }
    }


奇怪,为什么取消息也是用的死循环呢?


其实死循环就是为了保证一定要返回一条消息,如果没有可用消息,那么就阻塞在这里,一直到有新消息的到来。


其中,nativePollOnce方法就是阻塞方法,nextPollTimeoutMillis参数就是阻塞的时间。


那什么时候会阻塞呢?两种情况:


  • 1、有消息,但是当前时间小于消息执行时间,也就是代码中的这一句:


if (now < msg.when) {
    nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
}


这时候阻塞时间就是消息时间减去当前时间,然后进入下一次循环,阻塞。


  • 2、没有消息的时候,也就是上述代码的最后一句:


if (msg != null) {} 
    else {
    // No more messages.
    nextPollTimeoutMillis = -1;
    }


-1就代表一直阻塞。


MessageQueue没有消息时候会怎样?阻塞之后怎么唤醒呢?说说pipe/epoll机制?


接着上文的逻辑,当消息不可用或者没有消息的时候就会阻塞在next方法,而阻塞的办法是通过pipe/epoll机制


epoll机制是一种IO多路复用的机制,具体逻辑就是一个进程可以监视多个描述符,当某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作,这个读写操作是阻塞的。在Android中,会创建一个Linux管道(Pipe)来处理阻塞和唤醒。


  • 当消息队列为空,管道的读端等待管道中有新内容可读,就会通过epoll机制进入阻塞状态。
  • 当有消息要处理,就会通过管道的写端写入内容,唤醒主线程。


同步屏障和异步消息是怎么实现的?


其实在Handler机制中,有三种消息类型:


  • 同步消息。也就是普通的消息。
  • 异步消息。通过setAsynchronous(true)设置的消息。
  • 同步屏障消息。通过postSyncBarrier方法添加的消息,特点是target为空,也就是没有对应的handler。


这三者之间的关系如何呢?


  • 正常情况下,同步消息和异步消息都是正常被处理,也就是根据时间when来取消息,处理消息。
  • 当遇到同步屏障消息的时候,就开始从消息队列里面去找异步消息,找到了再根据时间决定阻塞还是返回消息。


也就是说同步屏障消息不会被返回,他只是一个标志,一个工具,遇到它就代表要去先行处理异步消息了。


所以同步屏障和异步消息的存在的意义就在于有些消息需要“加急处理”


同步屏障和异步消息有具体的使用场景吗?


使用场景就很多了,比如绘制方法scheduleTraversals


void scheduleTraversals() {
        if (!mTraversalScheduled) {
            mTraversalScheduled = true;
            // 同步屏障,阻塞所有的同步消息
            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
            // 通过 Choreographer 发送绘制任务
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
        }
    }
    Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
    msg.arg1 = callbackType;
    msg.setAsynchronous(true);
    mHandler.sendMessageAtTime(msg, dueTime);


在该方法中加入了同步屏障,后续加入一个异步消息MSG_DO_SCHEDULE_CALLBACK,最后会执行到FrameDisplayEventReceiver,用于申请VSYNC信号。


更多Choreographer相关内容可以看看这篇文章——https://www.jianshu.com/p/86d00bbdaf60


Message消息被分发之后会怎么处理?消息怎么复用的?


再看看loop方法,在消息被分发之后,也就是执行了dispatchMessage方法之后,还偷偷做了一个操作——recycleUnchecked


public static void loop() {
        for (;;) {
            Message msg = queue.next(); // might block
            try {
                msg.target.dispatchMessage(msg);
            } 
            msg.recycleUnchecked();
        }
    }
//Message.java
    private static Message sPool;
    private static final int MAX_POOL_SIZE = 50;
    void recycleUnchecked() {
        flags = FLAG_IN_USE;
        what = 0;
        arg1 = 0;
        arg2 = 0;
        obj = null;
        replyTo = null;
        sendingUid = UID_NONE;
        workSourceUid = UID_NONE;
        when = 0;
        target = null;
        callback = null;
        data = null;
        synchronized (sPoolSync) {
            if (sPoolSize < MAX_POOL_SIZE) {
                next = sPool;
                sPool = this;
                sPoolSize++;
            }
        }
    }


recycleUnchecked方法中,释放了所有资源,然后将当前的空消息插入到sPool表头。


这里的sPool就是一个消息对象池,它也是一个链表结构的消息,最大长度为50。


那么Message又是怎么复用的呢?在Message的实例化方法obtain中:


public static Message obtain() {
        synchronized (sPoolSync) {
            if (sPool != null) {
                Message m = sPool;
                sPool = m.next;
                m.next = null;
                m.flags = 0; // clear in-use flag
                sPoolSize--;
                return m;
            }
        }
        return new Message();
    }


直接复用消息池sPool中的第一条消息,然后sPool指向下一个节点,消息池数量减一。


Looper是干嘛呢?怎么获取当前线程的Looper?为什么不直接用Map存储线程和对象呢?


在Handler发送消息之后,消息就被存储到MessageQueue中,而Looper就是一个管理消息队列的角色。Looper会从MessageQueue中不断的查找消息,也就是loop方法,并将消息交回给Handler进行处理。


而Looper的获取就是通过ThreadLocal机制:


static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
    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));
    }
    public static @Nullable Looper myLooper() {
        return sThreadLocal.get();
    }


通过prepare方法创建Looper并且加入到sThreadLocal中,通过myLooper方法从sThreadLocal中获取Looper。


ThreadLocal运行机制?这种机制设计的好处?


下面就具体说说ThreadLocal运行机制。


//ThreadLocal.java
    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }
    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }


ThreadLocal类中的get和set方法可以大致看出来,有一个ThreadLocalMap变量,这个变量存储着键值对形式的数据。


  • key为this,也就是当前ThreadLocal变量。
  • value为T,也就是要存储的值。


然后继续看看ThreadLocalMap哪来的,也就是getMap方法:


//ThreadLocal.java
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
    //Thread.java
    ThreadLocal.ThreadLocalMap threadLocals = null;


原来这个ThreadLocalMap变量是存储在线程类Thread中的。


所以ThreadLocal的基本机制就搞清楚了:


在每个线程中都有一个threadLocals变量,这个变量存储着ThreadLocal和对应的需要保存的对象。


这样带来的好处就是,在不同的线程,访问同一个ThreadLocal对象,但是能获取到的值却不一样。


挺神奇的是不是,其实就是其内部获取到的Map不同,Map和Thread绑定,所以虽然访问的是同一个ThreadLocal对象,但是访问的Map却不是同一个,所以取得值也不一样。


这样做有什么好处呢?为什么不直接用Map存储线程和对象呢?


打个比方:


  • ThreadLocal就是老师。
  • Thread就是同学。
  • Looper(需要的值)就是铅笔。


现在老师买了一批铅笔,然后想把这些铅笔发给同学们,怎么发呢?两种办法:


  • 1、老师把每个铅笔上写好每个同学的名字,放到一个大盒子里面去(map),用的时候就让同学们自己来找。


这种做法就是Map里面存储的是同学和铅笔,然后用的时候通过同学来从这个Map里找铅笔。


这种做法就有点像使用一个Map,存储所有的线程和对象,不好的地方就在于会很混乱,每个线程之间有了联系,也容易造成内存泄漏。


  • 2、老师把每个铅笔直接发给每个同学,放到同学的口袋里(map),用的时候每个同学从口袋里面拿出铅笔就可以了。


这种做法就是Map里面存储的是老师和铅笔,然后用的时候老师说一声,同学只需要从口袋里拿出来就行了。


很明显这种做法更科学,这也就是ThreadLocal的做法,因为铅笔本身就是同学自己在用,所以一开始就把铅笔交给同学自己保管是最好的,每个同学之间进行隔离。


还有哪些地方运用到了ThreadLocal机制?


比如:Choreographer。


public final class Choreographer {
    // Thread local storage for the choreographer.
    private static final ThreadLocal<Choreographer> sThreadInstance =
            new ThreadLocal<Choreographer>() {
        @Override
        protected Choreographer initialValue() {
            Looper looper = Looper.myLooper();
            if (looper == null) {
                throw new IllegalStateException("The current thread must have a looper!");
            }
            Choreographer choreographer = new Choreographer(looper, VSYNC_SOURCE_APP);
            if (looper == Looper.getMainLooper()) {
                mMainInstance = choreographer;
            }
            return choreographer;
        }
    };
    private static volatile Choreographer mMainInstance;


Choreographer主要是主线程用的,用于配合 VSYNC中断信号。


所以这里使用ThreadLocal更多的意义在于完成线程单例的功能。



目录
相关文章
|
消息中间件 Android开发
Handler postDelayed的实现原理
老生常谈之Handler
143 0
|
消息中间件 Android开发
Handler源码解读——handler使用时的注意事项
工作中经常会遇到从子线程发送消息给主线程,让主线程更新UI的操作,常见的有handler.sendMessage(Message),和handler.post(runnable)和handler.postDelayed(runnable, milliseconds);一直在使用这些方法,却不知道他们的原理,今天就来解释一下他们的原理。
|
消息中间件 安全 Android开发
Handler二十七问|你真的了解我吗?(下)
对于handler,你会想到什么呢?
113 0
|
消息中间件 Android开发
【Android 异步操作】手写 Handler ( Handler 发送与处理消息 | Handler 初始化 | 完整 Handler 代码 )
【Android 异步操作】手写 Handler ( Handler 发送与处理消息 | Handler 初始化 | 完整 Handler 代码 )
140 0
|
消息中间件 调度 Android开发
面试:Handler 的工作原理是怎样的?
面试场景 平时开发用到其他线程吗?都是如何处理的? 基本都用 RxJava 的线程调度切换,嗯对,就是那个 observeOn 和 subscribeOn 可以直接处理,比如网络操作,RxJava 提供了一个叫 io 线程的处理。
1196 0