Android的Handler的常见面试问题总结

Posted 小图包

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android的Handler的常见面试问题总结相关的知识,希望对你有一定的参考价值。

本文是对常见面试问题的分析,关于Handler的运行机制详细分析见:android Handler的源码分析

常见问题包括:

1、Handler是怎么实现切换线程的?
2、MessageQueue是怎么增删消息的
3、一个线程可以有几个Handler?几个Looper?几个MessageQueue?

4、Looper取出消息又如何发送到指定的Handler呢?
5、简述ThreadLoacal的原理?
6、如何处理Handler使用不当造成的内存泄漏?
7、在UI中创建的Handler,通过post方式发送的消息在run方法中可以进行UI更新吗?

8、Handler机制——同步屏障 是什么,哪里用过同步屏障?

9、Looper死循环为什么不会导致应用卡死,会耗费大量资源吗?

10、主线程的Looper何时退出?能否手动退出?

11、系统为什么不建议在子线程中访问UI?

12 、Android IdleHander 是否了解,是否使用过。

13、如何自定义一个Hander 

一、Handler是怎么实现切换线程的?

  1 调用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));
}

创建了一个Looper对象保存在了一个ThreadLocal对象中.

接着看创建了一个Loper对象,在Loper做了什么

private Looper(boolean quitAllowed) {
      mQueue = new MessageQueue(quitAllowed);
      mThread = Thread.currentThread();
 }
// 创建了一个MessageQueue对象,这个就是一个消息队列用来保存我们发送的Message,其实这个MessageQUueue是一个单链表。

创建了一个MessageQueue对象

 2 创建Handler

public Handler(Callback callback, boolean async) {
        ......
        mLooper = Looper.myLooper();
        ......
        mQueue = mLooper.mQueue;
        mCallback = callback;
        mAsynchronous = async;
    }

Hanlder中获取了一个Looper对象,这个Looper中保存了一个mQueue,

调用loop()

public static void loop() {
        final Looper me = myLooper();
        if (me == null) {
            throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
        }
        final MessageQueue queue = me.mQueue;
        Binder.clearCallingIdentity();
        final long ident = Binder.clearCallingIdentity();
        for (;;) {// 无限循环
            Message msg = queue.next(); // might block
            if (msg == null) {
                // No message indicates that the message queue is quitting.
                return;
            }
            ......
            final long start = (slowDispatchThresholdMs == 0) ? 0 : SystemClock.uptimeMillis();
            final long end;
            try {// 处理消息
                msg.target.dispatchMessage(msg);
            ......
    }

这里看到 loop()方法中获取了Looper对象,取出了消息队列mQueue,然后开始循环,同时调用了MessageQueue的next()方法取出一个消息最后调用了这个消息的dispathchMessage();这里的msg.tagget.dispatchMessage()就是Handler中的消息处理的方法

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

通过上面代码我们知道调用了Hanlder的handleMessage(msg)

4 发送消息

调用sendMessage()等方法发送消息,追踪调用了sendMessageAtTime()的方法,然后调用了enqueueMessage()方法,最后在queue.enqueueMessage(msg, uptimeMillis);在queue中处理了消

public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
        MessageQueue queue = mQueue;
        if (queue == null) {
            RuntimeException e = new RuntimeException(
                    this + " sendMessageAtTime() called with no mQueue");
            Log.w("Looper", e.getMessage(), e);
            return false;
        }
        return enqueueMessage(queue, msg, uptimeMillis);
    }
private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
        msg.target = this;
        if (mAsynchronous) {
            msg.setAsynchronous(true);
        }
        return queue.enqueueMessage(msg, uptimeMillis);
    }
boolean enqueueMessage(Message msg, long when) {
       synchronized (this) {// 同步的操作,同一时间只能一个线程操作
            if (mQuitting) {
                IllegalStateException e = new IllegalStateException(
                        msg.target + " sending message to a Handler on a dead thread");
                Log.w(TAG, e.getMessage(), e);
                msg.recycle();
                return false;
            }
            ......
            msg.markInUse();
            msg.when = when;
            Message p = mMessages;
            boolean needWake;
            if (p == null || when == 0 || when < p.when) {// 判断消息要执行的时间顺序
                // New head, wake up the event queue if blocked.
                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; // invariant: p == prev.next
                prev.next = msg;
            }
        }
        return true;
    }

通过消息要执行的时间,最后把消息插入链表中。主线程的loop()方法一直在循环处理着消息,发送的消息就会到loop()方法中去,最后交给Handler处理。

线程的切换

发送的过程,我们在不同的线程发送消息,线程之间的资源是共享的。也就是任何变量在任何线程都可以修改,只要做并发操作就好了。上述代码中插入队列就是加锁的synchronized,Handler中我们使用的是同一个MessageQueue对象,同一时间只能一个线程对消息进行入队操作。消息存储到队列中后,主线程的Looper还在一直循环loop()处理。这样主线程就能拿到子线程存储的Message对象,这样就完成了线程的切换.

 

二  MessageQueue是怎么增删消息的?

增加:通过enqueueMessage方法,通过对消息添加时间和之前消息的判断,将新添加的消息放置在栈顶

 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));
    }

删除:通过next方法取出消息,在loop()中进行消息的分发处理,在next返回消息前对消息属性进行修改

// MessageQueue中的next()方法
msg.next = null;
msg.markInUse();
// Message中的markInUse()方法,表明消息已经使用
void markInUse() {
        flags |= FLAG_IN_USE;
    }

三、一个线程可以有几个Handler?几个Looper?几个MessageQueue?

一个线程能够创建多个Handler,Handler跟Looper没有对应关系,线程才跟Looper有对应关系,一个线程对应着一个Looper,如下所示:

private static void prepare(boolean quitAllowed) {
    if (sThreadLocal.get() != null) {
        throw new RuntimeException("Only one Looper may be created per thread");
    }
    //将Looper存到当前线程的ThreadLocal中
    sThreadLocal.set(new Looper(quitAllowed));
}

创建Handler时,需要先调用Looper的prepare方法,在该方法中,会首先判断sThreadLocal.get()是否为空,如果不为空就抛出异常,内容是:一个线程只能创建一个Looper。如果sThreadLocal.get()为空,则会创建一个Looper对象并存入sThreadLocal中。

在调用Looper.prepare()时,会判断该线程的Looper是否为空,只有为空的情况才会调用Looper的构造方法,创建MessageQueue,因此只有一个。

private Looper(boolean quitAllowed) {
        mQueue = new MessageQueue(quitAllowed);
        mThread = Thread.currentThread();
    }

四、 Looper取出消息又如何发送到指定的Handler呢?

从handler发送消息,最终会调用Handler类中的enqueueMessage方法,该方法源码如下所示:

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

1 处Message类中的target是什么,该target是一个Handler类型,所以msg的target指向了当前Handler对象,然后msg被加入到消息队列,通过调用下面代码

queue.enqueueMessage(msg, uptimeMillis);


一个线程中包含多个Handelr对象,而在消息的添加分发时,通过Message的target(Handler)标注。

五、简述ThreadLoacal的原理?

通过set和get方法,其内部维护这一个Map集合,其中key是当前的线程,value为需要存储的值。

详细可以参考如下

六 、 如何处理Handler使用不当造成的内存泄漏?

  1. 有延时消息,在界面关闭后及时移除Message/Runnable,调用handler.removeCallbacksAndMessages(null)
  2. 内部类导致的内存泄漏改为静态内部类,并对上下文或者Activity/Fragment使用弱引用。

七 、在UI中创建的Handler,通过post方式发送的消息在run方法中可以进行UI更新吗?

通过查看源码可以发现,其最终调用的还是sendMessage相关的方法,但是如果传入的Runnable形式,其内部会对其进行了封住成Message类。

 private static Message getPostMessage(Runnable r) {
        Message m = Message.obtain();
        m.callback = r;
        return m;
    }

在消息处理结束后,其回调至Runnable 的run方法中,需要注意的是这里的仍然是在UI线程中,因为我们创建的Handler是在UI线程中,且Handler将Runnable内部封住成Message的形式,在消息分发时首先检测callback是否为空,如下所示:

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

如果不为空则调用handleCallback方法,但是该方法实际上是调用message.callback.run(),而message.callbacK是我们在添加消息时赋值的Runable,所以最终调用的是Runable中的run()方法,因此还是回到UI线程中,可以更新UI界面。

private static void handleCallback(Message message) {
        message.callback.run();
    }


八 Handler机制——同步屏障 是什么,哪里用过同步屏障?

其中普通消息又称为同步消息,屏障消息又称为同步屏障。

我们通常使用的都是普通消息,而屏障消息就是在消息队列中插入一个屏障,在屏障之后的所有普通消息都会被挡着,不能被处理。不过异步消息却例外,屏障不会挡住异步消息,因此可以这样认为:屏障消息就是为了确保异步消息的优先级,设置了屏障后,只能处理其后的异步消息,同步消息会被挡住,除非撤销屏障。

哪里使用过同步屏障 在我之前的文章讲到过,链接如下

九、Looper死循环为什么不会导致应用卡死,会耗费大量资源吗?

Looper会在线程中不断的检索消息,如果是子线程的Looper死循环,一旦任务完成,用户应该手动退出,而不是让其一直休眠等待。(引用自Gityuan)线程其实就是一段可执行的代码,当可执行的代码执行完成后,线程的生命周期便该终止了,线程退出。而对于主线程,我们是绝不希望会被运行一段时间,自己就退出,那么如何保证能一直存活呢?简单做法就是可执行代码是能一直执行下去的,死循环便能保证不会被退出,例如,binder 线程也是采用死循环的方法,通过循环方式不同与 Binder 驱动进行读写操作,当然并非简单地死循环,无消息时会休眠。Android是基于消息处理机制的,用户的行为都在这个Looper循环中,我们在休眠时点击屏幕,便唤醒主线程继续进行工作

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

十、 主线程的Looper何时退出?能否手动退出?

在App退出时,ActivityThread中的mH(Handler)收到消息后,执行退出。

/ActivityThread.java
case EXIT_APPLICATION:
    if (mInitialApplication != null) {
        mInitialApplication.onTerminate();
    }
    Looper.myLooper().quit();
    break;

如果你尝试手动退出主线程Looper,便会抛出如下异常

Caused by: java.lang.IllegalStateException: Main thread not allowed to quit.
    at android.os.MessageQueue.quit(MessageQueue.java:428)
    at android.os.Looper.quit(Looper.java:354)
    at com.jackie.testdialog.Test2Activity.onCreate(Test2Activity.java:29)
    at android.app.Activity.performCreate(Activity.java:7802)
    at android.app.Activity.performCreate(Activity.java:7791)
    at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1299)
    at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3245)
    at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3409) 
    at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:83) 
    at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:135) 
    at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95) 
    at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2016) 
    at android.os.Handler.dispatchMessage(Handler.java:107) 
    at android.os.Looper.loop(Looper.java:214) 
    at android.app.ActivityThread.main(ActivityThread.java:7356) 
    at java.lang.reflect.Method.invoke(Native Method) 
    at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492) 
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:930) 

为什么不允许退出呢,因为主线程不允许退出,一旦退出就意味着程序挂了,退出也不应该用这种方式退出

十一 、系统为什么不建议在子线程中访问UI?

这是因为 Android 的UI控件不是线程安全的,如果在多线程中并发访问可能会导致UI控件处于不可预期的状态,那么为什么系统不对UI控件的访问加上锁机制呢?缺点有两个:

  1. 首先加上锁机制会让UI访问的逻辑变得复杂
  2. 锁机制会降低UI访问的效率,因为锁机制会阻塞某些线程的执行。

所以最简单且高效的方法就是采用单线程模型来处理UI操作。(安卓开发艺术探索)

十二 、Android IdleHander 是否了解,是否使用过。

IdleHandler是一个回调接口,可以通过MessageQueue的addIdleHandler添加实现类。当MessageQueue中的任务暂时处理完了(没有新任务或者下一个任务延时在之后),这个时候会回调这个接口,返回false,那么就会移除它,返回true就会在下次message处理完了的时候继续回调。


    public static interface IdleHandler {
       
        boolean queueIdle();

    }

该方法返回一个boolean值,

如果为false则执行完毕之后移除这条消息(一次性完事),

如果为true则保留,等到下次空闲时会再次执行(重复执行)。

看MessageQueue的源码可以发现有两处关于IdleHandler的声明,分明是:

    1 存放IdleHandler的ArrayList(mIdleHandlers),

     2 还有一个IdleHandler数组(mPendingIdleHandlers)。

后面的数组,它里面放的IdleHandler实例都是临时的,也就是每次使用完(调用了queueIdle方法)之后,都会置空(mPendingIdleHandlers[i] = null)

下面看下MessageQueue的next()方法可以发现确实是这样

Message next() {
        ......
        for (;;) {
            ......
            synchronized (this) {
        // 此处为正常消息队列的处理
                ......
                if (mQuitting) {
                    dispose();
                    return null;
                }
                if (pendingIdleHandlerCount < 0
                        && (mMessages == null || now < mMessages.when)) {
                    pendingIdleHandlerCount = mIdleHandlers.size();
                }
                if (pendingIdleHandlerCount <= 0) {
                    // No idle handlers to run.  Loop and wait some more.
                    mBlocked = true;
                    continue;
                }
                if (mPendingIdleHandlers == null) {
                    mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)];
                }
                mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);
            }

            for (int i = 0; i < pendingIdleHandlerCount; i++) {
                final IdleHandler idler = mPendingIdleHandlers[i];
                mPendingIdleHandlers[i] = null; // release the reference to the handler
                boolean keep = false;
                try {
                    keep = idler.queueIdle();
                } catch (Throwable t) {
                    Log.wtf(TAG, "IdleHandler threw exception", t);
                }
                if (!keep) {
                    synchronized (this) {
                        mIdleHandlers.remove(idler);
                    }
                }
            }
            pendingIdleHandlerCount = 0;
            nextPollTimeoutMillis = 0;
        }
    }

就在MessageQueue的next方法里面。

流程是这样的:

  1. 如果本次循环拿到的Message为空,或者!这个Message是一个延时的消息而且还没到指定的触发时间,那么,就认定当前的队列为空闲状态,
  2. 接着就会遍历mPendingIdleHandlers数组(这个数组里面的元素每次都会到mIdleHandlers中去拿)来调用每一个IdleHandler实例的queueIdle方法,
  3. 如果这个方法返回false的话,那么这个实例就会从mIdleHandlers中移除,也就是当下次队列空闲的时候,不会继续回调它的queueIdle方法了。

处理完IdleHandler后会将nextPollTimeoutMillis设置为0,也就是不阻塞消息队列,当然要注意这里执行的代码同样不能太耗时,因为它是同步执行的,如果太耗时肯定会影响后面的message执行。

其他可以使用的地方比如:

1.Activity启动优化:onCreate,onStart,onResume中耗时较短但非必要的代码可以放到IdleHandler中执行,减少启动时间

2.想要在一个View绘制完成之后添加其他依赖于这个View的View,当然这个用View#post()也能实现,区别就是前者会在消息队列空闲时执行

3一些第三方库中有使用,比如LeakCanary,Glide中有使用到。

至此Handler面试常问的分析完的了。

 

以上是关于Android的Handler的常见面试问题总结的主要内容,如果未能解决你的问题,请参考以下文章

Android Handler面试总结

Android大牛近期大厂面试详解(附解答)

Android开发面试常见的 Handler 相关面试题,你能答上几题?

Android面试:Handler八大问题汇总

Android常见原理性面试题

Android岗常见40道面试题,面试前必须了解的知识点