Handler面试题总结

1、一个线程有几个Handler?
一个线程有任意个Handler,可以new多个Handler,但最终同一线程多个Handler发的消息都在同一个Looper去处理。

2、一个线程有几个Looper?如何保证?
一个线程只有一个Looper。
首先看下Looper中有这么一个static final变量

//Looper.java
static final ThreadLocal sThreadLocal = new ThreadLocal();

会去new一个ThreadLocal对象,ThreadLocal是一个线程上下文变量,用于存储线程上下文。而在ThreadLocal中会有一个静态内部类,ThreadLocalMap。并且在Thread里面

//Thread.java
ThreadLocal.ThreadLocalMap threadLocals = null;

取到threadLocals是一个HashMap,HashMap是一个键值对,key-value是一一对应的,所以在thread中只有一个threadLocals。
在Looper.prepare时,调用sThreadLocal.set(new Looper()),set里面是这样的

    //ThreadLocal.java
    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

获取当前线程,并且取到当前线程的ThreadLocalMap,也就是刚才说的threadLocals。并set一个键值对,并且是一一对应的。而且如果sThreadLocal对应有唯一值后,不能再被set替换,这就是为什么只能prepare一次。所以,一个sThreadLocal对应一个Looper。而sThreadLocal是static final变量,唯一的,所以一个线程中只有一个Looper。

3、Handler内存泄漏的原因?为什么其他的内部类没有说过这个问题?
首先Handler是匿名内部类,持有外部类的引用。再来看看MessageQueue中有大量的Message,而在发送Message时候把Handler与Message绑定。所以,Message中有持有Handler,Handler持有外部类的引用,因此Handler持有Activity的引用,Activity中有大量的内存。当Message是延迟消息时,Activity走了onDestroy(),导致Activity无法被释放回收。
解决问题是:在onDestroy里面清除MessageQueue中所有的Message;将handler声明为静态内部类,用软引用的或者弱引用持有Activity。

4、不断往MessageQueue里面放消息,为什么不会导致OOM?
首先,OOM的原因是找不到一个连续的想要大小的空间(有空间但是没有连续的想要大小的空间),而Message的空间是非常小的,只会导致手机内存空间不足,不会导致OOM。

5、为什么主线程可以new Handler?如果想要在子线程中new Handler要做哪些工作?
主线程在ActivityThread的main()中就做了new Handler的准备工作了;而在子线程new Handler时候,一般都会这么做:

class MyThread : Thread() {
    private var mLooper:Looper? = null
    override fun run() {
        Looper.prepare()
        mLooper = Looper.myLooper()
        Looper.loop()
    }
}
//
val myThread = MyThread()
myThread.start()
val handler = myThread.mLooper?.let { Handler(it) }

看上去没啥问题,但这样做有一个问题,因为Thread是异步线程,不能保证mLooper是取到值的。所以在子线程里面new Handler时候,要通过synchronized、wait和notifyAll来配合使用,这篇文章有讲到线程间通信。或者可以看看HandlerThread,就是这么实现的。

6、子线程中维护Looper,消息队列无消息的时候的处理方案是什么?有什么用?主线程呢?
假如Looper处理完后,没有退出,会导致子线程没有消息时候一直存在,导致内存泄漏。解决办法是调用MessageQueue中的quit

    //MessageQueue.java
    void quit(boolean safe) {
        if (!mQuitAllowed) {
            throw new IllegalStateException("Main thread not allowed to quit.");
        }

        synchronized (this) {
            if (mQuitting) {
                return;
            }
            mQuitting = true;

            if (safe) {
                removeAllFutureMessagesLocked();
            } else {
                removeAllMessagesLocked();
            }

            // We can assume mPtr != 0 because mQuitting was previously false.
            nativeWake(mPtr);
        }
    }

把mQuitting设为true,然后把消息队列的消息全部清空,再唤醒线程。

    //MessageQueue.java
    Message next() {
    ···
     if (mQuitting) {
       dispose();
       return null;
     }
    ···
    }

返回null,Looper.loop()中取到null,退出死循环,loop循环结束,导致线程结束,线程结束内存释放。
主线程:主线程不能quit,主线程quit会报异常"Main thread not allowed to quit.",为什么?因为几乎从 App 启动,到 Activity 相关的生命周期,Services 相关的生命周期,到 App 退出等等,都是通过 Handler 来驱动的,如果把主线程的Looper退出死循环,那么后续将无法正常工作。

7、既然可以存在多个Handler往MessageQueue中添加数据(发消息时各个Handler可能处于不用线程),那它内部是如何确保线程安全的?取消息呢?
在MessageQueue中,enqueueMessage是存消息的操作,其中有synchronized来保证每次只有一个Handler来操作enqueueMessage的过程:

//MessageQueue.java
boolean enqueueMessage(Message msg, long when) {
···
  synchronized (this) {
  ···
  }
···
}

而取消息呢,是通过next来取消息,同样里面有synchronized,但是取消息是只有在Looper.loop的时候去取,为什么要加synchronized?

//MessageQueue.java
Message next() {
···
  synchronized (this) {
  ···
  }
···
}

因为存消息的时候也用了synchronized,并且用同一个this作为监视器,所以操作的是同一个队列时,要保证存取操作不能同时发生。HashMap之所以线程不安全,是因为操作同一个链表时,存和取的操作同时发生。

8、使用Message时如何去创建?为什么?
Message中有一个消息池,使用完后的消息不会被GC回收,而是都会把消息内容都清除,并放进消息池中,可以被复用。这样做可以减少new Message的过程。每new Message,就会有一块内存分配给它;每分配一块内存,程序的可用内存也就少一块;当程序被占用的内存达到一定临界程度,GC 也就是垃圾回收器(Garbage Collector)就会出动,来释放掉一部分不再被使用的内存。被回收后,虽然内存空间变大了,但可能找不到一块连续容量大的内存,会出现OOM问题;而且,Android本身是基于Handler来处理消息的,假如每次都是new Message,在1S内会new上百个上千个Message,就会频繁的调用GC,导致内存抖动,而内存抖动会导致卡顿。

9、Handler的消息阻塞是怎么实现的?为什么主线程不会阻塞?
ANR产生的原因是Message没能在确定的时间内处理完成,而Looper是不管Message有没有处理完,只要MessageQueue里面有消息,那么就会出来去给Handler处理,当没有消息时,Looper会阻塞挂起。Looper的阻塞是因为没消息,所以ANR和Looper是死循环是两码事,没有关系。所以应用没有消息的时候,会处于睡眠状态,cpu会释放资源,而ANR是消息没及时得到处理。

最后,附上Handler源码解析。

你可能感兴趣的:(Handler面试题总结)