美文网首页Android面试相关Android面试系列Android技术知识
Android面试(五):异步消息机制之Handler面试你所需

Android面试(五):异步消息机制之Handler面试你所需

作者: zhang_pan | 来源:发表于2018-02-05 21:30 被阅读105次

    1. 什么是Handler,为什么要有Handler?

    Android中主线程也叫UI线程,主线程主要是用来创建、更新UI的,而其他耗时操作,比如网络访问,文件处理、多媒体处理等都需要在子线程中操作,之所以在子线程中操作是为了保证UI的流畅程度,手机显示的刷新频率是60Hz,也就是一秒钟刷新60次,每16.67毫秒刷新一次,为了不丢失帧,那么主线程处理代码最好不要超过16毫秒。当子线程处理完数据后,为了防止UI处理逻辑的混乱(1.锁机制会让UI处理逻辑变得混乱;2.锁机制会降低UI访问的效率,因为锁机制会阻塞某些线程的执行),Android只允许主线程修改UI,那么这时候就需要Handler来充当子线程和主线程之间的桥梁了。

    2. Handler的使用方法:

    1. post(runnable)
    2. sendMessage(message)
      其实post(runnable)和sendMessage(message)最终底层都是调用了sendMessageAtTime方法。

    3. Handler消息机制的原理:

    在主线程中Android默认已经调用了Looper.preperMainLooper方法,调用该方法的目的是在Looper中创建MessageQueue成员变量并把Looper对象绑定到当前线程中(ThreadLocal)。当调用Handler的sendMessage方法的时候,就将Message对象添加到了Looper创建的MessageQueue队列中,同时给Message指定了target对象,其实这个target对象就是Handler对象。主线程默认执行了Looper.looper()方法,该方法从Looper的成员变量MessageQueue队列中调用Message的target对象的dispatchMessage方法(也就是msg.target.dispatchMessage方法)取出Message,然后在dispatchMessage方法中调用Message的target对象的handleMessage()方法(也就是msg.target.handleMessag),这样就完成了整个消息机制。

    我们从源码的角度来分析上述原理,首先我们来看Handler的构造方法:

    public Handler(Callback callback, boolean async) {
            if (FIND_POTENTIAL_LEAKS) {
                final Class<? extends Handler> klass = getClass();
                if ((klass.isAnonymousClass() || klass.isMemberClass() || klass.isLocalClass()) &&
                        (klass.getModifiers() & Modifier.STATIC) == 0) {
                    Log.w(TAG, "The following Handler class should be static or leaks might occur: " +
                        klass.getCanonicalName());
                }
            }
    
            mLooper = Looper.myLooper();
            if (mLooper == null) {
                throw new RuntimeException(
                    "Can't create handler inside thread that has not called Looper.prepare()");
            }
            mQueue = mLooper.mQueue;
            mCallback = callback;
            mAsynchronous = async;
        }
    

    我们发现Looper.myLooper()内部是通过sThreadLocal.get()获得Looper的,(关于ThreadLocal:不同的线程访问同一个ThreadLocal,不管是get方法还是set方法对其所做的操作,仅限于各自线程内部。这就是为什么用ThreadLocal来保存Looper,因为这样才能使每一个线程有单独唯一的Looper。)我们可能会想,这是通过get方法获得Looper,那么何时set的呢?
    当我们观察Looper这个类,发现有一个方法prepareMainLooper:

    public static void prepareMainLooper() {
            prepare(false);
            synchronized (Looper.class) {
                if (sMainLooper != null) {
                    throw new IllegalStateException("The main Looper has already been prepared.");
                }
                sMainLooper = myLooper();
            }
        }
    

    在该方法的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));
        }
    

    哦,原来set方法是在这里调用的哈,接下来我们还剩下一个疑问,那就是prepareMainLooper是在哪里调用的呢?其实Android主线程对应一个类ActivityThread,而每个Android应用程序都是从该类的main函数开始的:

    public static void main(String[] args) {
            ...
            Looper.prepareMainLooper();
    
            ActivityThread thread = new ActivityThread();
            thread.attach(false);
    
            if (sMainThreadHandler == null) {
                sMainThreadHandler = thread.getHandler();
            }
    
            if (false) {
                Looper.myLooper().setMessageLogging(new
                        LogPrinter(Log.DEBUG, "ActivityThread"));
            }
    
            // End of event ActivityThreadMain.
            Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
            Looper.loop();
    
            throw new RuntimeException("Main thread loop unexpectedly exited");
        }
    

    我们可以看到,Looper.prepareMainLooper就是在这里调用的,首先程序是从这个main开始的,依次调用了prepareMainLooper ——> prepare ——> sThreadLocal.set,是不是有种茅塞顿开的感觉呢?我们继续看这个main函数,内部调用了Looper.loop,这是Handler机制很重要的一个方法,这也是为什么我们经常说Android主线程默认给我们调用了Looper.prepare和Looper.looper的原因。
    接下来我们再来看我们手动调用了handler.sendMessage或者handler.postRunnable方法,默认底层都是调用handler.sendMessageAtTime,该方法内部调用了enqueueMessage:

    private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
            msg.target = this;
            if (mAsynchronous) {
                msg.setAsynchronous(true);
            }
            return queue.enqueueMessage(msg, uptimeMillis);
        }
    

    我们可以看到这里给msg.target指定了一个this对象,其实这个this就是Handler对象(因为这是在Handler类中啊,又不是内部类其它的),我们继续看到queue.enqueueMessage方法:

    boolean enqueueMessage(Message msg, long when) {
               ...
                boolean needWake;
                if (p == null || when == 0 || when < p.when) {
                    //插入消息到链表的头部:如果当前MessageQueue消息队列为空,或者插入的消息触发时间为0,
                    //亦或是插入消息的触发时间小于现有头结点的触发时间
                    // New head, wake up the event queue if blocked.
                    msg.next = p;
                    mMessages = msg;
                    needWake = mBlocked;
                } else {
                    //根据触发时间,将插入的消息放入到合适的位置
                    // Inserted within the middle of the queue.  Usually we don't have to wake
                    // up the event queue unless there is a barrier at the head of the queue
                    // and the message is the earliest asynchronous message in the queue.
                    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; // invariant: p == prev.next
                    prev.next = msg;
                }
    
                // We can assume mPtr != 0 because mQuitting is false.
                if (needWake) {
                    nativeWake(mPtr);
                }
            }
            return true;
        }
    

    通过调用此方法将消息发送到MessageQueue消息队列中,(这里我一直存在一个问题,这里明明做了触发时间相关的排序,并不符合队列的先进先出的特点,可为什么一直叫做消息队列,就连用的类名翻译过来也是如此,而不是链表呢?还是说这是广义上的队列?如果有知道的大牛,可以跟我说说!!)
    刚刚说过Android主线程,也就是ActivityThread的main函数会调用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;
                }
    
                final long slowDispatchThresholdMs = me.mSlowDispatchThresholdMs;
                ...
                final long start = (slowDispatchThresholdMs == 0) ? 0 : SystemClock.uptimeMillis();
                final long end;
                try {
                    msg.target.dispatchMessage(msg);
                    end = (slowDispatchThresholdMs == 0) ? 0 : SystemClock.uptimeMillis();
                } finally {
                    if (traceTag != 0) {
                        Trace.traceEnd(traceTag);
                    }
                }
                ...
                msg.recycleUnchecked();
            }
        }
    

    loop方法中调用了queue.next()方法:next()方法是一个无线循环的方法,如果消息队列中没有消息,那么next()方法会一直阻塞,当有新消息到来时,next()方法会将这条消息返回同时也将这条消息从链表中删除。我们主要再来看msg.target.dispatchMessage方法,从上面的分析可以知道msg.target其实就是Handler对象,我们找到dispatchMessage方法:

    public void dispatchMessage(Message msg) {
            if (msg.callback != null) {
                handleCallback(msg);
            } else {
                if (mCallback != null) {
                    if (mCallback.handleMessage(msg)) {
                        return;
                    }
                }
                handleMessage(msg);
            }
        }
    

    其中调用了handleMessage,这也就是我们创建Handler类或其子类,所需要重写的handleMessage方法。至此,整个Handler消息机制就走通了,面试的时候,我们只需要说上面的原理,看源码主要是为了更深入的了解,而不是简单的记忆、背诵,要在理解的基础上去记。

    4. Handler引起的内存泄漏以及解决办法

    原因:
    非静态内部类持有外部类的强引用,导致外部Activity无法释放。

    解决办法:
    1.handler内部持有外部activity的弱引用
    2.把handler改为静态内部类
    3.mHandler.removeCallbacksAndMessage(尤其针对延时消息)

    5. Handler相关的问题:

    (1) Looper死循环为什么不会导致应用卡死?

    对于线程即是一段可执行的代码,当可执行代码执行完成后,线程生命周期便该终止了,线程退出。而对于主线程,我们是绝不希望会被运行一段时间,自己就退出,那么如何保证能一直存活呢?简单做法就是可执行代码是能一直执行下去的,死循环便能保证不会被退出,例如,binder 线程也是采用死循环的方法,通过循环方式不同与 Binder 驱动进行读写操作,当然并非简单地死循环,无消息时会休眠。但这里可能又引发了另一个问题,既然是死循环又如何去处理其他事务呢?通过创建新线程的方式。真正会卡死主线程的操作是在回调方法 onCreate/onStart/onResume 等操作时间过长,会导致掉帧,甚至发生 ANR,looper.loop 本身不会导致应用卡死。

    (2) 主线程的死循环一直运行是不是特别消耗 CPU 资源呢?

    其实不然,这里就涉及到 Linux pipe/epoll机制,简单说就是在主线程的 MessageQueue 没有消息时,便阻塞在 loop 的 queue.next() 中的 nativePollOnce() 方法里,此时主线程会释放 CPU 资源进入休眠状态,直到下个消息到达或者有事务发生,通过往 pipe 管道写端写入数据来唤醒主线程工作。这里采用的 epoll 机制,是一种IO多路复用机制,可以同时监控多个描述符,当某个描述符就绪(读或写就绪),则立刻通知相应程序进行读或写操作,本质同步I/O,即读写是阻塞的。 所以说,主线程大多数时候都是处于休眠状态,并不会消耗大量CPU资源。

    (3) 子线程中Toast、showDialog问题

    Toast 本质是通过 window 显示和绘制的(操作的是 window),而子线程不能更新UI 是因为 ViewRootImpl 的 checkThread方法 在 Activity 维护 View树 的行为。
    Dialog 亦是如此。

    参考链接:Android 消息机制——你真的了解Handler?

    喜欢本篇博客的简友们,就请来一波点赞,您的每一次关注,将成为我前进的动力,谢谢!

    相关文章

      网友评论

        本文标题:Android面试(五):异步消息机制之Handler面试你所需

        本文链接:https://www.haomeiwen.com/subject/wqgbzxtx.html