关于Handler的面试专题

Handler老生常谈,总感觉会用,搞清楚了,但是呢,又总感觉缺了下什么,记录下。好记性不如烂键盘。

一、Handler源码吃透

首先,我们需要确定前提的是一个Thread线程只有一个Looper,一个MessageQueue,多个Handler对象。

Handler机制的整体架构类似于一个传送带装置。


Handler架构图.png
  • Handler:类似于给传送带放置货物(sendMessage()),从传送带上取货物(处理货物)(dispatchMessage())
  • Message:传送带上的货物
  • Looper:使传送带循环转起来的电机(Looper.loop())
  • MessageQueue:传送带
1.1 Handler

在一个线程中可以有很多个Handler,它们都可以发送Message,发送的的话,有很多的sendMessage方法,最终是由MessageQueue.enqueueMessage处理。并在处理时,由他们各自的handleMessage()函数处理。

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

 private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,
            long uptimeMillis) {
        msg.target = this;
        msg.workSourceUid = ThreadLocalWorkSource.getUid();

        if (mAsynchronous) {
            msg.setAsynchronous(true);
        }
        return queue.enqueueMessage(msg, uptimeMillis);
    }
1.2 Message
  • Message是一个单向链表结构的实体类,保持了很多信息。
  • 它有一个next变量指向下一个MessageZ节点,并保持着一个缓存池。当被释放时,调用它的recycle()函数。但是它并不会被释放,而是缓存起来,等待下一次调用obtain()方法时,重新赋值使用。
  • 另外,它的target变量将会持有外部Handler,这也是内存泄漏的主要因素。
    public void recycle() {
        ...
        recycleUnchecked();
    }

  void recycleUnchecked() {
        what = 0;
       ...
        data = null;

        synchronized (sPoolSync) {
            if (sPoolSize < MAX_POOL_SIZE) {
                next = sPool;
                sPool = this;
                sPoolSize++;
            }
        }
    }

    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();
    }
1.3 MessageQueue

它通过enqueueMessage()方法来将Message对象按时间when的先后加入链表中,通过next()方法从链表中取出Message。它始终有一个mMessage指向链表头结点。

boolean enqueueMessage(Message msg, long when) {
        if (msg.target == null) {
            throw new IllegalArgumentException("Message must have a target.");
        }
        if (msg.isInUse()) {
            throw new IllegalStateException(msg + " This message is already in use.");
        }

        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 {
                // 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;
    }
1.4 Looper

每个Thread线程只有一个Looper对象(它是有ThreadLocal来保证的。),而每个Looper对象也只有一个MessageQueue。

  • Looper.prepareMainLooper()来创建主线程的Looper对象
    public static void prepareMainLooper() {
        prepare(false);
        synchronized (Looper.class) {
            if (sMainLooper != null) {
                throw new IllegalStateException("The main Looper has already been prepared.");
            }
            sMainLooper = myLooper();
        }
    }
  • Looper.prepare()创建子线程中的Looper对象,先从ThreadLocal中获取,有就抛出异常,一个线程只能创建一个。没有就会实例化一个Looper对象,并set到ThreadLocal中。
  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.loop()开启死循环来,不断遍历MessageQueue看是否有消息,有就通过msg.target(实际就是Handler)来dispatchMessage()处理消息,最后回收Message。
    public static void loop() {
        final Looper me = myLooper();
        ...
        final MessageQueue queue = me.mQueue;
        ...
        for (;;) {
            Message msg = queue.next(); 
            ...
            try {
                msg.target.dispatchMessage(msg);
               ...
            } catch (Exception exception) {
               ...
            }

            msg.recycleUnchecked();
        }
    }

二、Looper死循环为什么不会导致应用卡死?

基于前面我们已经知道了,App的启动流程,最终会Zygote进程孵化出App进程。而ActivityThread就是App的进程所在,在它的main()方法中,实例化了一个主线程的Looper对象,并将其存储在主线程的ThreadLocal.ThreadLocalMap中。
我们知道,如果main()方法执行完,那么意味着进程就结束了。但是这是App进程,我们想要它结束吗?Android并不想让App进程退出,所以这里Looper.loop()死循环阻塞也是这个作用,保持App进程。
而Android是以事件为驱动的系统,但没有事件来时,就应该去展示静态的界面。

    public static void main(String[] args) {
        ...
        Looper.prepareMainLooper();
        ....
        Looper.loop();
    }

三、使用Handler的postDelay消息队列有什么变化?

    1. Hanlder的postDelayed()有三个重载方法,它们都会去调用sendMessageDelayed()方法。
    public final boolean postDelayed(@NonNull Runnable r, long delayMillis) {
        return sendMessageDelayed(getPostMessage(r), delayMillis);
    }
    1. 在sendMessageDelayed()它将延迟时间与系统启动时间相加,作为一个消息需要处理的时间。通过这个时间值插入到MessageQueue中。
public final boolean sendMessageDelayed(@NonNull Message msg, long delayMillis) {
        if (delayMillis < 0) {
            delayMillis = 0;
        }
        return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
    }

四、如何保证多个Handler线程安全?

一个线程只有一个Looper和一个MessageQueue实例,可以有多个Handler对象。在MessageQueue中像enqueueMessage()、next()方法都加了synchronized(this)同步锁,保证线程安全。

五、Message如何创建?哪种方式更好?

    1. 使用new Message()方法创建
    1. 使用Message.obtain()方式创建
      我们知道在系统中可能会发送无数个Message,使用第一种方式的话会导致频繁创建和频繁销毁,这将导致内存抖动。所以Handler中推进使用第二种方式来创建,它优先使用缓冲池中的Message,如果缓冲池没有可用的才回去实例化一个message。但Message对象被Handler处理完后,不是直接销毁,而是将其重置,并缓存到缓存池中,以供下一次使用。
    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();
    }

    void recycleUnchecked() {
        // Mark the message as in use while it remains in the recycled object pool.
        // Clear out all other details.
        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++;
            }
        }
    }

六、关于ThreadLocal,你的理解是?

    1. ThreadLocal在Handler体系中用来保证线程中只有一个Looper对象,在调用Looper.prepare()方法时,会先从sThreadLocal.get()中获取,如果能够获取到,会抛出异常,获取不到才会实例化一个Looper对象,并通过ThreadLocal.set()方法保存起来。
    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));
    }
    1. ThreadLocal这种机制原理,并不是ThreadLocal自己实现的,而是通过它的内部类ThreadLocalMap来实现,它是一个Map,以ThreadLocal为Key,泛型(T)为值。这里就是Looper对象为值了。
public class ThreadLocal {
      static class ThreadLocalMap {
        static class Entry extends WeakReference> {
            Entry(ThreadLocal k, Object v) {
                super(k);
                value = v;
            }
        }

        private Entry[] table;
  }
}
    1. 而每个Thread线程中都会有一个ThreadLocal.ThreadLocalMap的变量,所以当调用ThreadLocal.get()方法时,都会去获取当前线程,并获取它的ThreadLocal.ThreadLocalMap对象,再从中以ThreadLocal为key来获取Looper
Thread.java
class Thread implements Runnable {
  ThreadLocal.ThreadLocalMap threadLocals = null;
}
ThreadLocal.java
public class ThreadLocal {
   public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                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);
    }
}

七、为什么不能在子线程更新UI?

举个例子:TextView.setText() --> TextView.checkForRelayout() ---> View.requestLayout() ---> ViewRootImpl.requestLayout() ---> ViewRootImpl.checkThread()
每次都需要检测当前线程是否是主线程,主线程至于是哪里?什么时间设置的需要另外追踪?

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

解惑:
1、为什么子线程不能操作UI

当一个线程第一次启动时,Android同时会启动一个对应的主线程,这个主线程就是UI线程(ActivityThread)。UI线程负责处理和UI相关的事件,如用户按键点击、屏幕触摸等。系统不会为每个组件都单独创建一个线程,在同一进程中UI组件都会在UI线程中实例化。系统对每个组件调用都是从UI线程分发出去的。所以响应系统回调的方法永远都是在UI线程里运行。如onKeyDown()的回调

  1. 那为什么选择一个主线程干这些活呢?换个说法,Android为什么使用单线程模型,它有什么好处?

现代GUI线程就是使用了单线程模型(采用一个专门的线程从队里中抽取事件,并把它们转发给应用程序定义的事件处理器)。单线程化不单单存在于Android中,Qt、XWindows都是单线程化。当然,也有人试图用多线程的GUI,最终由于竞争条件和死锁导致的稳定性问题等。又回到了单线程化的事件队列模型上来。单线程模型通过限制来达到线程安全。

  1. 在子线程中更新UI抛出异常的原因?(ViewRootImpl构造方法会初始化ViewRoot的mThread,更新Ui时会对比mThread和Thread.currentThread())
ViewRootImpl.java
public ViewRootImpl(Context context, Display display) {
    ...
    mThread = Thread.currentThread();
    ......
}

   void checkThread() {
        if (mThread != Thread.currentThread()) {
            throw new CalledFromWrongThreadException(
                    "Only the original thread that created a view hierarchy can touch its views.");
        }
    }
  1. 非UI线程是可以刷新UI的,前提是它要拥有自己的ViewRootImpl。可以通过WindowManager.addView()来实现,在WindowManagerImpl.addView() ---> WindowManagerGlobal.addView()内部将会实例化ViewRootImpl
class NonUiThread extends Thread{
      @Override
      public void run() {
         Looper.prepare();
         TextView tx = new TextView(MainActivity.this);
         tx.setText("non-UiThread update textview");
 
         WindowManager windowManager = MainActivity.this.getWindowManager();
         WindowManager.LayoutParams params = new WindowManager.LayoutParams(
             200, 200, 200, 200, WindowManager.LayoutParams.FIRST_SUB_WINDOW,
                 WindowManager.LayoutParams.TYPE_TOAST,PixelFormat.OPAQUE);
         windowManager.addView(tx, params); 
         Looper.loop();
     }
 }
  1. 那么主线程什么时候创建的ViewRootImpl呢?本把主线程赋值的?实在onResume()的时候,对应到ActivityThread.handleResumeActivity()方法。
ActivityThread.java
final void handleResumeActivity(IBinder token,
            boolean clearHide, boolean isForward, boolean reallyResume) {
       ...
        ActivityClientRecord r = performResumeActivity(token, clearHide);
        ......
        if (r.window == null && !a.mFinished && willBeVisible) {
            r.window = r.activity.getWindow();
            View decor = r.window.getDecorView();
            decor.setVisibility(View.INVISIBLE);
            ViewManager wm = a.getWindowManager();
            WindowManager.LayoutParams l = r.window.getAttributes();
            a.mDecor = decor;
            l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
            l.softInputMode |= forwardBit;
            if (a.mVisibleFromClient) {
                a.mWindowAdded = true;
                wm.addView(decor, l);
            }
    ......
}

八、子线程中维护Looper在消息队列无消息的时候处理方案?

子线程中我们创建了Handler,并调用了Looper.prepare()和loop()。一旦调用loop()就开启了阻塞。如果消息队列无消息时,子线程仍然会阻塞,只有我们手动调用了子线程中Looper.quit()后才会解除阻塞,往后执行。还可以释放Message内存。

new Thread(){
      public void run() {
        Looper.prepare();
        new Handler(){
          ...
        }
        Looper.loop();
    }
}
    1. 看到loop()函数中只有msg==null的时候才会return,解除阻塞。
Looper.java
public static void loop() {
        for (;;) {
            Message msg = queue.next(); // might block
            if (msg == null) {
                // No message indicates that the message queue is quitting.
                return;
            }
            ...
      }  
}
    1. 当消息队列无消息时,nativePollOnce()会阻塞等待下一个消息到来,如果调用了quit()就会唤醒return null。
  Message next() {
        ...
        for (;;) {
            ...
            nativePollOnce(ptr, nextPollTimeoutMillis);
            synchronized (this) {
                ...
                 if (mQuitting) {
                    dispose();
                    return null;
                }
                ...
          }
      }
}
    1. 当我们手动调用子线程Looper.quit()时,它调用的是MessageQueue.quit()。首先,主线程的Looper不允许退出;其次,它会移除和释放MessageQueue中的message内存;另外注意到nativeWake它将通知唤醒MesageQueue.next()的阻塞,往后执行,返回null。
    void quit(boolean safe) {
        if (!mQuitAllowed) {
            throw new IllegalStateException("Main thread not allowed to quit.");
        }

        synchronized (this) {
           ...
            if (safe) {
                removeAllFutureMessagesLocked();
            } else {
                removeAllMessagesLocked();
            }

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

九、什么事epoll机制

    1. Handler消息框架队列需要解决时间排序问题和阻塞问题。
    1. 事件不仅仅是应用层的东西,底层,驱动层都有事件,所以单单依靠java层面的队列如BlockingQueue无法解决
    1. 消息I/O有阻塞I/O和非阻塞I/O:阻塞I/O是一堆事件得一个一个挨个处理,这种不可忍。非阻塞I/O每个事件和事件处理器都类似有单独通道连接处理。
    1. Android系统事件和App事件处理类似C/S模型,C/S模型通信可以用Socket来解决,Socket采用的是select模型。
    1. Select模型(非阻塞I/O)可以处理非阻塞,但是它需要轮询来获取对应的事件处理器(这里对应哪个App来处理事件),另外它的事件需要拷贝。
    1. epoll机制(异步I/O),Looper.prepare()--> new MessageQueue() ---> nativeInit()时调用底层的epoll_create()函数为每个App添加文件描述符,并将其添加到B+树(红黑树)中,但调用Looper.loop()开启死循环时,MessgeQueue.next()就会调用nativePollOnce()来阻塞,底层为epoll_wait()。当有事件(epoll_ctl)来时,事件携带了文件描述符,就可以通过红黑树快速定位哪个App处理事件。并将其加入缓冲队列中,等待一个一个处理分发。


      Android事件响应模型
epoll B+树
epoll底层逻辑
Handler java-底层调用关系

十、一个线程有几个looper? 如何保证,又可以有几个Handler

一个线程只有一个Looper和一个messageQueue,通过ThreadLocal保证。可以有多个Handler.

十一、handler内存泄漏的原因,其他内部类为什么没有这个问题

    1. 根本原因:长生命周期对象持有短生命周期对象
    1. 内部类会持有外部类的引用,GCRoot链: MainActivity --> Handler ---> Message ---> MessageQueue ---> Looper ---> Thread
    1. 解决内存泄漏: 本质是断开GCRootl链,1:将Handler定义为static,它将不在持有外部类引用。 2:Activity onDestroy()时,Handler.removeCallbacksAndMessages移除所有的Message,因为Message持有Handler。

十二、为什么主线程可以new Handler 其他子线程可以吗 怎么做?

    1. Handler机制需要有四个要素:Looper,MessageQueue,Message,Handler。实例化Handler之间需要先实例化Looper,不然会抛出异常。它需要执行在prepare()和loop()之间
  public Handler(@Nullable Callback callback, boolean async) {
        ...
        mLooper = Looper.myLooper();
        if (mLooper == null) {
            throw new RuntimeException(
                "Can't create handler inside thread " + Thread.currentThread()
                        + " that has not called Looper.prepare()");
        }
        mQueue = mLooper.mQueue;
        ...
    }
    1. 之前我们说过在ActivityThread的main方法中,系统已经帮我们调用了prepareMainLooper()实例化了主线程的Looper对象,并且调用了loop()。主线程中所有代码都执行在它们俩之间。
ActivityThread.java
    public static void main(String[] args) {
        Looper.prepareMainLooper();
        ...
        Looper.loop();
    }
    1. 子线程如果我们需要new Handler(),则需要先Looper.prepare()创建子线程Looper对象。并且如果子线程需要退出的话,需要手动调用Looper.quit()方法。
new Thread(){
      public void run() {
        Looper.prepare();
        new Handler(){
          ...
        }
        Looper.loop();
    }
}

十三、Handler中的生产者-消费者设计模式你理解不?

Handler架构图.png

你可能感兴趣的:(关于Handler的面试专题)