Handler系列面試題:如何深挖原理進大廠?

1.簡述Handler的實現原理

Android 應用是經過消息驅動運行的,在 Android 中一切皆消息,包括觸摸事件,視圖的繪製、顯示和刷新等等都是消息。Handler 是消息機制的上層接口,平時開發中咱們只會接觸到 Handler 和 Message,內部還有 MessageQueue 和 Looper 兩大助手共同實現消息循環系統。 java

(1)Handler 經過Handler的sendXXX或者postXXX來發送一個消息,這裏要注意post(Runnable r)方法也會將Runnable包裝成一個Message,代碼以下:git

public final boolean post(Runnable r){
    	return  sendMessageDelayed(getPostMessage(r), 0);
    }
    public final boolean postDelayed(Runnable r, long delayMillis){
    	return sendMessageDelayed(getPostMessage(r), delayMillis);
    }
private static Message getPostMessage(Runnable r) {
        Message m = Message.obtain();
        m.callback = r;
        return m;
    }
複製代碼

從代碼中能夠看到將Runnable賦值給了Message.callback了。最終sendXXX和postXXX都會調用到sendMessageAtTime,代碼以下:github

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);
    }
複製代碼

在這個方法中最終調用了enqueueMessage方法,這裏注意將this賦值給了Message.target,而此處this就是Handler。web

private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
        msg.target = this;
        if (mAsynchronous) {
            msg.setAsynchronous(true);
        }
        //轉到 MessageQueue 的 enqueueMessage 方法
        return queue.enqueueMessage(msg, uptimeMillis);
    }
複製代碼

enqueueMessage方法最終調用了MessageQueue的enqueueMessage方法,將消息放入隊列。  (2)MessageQueue MessageQueue是一個優先級隊列,核心方法是enqueueMessage和next方法,也就是將插入隊列,將消息取出隊列的操做。 之因此說MessageQueue是一個優先級隊列是由於enqueueMessage方法中會根據Message的執行時間來對消息插入,這樣越晚執行的消息會被插入到隊列的後邊。安全

而next方法是一個死循環,若是隊列中有消息,則next方法會將Message移除隊列並返回該Message,若是隊列中沒有消息該方法則會處於阻塞狀態。markdown

(3)Looper Looper能夠理解爲一個消息泵,Looper的核心方法是loop。注意loop方法的第一行會首先經過myLooper來獲得當前線程的Looper,接着拿到Looper中的MessageQueue,而後開啓一個死循環,它會不斷的經過MessageQueue的next方法將消息取出來,並執行。代碼以下:併發

public static void loop() {
        final Looper me = myLooper();// 這裏要特別注意,是從ThreadLocal中拿到當前線程的Looper。
        if (me == null) {
            throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
        }
        final MessageQueue queue = me.mQueue;

        for (;;) {
            //從 MessageQueue 中取消息
            Message msg = queue.next(); // might block
            if (msg == null) {
                // No message indicates that the message queue is quitting.
                return;
            }
			//經過 Handler 分發消息
            msg.target.dispatchMessage(msg);
            //回收消息
            msg.recycleUnchecked();
        }
    }
複製代碼

能夠看到在取出Message後則會調用Message.target調用dispatchMessage方法,這裏target就是Handler,它是在Handler的enqueueMessage時賦值的。緊接着將Message進行了回收。 接下來再回到Handler看dispatchMessage,代碼以下:異步

public void dispatchMessage(Message msg) {
        if (msg.callback != null) {
            //經過 handler.postXxx 形式傳入的 Runnable
            handleCallback(msg);
        } else {
            if (mCallback != null) {
                //以 Handler(Handler.Callback) 寫法
                if (mCallback.handleMessage(msg)) {
                    return;
                }
            }
            //以 Handler(){} 內存泄露寫法
            handleMessage(msg);
        }
    }
複製代碼

相關知識點參考:github.com/733gh/xiong…async

能夠看到,這裏最終會調用到咱們本身的實現方法。至此完結。ide

2.一個線程有幾個Handler?一個線程有幾個Looper?如何保證?

Handler的個數與所在線程無關,能夠在線程中實例化任意多個Handler。一個線程中只有一個Looper。Looper的構造方法被聲明爲了private,咱們沒法經過new關鍵字來實例化Looper,惟一開放的能夠實例化Looper的方法是prepare。prepare方法的源碼以下:

public static void prepare() {
        prepare(true);
    }

    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));
    }
複製代碼

咱們知道ThreadLocal是一個線程內部的數據存儲類,當某個線程調用prepare方法的時候,會首先經過ThreadLocal檢查這個線程是否已經建立了Looper,若是還沒建立,則實例化Looper並將實例化後的Looper保存到ThreadLocal中,而若是ThreadLocal中已經保存了Looper,則會拋出一個RuntimeException的異常。那麼意味着在一個線程中最多隻能調用一次prepare方法,這樣就保證了Looper的惟一性。

3.Handler線程是如何切換的?

(1)假設如今有一個線程A,在A線程中經過Looper.prepare和Looper.loop來開啓Looper,而且在A線程中實例化出來一個Handler。Looper.prepare()方法被調用時會爲會初始化Looper併爲ThreadLocal 設置Looper,此時ThreadLocal中就存儲了A線程的Looper。另外MessageQueue也會在Looper中被初始化。

(2)接着當調用Loop.loop方法時,loop方法會經過myLooper獲得A線程中的Looper,進而拿到Looper中的MessageQueue,接着開啓死循環等待執行MessageQueue中的方法。 (3)此時,再開啓一個線程B,並在B線程中經過Handler發送出一個Message,這個Message最終會經過sendMessageAtTime方法調用到MessageQueue的equeueMessage方法將消息插入到隊列。

(3)因爲Looper的loop是一個死循環,當MessageQueue中被插入消息的時候,loop方法就會取出MessageQueue中的消息,並執行callback。而此時,Looper是A線程的Looper,進而調用的Message或者Handler的Callback都是執行在A線成中的。以此達到了線程的切換。

4.Handler內存泄漏的緣由是什麼?如何解決?

一般在使用Handler的時候回經過匿名內部類的方式來實例化Handler,而非靜態的匿名內部類默認持有外部類的引用,即匿名內部類Handler持有了外部類。而致使內存泄漏的根本緣由是是由於Handler的生命週期與宿主的生命週期不一致。

好比說在Activity中實例化了一個非靜態的匿名內部類Handler,而後經過Handler發送了一個延遲消息,可是在消息還未執行時結束了Activity,此時因爲Handler持有Activity,就會致使Activity沒法被GC回收,也就是出現了內存泄漏的問題。

解決方式:能夠把Handler聲明爲靜態的匿名內部類,但這樣一來,在Handler內部就沒辦法調用到Activity中的非靜態方法或變量。那麼最終的解決方案可使用靜態內部類 + 弱引用來解決。代碼以下:

public class MainActivity extends AppCompatActivity {

    private MyHandler mMyHandler = new MyHandler(this);

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

    private void handleMessage(Message msg) {

    }

    static class MyHandler extends Handler {
        private WeakReference<Activity> mReference;

        MyHandler(Activity reference) {
            mReference = new WeakReference<>(reference);
        }

        @Override
        public void handleMessage(Message msg) {
            MainActivity activity = (MainActivity) mReference.get();
            if (activity != null) {
                activity.handleMessage(msg);
            }
        }
    }

    @Override
    protected void onDestroy() {
        mMyHandler.removeCallbacksAndMessages(null);
        super.onDestroy();
    }
}
複製代碼

相關知識點參考:github.com/733gh/xiong…

5.主線程爲何不用初始化Looper?

答:由於應用在啓動的過程當中就已經初始化主線程Looper了。

每一個java應用程序都是有一個main方法入口,Android是基於Java的程序也不例外。Android程序的入口在ActivityThread的main方法中:

public static void main(String[] args) {
    ...
 // 初始化主線程Looper
    Looper.prepareMainLooper();
    ...
    // 新建一個ActivityThread對象
    ActivityThread thread = new ActivityThread();
    thread.attach(false, startSeq);
 
    // 獲取ActivityThread的Handler,也是他的內部類H
    if (sMainThreadHandler == null) {
        sMainThreadHandler = thread.getHandler();
    }
 
    ...
    Looper.loop();
 // 若是loop方法結束則拋出異常,程序結束
    throw new RuntimeException("Main thread loop unexpectedly exited");
}
複製代碼

main方法中先初始化主線程Looper,新建ActivityThread對象,而後再啓動Looper,這樣主線程的Looper在程序啓動的時候就跑起來了。咱們不須要再去初始化主線程Looper。

6.Handler如何保證MessageQueue併發訪問安全?

答:循環加鎖,配合阻塞喚醒機制。

咱們能夠發現MessageQueue實際上是「生產者-消費者」模型,Handler不斷地放入消息,Looper不斷地取出,這就涉及到死鎖問題。若是Looper拿到鎖,可是隊列中沒有消息,就會一直等待,而Handler須要把消息放進去,鎖卻被Looper拿着沒法入隊,這就形成了死鎖。Handler機制的解決方法是循環加鎖。在MessageQueue的next方法中:

Message next() {
   ...
    for (;;) {
  ...
        nativePollOnce(ptr, nextPollTimeoutMillis);
        synchronized (this) {
            ...
        }
    }
}
複製代碼

咱們能夠看到他的等待是在鎖外的,當隊列中沒有消息的時候,他會先釋放鎖,再進行等待,直到被喚醒。這樣就不會形成死鎖問題了。

那在入隊的時候會不會由於隊列已經滿了而後一邊在等待消息處理一邊拿着鎖呢?這一點不一樣的是MessageQueue的消息沒有上限,或者說他的上限就是JVM給程序分配的內存,若是超出內存會拋出異常,但通常狀況下是不會的。

相關知識點參考:github.com/733gh/xiong…

7.Handler的阻塞喚醒機制是怎麼回事?

答: Handler的阻塞喚醒機制是基於Linux的阻塞喚醒機制。

這個機制也是相似於handler機制的模式。在本地建立一個文件描述符,而後須要等待的一方則監聽這個文件描述符,喚醒的一方只須要修改這個文件,那麼等待的一方就會收到文件從而打破喚醒。和Looper監聽MessageQueue,Handler添加message是比較相似的。

8.能不能讓一個Message加急被處理?/ 什麼是Handler同步屏障?

答:能夠 / 一種使得異步消息能夠被更快處理的機制

若是向主線程發送了一個UI更新的操做Message,而此時消息隊列中的消息很是多,那麼這個Message的處理就會變得緩慢,形成界面卡頓。因此經過同步屏障,可使得UI繪製的Message更快被執行。

什麼是同步屏障?這個「屏障」實際上是一個Message,插入在MessageQueue的鏈表頭,且其target==null。Message入隊的時候不是判斷了target不能爲null嗎?不不不,添加同步屏障是另外一個方法:

public int postSyncBarrier() {
    return postSyncBarrier(SystemClock.uptimeMillis());
}
 
private int postSyncBarrier(long when) {
    synchronized (this) {
        final int token = mNextBarrierToken++;
        final Message msg = Message.obtain();
        msg.markInUse();
        msg.when = when;
        msg.arg1 = token;
 
        Message prev = null;
        Message p = mMessages;
        // 把當前須要執行的Message所有執行
        if (when != 0) {
            while (p != null && p.when <= when) {
                prev = p;
                p = p.next;
            }
        }
        // 插入同步屏障
        if (prev != null) { // invariant: p == prev.next
            msg.next = p;
            prev.next = msg;
        } else {
            msg.next = p;
            mMessages = msg;
        }
        return token;
    }
}
複製代碼

能夠看到同步屏障就是一個特殊的target,哪裏特殊呢?target==null,咱們能夠看到他並無給target屬性賦值。那這個target有什麼用呢?看next方法:

Message next() {
    ...
 
    // 阻塞時間
    int nextPollTimeoutMillis = 0;
    for (;;) {
        ...
        // 阻塞對應時間 
        nativePollOnce(ptr, nextPollTimeoutMillis);
  // 對MessageQueue進行加鎖,保證線程安全
        synchronized (this) {
            final long now = SystemClock.uptimeMillis();
            Message prevMsg = null;
            Message msg = mMessages;
            /**
            *  1
            */
            if (msg != null && msg.target == null) {
                // 同步屏障,找到下一個異步消息
                do {
                    prevMsg = msg;
                    msg = msg.next;
                } while (msg != null && !msg.isAsynchronous());
            }
            if (msg != null) {
                if (now < msg.when) {
                    // 下一個消息還沒開始,等待二者的時間差
                    nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
                } else {
                    // 得到消息且如今要執行,標記MessageQueue爲非阻塞
                    mBlocked = false;
                    /**
              *  2
              */
                    // 通常只有異步消息纔會從中間拿走消息,同步消息都是從鏈表頭獲取
                    if (prevMsg != null) {
                        prevMsg.next = msg.next;
                    } else {
                        mMessages = msg.next;
                    }
                    msg.next = null;
                    msg.markInUse();
                    return msg;
                }
            } else {
                // 沒有消息,進入阻塞狀態
                nextPollTimeoutMillis = -1;
            }
 
            // 當調用Looper.quitSafely()時候執行完全部的消息後就會退出
            if (mQuitting) {
                dispose();
                return null;
            }
            ...
        }
        ...
    }
}
複製代碼

這個方法我在前面講過,咱們重點看一下關於同步屏障的部分,看註釋1的地方的代碼:

if (msg != null && msg.target == null) {
    // 同步屏障,找到下一個異步消息
    do {
        prevMsg = msg;
        msg = msg.next;
    } while (msg != null && !msg.isAsynchronous());
}
複製代碼

若是遇到同步屏障,那麼會循環遍歷整個鏈表找到標記爲異步消息的Message,即isAsynchronous返回true,其餘的消息會直接忽視,那麼這樣異步消息,就會提早被執行了。註釋2的代碼注意一下就能夠了。

注意,同步屏障不會自動移除,使用完成以後須要手動進行移除,否則會形成同步消息沒法被處理。從源碼中能夠看到若是不移除同步屏障,那麼他會一直在那裏,這樣同步消息就永遠沒法被執行了。

有了同步屏障,那麼喚醒的判斷條件就必須再加一個:MessageQueue中有同步屏障且處於阻塞中,此時插入在全部異步消息前插入新的異步消息。這個也很好理解,跟同步消息是同樣的。若是把全部的同步消息先忽視,就是插入新的鏈表頭且隊列處於阻塞狀態,這個時候就須要被喚醒了。看一下源碼:

boolean enqueueMessage(Message msg, long when) {
    ...
 
    // 對MessageQueue進行加鎖
    synchronized (this) {
        ...
        if (p == null || when == 0 || when < p.when) {
            msg.next = p;
            mMessages = msg;
            needWake = mBlocked;
        } else {
            /**
            * 1
            */
            // 當線程被阻塞,且目前有同步屏障,且入隊的消息是異步消息
            needWake = mBlocked && p.target == null && msg.isAsynchronous();
            Message prev;
            for (;;) {
                prev = p;
                p = p.next;
                if (p == null || when < p.when) {
                    break;
                }
                /**
                * 2
                */
                // 若是找到一個異步消息,說明前面有延遲的異步消息須要被處理,不須要被喚醒
                if (needWake && p.isAsynchronous()) {
                    needWake = false;
                }
            }
            msg.next = p; 
            prev.next = msg;
        }
  
        // 若是須要則喚醒隊列
        if (needWake) {
            nativeWake(mPtr);
        }
    }
    return true;
}
複製代碼

一樣,這個方法我以前講過,把無關同步屏障的代碼忽視,看到註釋1處的代碼。若是插入的消息是異步消息,且有同步屏障,同時MessageQueue正處於阻塞狀態,那麼就須要喚醒。而若是這個異步消息的插入位置不是全部異步消息以前,那麼不須要喚醒,如註釋2。

那咱們如何發送一個異步類型的消息呢?有兩種辦法:

  • 使用異步類型的Handler發送的所有Message都是異步的
  • 給Message標誌異步

Handler有一系列帶Boolean類型的參數的構造器,這個參數就是決定是不是異步Handler:

public Handler(@NonNull Looper looper, @Nullable Callback callback, boolean async) {
    mLooper = looper;
    mQueue = looper.mQueue;
    mCallback = callback;
    // 這裏賦值
    mAsynchronous = async;
}
複製代碼

可是異步類型的Handler構造器是標記爲hide,咱們沒法使用,因此咱們使用異步消息只有經過給Message設置異步標誌:

public void setAsynchronous(boolean async) {
    if (async) {
        flags |= FLAG_ASYNCHRONOUS;
    } else {
        flags &= ~FLAG_ASYNCHRONOUS;
    }
}
複製代碼

可是!!!! ,其實同步屏障對於咱們的平常使用的話實際上是沒有多大用處。由於設置同步屏障和建立異步Handler的方法都是標誌爲hide,說明谷歌不想要咱們去使用他。因此這裏同步屏障也做爲一個瞭解,能夠更加全面地理解源碼中的內容。

相關知識點參考:github.com/733gh/xiong…

相關文章
相關標籤/搜索