Android面试相关Android面试系列Android技术知识

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

2018-02-05  本文已影响105人  zhang_pan

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?

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

上一篇下一篇

猜你喜欢

热点阅读