Android中handler問題彙總

Hanking發表於2020-04-04

前言

handler機制幾乎是Android面試時必問的問題,雖然看過很多次handler原始碼,但是有些面試官問的問題卻不一定能夠回答出來,趁著機會總結一下面試中所覆蓋的handler知識點。

1、講講 Handler 的底層實現原理?

下面的這幅圖很完整的表現了整個handler機制。

在這裡插入圖片描述
要理解handler的實現原理,其實最重要的是理解Looper的實現原理,Looper才是實現handler機制的核心。任何一個handler在使用sendMessage或者post時候,都是先構造一個Message,並把自己放到message中,然後把Message放到對應的Looper的MessageQueue,Looper通過控制MessageQueue來獲取message執行其中的handler或者runnable。 要在當前執行緒中執行handler指定操作,必須要先看當前執行緒中有沒有looper,如果有looper,handler就會通過sendMessage,或者post先構造一個message,然後把message放到當前執行緒的looper中,looper會在當前執行緒中迴圈取出message執行,如果沒有looper,就要通過looper.prepare()方法在當前執行緒中構建一個looper,然後主動執行looper.loop()來實現迴圈。

梳理一下其實最簡單的就下面四條:

1、每一個執行緒中最多隻有一個Looper,通過ThreadLocal來儲存,Looper中有Message佇列,儲存handler並且執行handler傳送的message。

2、線上程中通過Looper.prepare()來建立Looper,並且通過ThreadLocal來儲存Looper,每一個執行緒中只能呼叫一次Looper.prepare(),也就是說一個執行緒中最多隻有一個Looper,這樣可以保證執行緒中Looper的唯一性。

3、handler中執行sendMessage或者post操作,這些操作執行的執行緒是handler中Looper所在的執行緒,和handler在哪裡建立沒關係,和Handler中的Looper在那建立有關係。

4、一個執行緒中只能有一個Looper,但是一個Looper可以對應多個handler,在同一個Looper中的訊息都在同一條執行緒中執行。

2、Handler機制,sendMessage和post(Runnable)的區別?

要看sendMessage和post區別,需要從原始碼來看,下面是幾種使用handler的方式,先看下這些方式,然後再從原始碼分析有什麼區別。 例1、 主執行緒中使用handler

//主執行緒
        Handler mHandler = new Handler(new Handler.Callback() {
            @Override
            public boolean handleMessage(@NonNull Message msg) {
                if (msg.what == 1) {
                    //doing something
                }
                return false;
            }
        });
        Message msg = Message.obtain();
        msg.what = 1;
        mHandler.sendMessage(msg);
複製程式碼

上面是在主執行緒中使用handler,因為在Android中系統已經在主執行緒中生成了Looper,所以不需要自己來進行looper的生成。如果上面的程式碼在子執行緒中執行,就會報

Can't create handler inside thread " + Thread.currentThread()
                        + " that has not called Looper.prepare()
複製程式碼

如果想著子執行緒中處理handler的操作,就要必須要自己生成Looper了。

例2 、子執行緒中使用handler

        Thread thread=new Thread(new Runnable() {
            @Override
            public void run() {
                Looper.prepare();
                Handler handler=new Handler();
                handler.post(new Runnable() {
                    @Override
                    public void run() {
                        
                    }
                });
                Looper.loop();
            }
        });
複製程式碼

上面在Thread中使用handler,先執行Looper.prepare方法,來在當前執行緒中生成一個Looper物件並儲存在當前執行緒的ThreadLocal中。 看下Looper.prepare()中的原始碼:

//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
    private Looper(boolean quitAllowed) {
        mQueue = new MessageQueue(quitAllowed);
        mThread = Thread.currentThread();
    }

複製程式碼

可以看到prepare方法中會先從sThreadLocal中取如果之前已經生成過Looper就會報錯,否則就會生成一個新的Looper並且儲存線上程的ThreadLocal中,這樣可以確保每一個執行緒中只能有一個唯一的Looper。

另外:由於Looper中擁有當前執行緒的引用,所以有時候可以用Looper的這種特點來判斷當前執行緒是不是主執行緒。

    @RequiresApi(api = Build.VERSION_CODES.KITKAT)
    boolean isMainThread() {
        return Objects.requireNonNull(Looper.myLooper()).getThread() == Looper.getMainLooper().getThread();
    }
複製程式碼

sendMessage vs post

先來看看sendMessage的程式碼呼叫鏈:

在這裡插入圖片描述
enqueueMessage原始碼如下:

    private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,
            long uptimeMillis) {
        msg.target = this;
        msg.workSourceUid = ThreadLocalWorkSource.getUid();
        return queue.enqueueMessage(msg, uptimeMillis);
    }
複製程式碼

enqueueMessage的程式碼處理很簡單,msg.target = this;就是把當前的handler物件給message.target。然後再講message進入到佇列中。

post程式碼呼叫鏈:

在這裡插入圖片描述
呼叫post時候會先呼叫getPostMessage生成一個Message,後面和sendMessage的流程一樣。下面看下getPostMessage方法的原始碼:

    private static Message getPostMessage(Runnable r) {
        Message m = Message.obtain();
        m.callback = r;
        return m;
    }
複製程式碼

可以看到getPostMessage中會先生成一個Messgae,並且把runnable賦值給message的callback.訊息都放到MessageQueue中後,看下Looper是如何處理的。

    for (;;) {
        Message msg = queue.next(); // might block
        if (msg == null) {
            return;
        }
        msg.target.dispatchMessage(msg);
    }
複製程式碼

Looper中會遍歷message列表,當message不為null時呼叫msg.target.dispatchMessage(msg)方法。看下message結構:

在這裡插入圖片描述
也就是說msg.target.dispatchMessage方法其實就是呼叫的Handler中的dispatchMessage方法,下面看下dispatchMessage方法的原始碼:

    public void dispatchMessage(@NonNull Message msg) {
        if (msg.callback != null) {
            handleCallback(msg);
        } else {
            if (mCallback != null) {
                if (mCallback.handleMessage(msg)) {
                    return;
                }
            }
            handleMessage(msg);
        }
    }
//
 private static void handleCallback(Message message) {
        message.callback.run();
    }
複製程式碼

因為呼叫post方法時生成的message.callback=runnable,所以dispatchMessage方法中會直接呼叫 message.callback.run();也就是說直接執行post中的runnable方法。 而sendMessage中如果mCallback不為null就會呼叫mCallback.handleMessage(msg)方法,否則會直接呼叫handleMessage方法。

總結 post方法和handleMessage方法的不同在於,post的runnable會直接在callback中呼叫run方法執行,而sendMessage方法要使用者主動重寫mCallback或者handleMessage方法來處理。

3、Looper會一直消耗系統資源嗎?

首先給出結論,Looper不會一直消耗系統資源,當Looper的MessageQueue中沒有訊息時,或者定時訊息沒到執行時間時,當前持有Looper的執行緒就會進入阻塞狀態。

在這裡插入圖片描述
下面看下looper所在的執行緒是如何進入阻塞狀態的。looper阻塞肯定跟訊息出隊有關,因此看下訊息出隊的程式碼。

訊息出隊

   Message next() {
        // Return here if the message loop has already quit and been disposed.
        // This can happen if the application tries to restart a looper after quit
        // which is not supported.
        final long ptr = mPtr;
        if (ptr == 0) {
            return null;
        }
        int nextPollTimeoutMillis = 0;
        for (;;) {
            if (nextPollTimeoutMillis != 0) {
                Binder.flushPendingCommands();
            }
            nativePollOnce(ptr, nextPollTimeoutMillis);
            // While calling an idle handler, a new message could have been delivered
            // so go back and look again for a pending message without waiting.
           	if(hasNoMessage)
           	{
           	nextPollTimeoutMillis =-1;
           	}
        }
    }
複製程式碼

上面的訊息出隊方法被簡寫了,主要看下面這段,沒有訊息的時候nextPollTimeoutMillis=-1;

 	if(hasNoMessage)
           	{
           	nextPollTimeoutMillis =-1;
           	}
複製程式碼

看for迴圈裡面這個欄位所其的作用:

 if (nextPollTimeoutMillis != 0) {
                Binder.flushPendingCommands();
            }
  nativePollOnce(ptr, nextPollTimeoutMillis);
複製程式碼

Binder.flushPendingCommands();這個方法的作用可以看原始碼裡面給出的解釋:

    /**
     * Flush any Binder commands pending in the current thread to the kernel
     * driver.  This can be
     * useful to call before performing an operation that may block for a long
     * time, to ensure that any pending object references have been released
     * in order to prevent the process from holding on to objects longer than
     * it needs to.
     */
複製程式碼

也就是說在使用者執行緒要進入阻塞之前跟核心執行緒傳送訊息,防止使用者執行緒長時間的持有某個物件。再看看下面這個方法: nativePollOnce(ptr, nextPollTimeoutMillis);當nextPollingTimeOutMillis=-1時,這個native方法會阻塞當前執行緒,執行緒阻塞後,等下次有訊息入隊才會重新進入可執行狀態,所以Looper並不會一直死迴圈消耗執行記憶體,對佇列中的顏色訊息還沒到時間時也會阻塞當前執行緒,但是會有一個阻塞時間也就是nextPollingTimeOutMillis>0的時間。

當訊息佇列中沒有訊息的時候looper肯定是被訊息入隊喚醒的。

訊息入隊

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;
    }
複製程式碼

上面可以看到訊息入隊之後會有一個

  if (needWake) {
                nativeWake(mPtr);
            }
複製程式碼

方法,呼叫這個方法就可以喚醒執行緒了。另外訊息入隊的時候是根據訊息的delay時間來在連結串列中排序的,delay時間長的排在後面,時間短的排在前面。如果時間相同那麼按插入時間先後來排,插入時間早的在前面,插入時間晚的在後面。

4、android的Handle機制,Looper關係,主執行緒的Handler是怎麼判斷收到的訊息是哪個Handler傳來的?

Looper是如何判斷Message是從哪個handler傳來的呢?其實很簡單,在1中分析過,handler在sendMessage的時候會構建一個Message物件,並且把自己放在Message的target裡面,這樣的話Looper就可以根據Message中的target來判斷當前的訊息是哪個handler傳來的。

5、Handler機制流程、Looper中延遲訊息誰來喚醒Looper?

從3中知道在訊息出隊的for迴圈佇列中會呼叫到下面的方法。

nativePollOnce(ptr, nextPollTimeoutMillis);
複製程式碼

如果是延時訊息,會在被阻塞nextPollTimeoutMillis時間後被叫醒,nextPollTimeoutMillis就是訊息要執行的時間和當前的時間差。

6、Handler是如何引起記憶體洩漏的?如何解決?

在子執行緒中,如果手動為其建立Looper,那麼在所有的事情完成以後應該呼叫quit方法來終止訊息迴圈,否則這個子執行緒就會一直處於等待的狀態,而如果退出Looper以後,這個執行緒就會立刻終止,因此建議不需要的時候終止Looper。

Looper.myLooper().quit()
複製程式碼

那麼,如果在Handler的handleMessage方法中(或者是run方法)處理訊息,如果這個是一個延時訊息,會一直儲存在主執行緒的訊息佇列裡,並且會影響系統對Activity的回收,造成記憶體洩露。

具體可以參考Handler記憶體洩漏分析及解決

總結一下,解決Handler記憶體洩露主要2點

1 、有延時訊息,要在Activity銷燬的時候移除Messages

2、 匿名內部類導致的洩露改為匿名靜態內部類,並且對上下文或者Activity使用弱引用。

7、handler機制中如何確保Looper的唯一性?

Looper是儲存線上程的ThreadLocal裡面的,使用Handler的時候要呼叫Looper.prepare()來建立一個Looper並放在當前的執行緒的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));
    }
複製程式碼

可以看到,如果多次呼叫prepare的時候就會報Only one Looper may be created per thread,所以這樣就可以保證一個執行緒中只有唯一的一個Looper。

8、Handler 是如何能夠執行緒切換,傳送Message的?

handler的執行跟建立handler的執行緒無關,跟建立looper的執行緒相關,加入在子執行緒中建立一個Handler,但是Handler相關的Looper是主執行緒的,這樣,如果handler執行post一個runnable,或者sendMessage,最終的handle Message都是在主執行緒中執行的。

        Thread thread=new Thread(new Runnable() {
            @Override
            public void run() {
                Looper.prepare();
                Handler handler=new Handler(getMainLooper());
                handler.post(new Runnable() {
                    @Override
                    public void run() {
                        Toast.makeText(MainActivity.this,"hello,world",Toast.LENGTH_LONG).show();
                    }
                });
                Looper.loop();
            }
        });
        thread.start();
複製程式碼

參考文獻

1、www.jianshu.com/p/ea7beaeee…

相關文章