當前位置:網站首頁>27道 Handler 經典面試題,你注意查收

27道 Handler 經典面試題,你注意查收

2022-01-27 23:08:06 塗程

前言

對於handler,你會想到什麼呢?

面試必問?項目常用?體系龐大?

既然它如此重要,不知對面的你了解它多深呢?今天就和大家一起打破砂鍋問到底,看看Handler這口砂鍋的底到底在哪裏。

二十七問,從問題的角度再讀Handler。

知識梳理

1、Handler被設計出來的原因?有什麼用?

一種東西被設計出來肯定就有它存在的意義,而Handler的意義就是切換線程。

作為Android消息機制的主要成員,它管理著所有與界面有關的消息事件,常見的使用場景有:

  • 跨進程之後的界面消息處理。

比如Activity的啟動,就是AMS在進行進程間通信的時候,通過Binder線程 將消息發送給ApplicationThread的消息處理者Handler,然後再將消息分發給主線程中去執行。

  • 網絡交互後切換到主線程進行UI更新

當子線程網絡操作之後,需要切換到主線程進行UI更新。

總之一句話,Hanlder的存在就是為了解决在子線程中無法訪問UI的問題。

2、為什麼建議子線程不訪問(更新)UI?

因為Android中的UI控件不是線程安全的,如果多線程訪問UI控件那還不亂套了。

那為什麼不加鎖呢?

  • 會降低UI訪問的效率。本身UI控件就是離用戶比較近的一個組件,加鎖之後自然會發生阻塞,那麼UI訪問的效率會降低,最終反應到用戶端就是這個手機有點卡。
  • 太複雜了。本身UI訪問時一個比較簡單的操作邏輯,直接創建UI,修改UI即可。如果加鎖之後就讓這個UI訪問的邏輯變得很複雜,沒必要。

所以,Android設計出了 單線程模型 來處理UI操作,再搭配上Handler,是一個比較合適的解决方案。

3、子線程訪問UI的 崩潰原因 和 解决辦法?

崩潰發生在ViewRootImpl類的checkThread方法中:

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

其實就是判斷了當前線程 是否是 ViewRootImpl創建時候的線程,如果不是,就會崩潰。

而ViewRootImpl創建的時機就是界面被繪制的時候,也就是onResume之後,所以如果在子線程進行UI更新,就會發現當前線程(子線程)和View創建的線程(主線程)不是同一個線程,發生崩潰。

解决辦法有三種:

  • 在新建視圖的線程進行這個視圖的UI更新,主線程創建View,主線程更新View。
  • ViewRootImpl創建之前進行子線程的UI更新,比如onCreate方法中進行子線程更新UI。
  • 子線程切換到主線程進行UI更新,比如Handler、view.post方法。

4、MessageQueue是幹嘛呢?用的什麼數據結構來存儲數據?

看名字應該是個隊列結構,隊列的特點是什麼?先進先出,一般在隊尾增加數據,在隊首進行取數據或者删除數據。

Hanlder中的消息似乎也滿足這樣的特點,先發的消息肯定就會先被處理。但是,Handler中還有比較特殊的情况,比如延時消息。

延時消息的存在就讓這個隊列有些特殊性了,並不能完全保證先進先出,而是需要根據時間來判斷,所以Android中采用了鏈錶的形式來實現這個隊列,也方便了數據的插入。

來一起看看消息的發送過程,無論是哪種方法發送消息,都會走到sendMessageDelayed方法

    public final boolean sendMessageDelayed(@NonNull Message msg, long delayMillis) {
    
        if (delayMillis < 0) {
    
            delayMillis = 0;
        }
        return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
    }

    public boolean sendMessageAtTime(@NonNull Message msg, long uptimeMillis) {
    
        MessageQueue queue = mQueue;
        return enqueueMessage(queue, msg, uptimeMillis);
    }

sendMessageDelayed方法主要計算了消息需要被處理的時間,如果delayMillis為0,那麼消息的處理時間就是當前時間。

然後就是關鍵方法enqueueMessage

    boolean enqueueMessage(Message msg, long when) {
    
        synchronized (this) {
    
            msg.markInUse();
            msg.when = when;
            Message p = mMessages;
            boolean needWake;
            if (p == null || when == 0 || when < p.when) {
    
                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; 
                prev.next = msg;
            }

            if (needWake) {
    
                nativeWake(mPtr);
            }
        }
        return true;
    }

不懂得地方先不看,只看我們想看的:

  • 首先設置了Message的when字段,也就是代錶了這個消息的處理時間
  • 然後判斷當前隊列是不是為空,是不是即時消息,是不是執行時間when大於錶頭的消息時間,滿足任意一個,就把當前消息msg插入到錶頭。
  • 否則,就需要遍曆這個隊列,也就是鏈錶,找出when小於某個節點的when,找到後插入。

好了,其他內容暫且不看,總之,插入消息就是通過消息的執行時間,也就是when字段,來找到合適的比特置插入鏈錶。

具體方法就是通過死循環,使用快慢指針p和prev,每次向後移動一格,直到找到某個節點p的when大於我們要插入消息的when字段,則插入到p和prev之間。 或者遍曆到鏈錶結束,插入到鏈錶結尾。

所以,MessageQueue就是一個用於存儲消息、用鏈錶實現的特殊隊列結構。

5、延遲消息是怎麼實現的?

總結上述內容,延遲消息的實現主要跟消息的統一存儲方法有關,也就是上文說過的enqueueMessage方法。

無論是即時消息還是延遲消息,都是計算出具體的時間,然後作為消息的when字段進程賦值。

然後在MessageQueue中找到合適的比特置(安排when小到大排列),並將消息插入到MessageQueue中。

這樣,MessageQueue就是一個按照消息時間排列的一個鏈錶結構。

6、MessageQueue的消息怎麼被取出來的?

剛才說過了消息的存儲,接下來看看消息的取出,也就是queue.next方法。

    Message next() {
    
        for (;;) {
    
            if (nextPollTimeoutMillis != 0) {
    
                Binder.flushPendingCommands();
            }

            nativePollOnce(ptr, nextPollTimeoutMillis);

            synchronized (this) {
    
                // Try to retrieve the next message. Return if found.
                final long now = SystemClock.uptimeMillis();
                Message prevMsg = null;
                Message msg = mMessages;
                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 {
    
                        // Got a message.
                        mBlocked = false;
                        if (prevMsg != null) {
    
                            prevMsg.next = msg.next;
                        } else {
    
                            mMessages = msg.next;
                        }
                        msg.next = null;
                        msg.markInUse();
                        return msg;
                    }
                } else {
    
                    // No more messages.
                    nextPollTimeoutMillis = -1;
                }
            }
        }
    }

奇怪,為什麼取消息也是用的死循環呢?

其實死循環就是為了保證一定要返回一條消息,如果沒有可用消息,那麼就阻塞在這裏,一直到有新消息的到來。

其中,nativePollOnce方法就是阻塞方法,nextPollTimeoutMillis參數就是阻塞的時間。

那什麼時候會阻塞呢?兩種情况:

  • 1、有消息,但是當前時間小於消息執行時間,也就是代碼中的這一句:
if (now < msg.when) {
    
    nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
}

這時候阻塞時間就是消息時間减去當前時間,然後進入下一次循環,阻塞。

  • 2、沒有消息的時候,也就是上述代碼的最後一句:
if (msg != null) {
    } 
    else {
    
    // No more messages.
    nextPollTimeoutMillis = -1;
    }

-1就代錶一直阻塞。

7、MessageQueue沒有消息時候會怎樣?阻塞之後怎麼喚醒呢?說說pipe/epoll機制?

接著上文的邏輯,當消息不可用或者沒有消息的時候就會阻塞在next方法,而阻塞的辦法是通過pipe/epoll機制

epoll機制是一種IO多路複用的機制,具體邏輯就是一個進程可以監視多個描述符,當某個描述符就緒(一般是讀就緒或者寫就緒),能够通知程序進行相應的讀寫操作,這個讀寫操作是阻塞的。在Android中,會創建一個Linux管道(Pipe)來處理阻塞和喚醒。

  • 當消息隊列為空,管道的讀端等待管道中有新內容可讀,就會通過epoll機制進入阻塞狀態。
  • 當有消息要處理,就會通過管道的寫端寫入內容,喚醒主線程。

那什麼時候會怎麼喚醒消息隊列線程呢?

還記得剛才插入消息的enqueueMessage方法中有個needWake字段嗎,很明顯,這個就是錶示是否喚醒的字段。

其中還有個字段是mBlocked,看字面意思是阻塞的意思,去代碼裏面找找:

Message next() {
    
        for (;;) {
    
            synchronized (this) {
    
                if (msg != null) {
    
                    if (now < msg.when) {
    
                        nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
                    } else {
    
                        // Got a message.
                        mBlocked = false;
                        return msg;
                    }
                } 
                if (pendingIdleHandlerCount <= 0) {
    
                    // No idle handlers to run. Loop and wait some more.
                    mBlocked = true;
                    continue;
                }
            }
        }
    }

在獲取消息的方法next中,有兩個地方對mBlocked賦值:

  • 當獲取到消息的時候,mBlocked賦值為false,錶示不阻塞。
  • 當沒有消息要處理,也沒有idleHandler要處理的時候,mBlocked賦值為true,錶示阻塞。

好了,確實這個字段就錶示是否阻塞的意思,再去看看enqueueMessage方法中,喚醒機制:

    boolean enqueueMessage(Message msg, long when) {
    
        synchronized (this) {
    
            boolean needWake;
            if (p == null || when == 0 || when < p.when) {
    
                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; 
                prev.next = msg;
            }

            if (needWake) {
    
                nativeWake(mPtr);
            }
        }
        return true;
    }
  • 當鏈錶為空或者時間小於錶頭消息時間,那麼就插入錶頭,並且設置是否喚醒為mBlocked

再結合上述的例子,也就是當有新消息要插入錶頭了,這時候如果之前是阻塞狀態(mBlocked=true),那麼就要喚醒線程了。

  • 否則,就需要取鏈錶中找到某個節點並插入消息,在這之前需要賦值needWake = mBlocked && p.target == null && msg.isAsynchronous()

也就是在插入消息之前,需要判斷是否阻塞,並且錶頭是不是屏障消息,並且當前消息是不是异步消息。 也就是如果現在是同步屏障模式下,那麼要插入的消息又剛好是异步消息,那就不用管插入消息問題了,直接喚醒線程,因為异步消息需要先執行。

  • 最後一點,是在循環裏,如果發現之前就存在异步消息,那就還是設置是否喚醒為false

意思就是,如果之前有异步消息了,那肯定之前就喚醒過了,這時候就不需要再次喚醒了。

最後根據needWake的值,决定是否調用nativeWake方法喚醒next()方法。

8、同步屏障和异步消息是怎麼實現的?

其實在Handler機制中,有三種消息類型:

  • 同步消息。也就是普通的消息。
  • 异步消息。通過setAsynchronous(true)設置的消息。
  • 同步屏障消息。通過postSyncBarrier方法添加的消息,特點是target為空,也就是沒有對應的handler。

這三者之間的關系如何呢?

  • 正常情况下,同步消息和异步消息都是正常被處理,也就是根據時間when來取消息,處理消息。
  • 當遇到同步屏障消息的時候,就開始從消息隊列裏面去找异步消息,找到了再根據時間决定阻塞還是返回消息。
Message msg = mMessages;
if (msg != null && msg.target == null) {
    
      do {
    
      prevMsg = msg;
      msg = msg.next;
      } while (msg != null && !msg.isAsynchronous());
}

也就是說同步屏障消息不會被返回,他只是一個標志,一個工具,遇到它就代錶要去先行處理异步消息了。

所以同步屏障和异步消息的存在的意義就在於有些消息需要“加急處理”

9、同步屏障和异步消息有具體的使用場景嗎?

使用場景就很多了,比如繪制方法scheduleTraversals

    void scheduleTraversals() {
    
        if (!mTraversalScheduled) {
    
            mTraversalScheduled = true;
            // 同步屏障,阻塞所有的同步消息
            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
            // 通過 Choreographer 發送繪制任務
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
        }
    }

    Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
    msg.arg1 = callbackType;
    msg.setAsynchronous(true);
    mHandler.sendMessageAtTime(msg, dueTime);

在該方法中加入了同步屏障,後續加入一個异步消息MSG_DO_SCHEDULE_CALLBACK,最後會執行到FrameDisplayEventReceiver,用於申請VSYNC信號。

10、Message消息被分發之後會怎麼處理?消息怎麼複用的?

再看看loop方法,在消息被分發之後,也就是執行了dispatchMessage方法之後,還偷偷做了一個操作——recycleUnchecked

    public static void loop() {
    
        for (;;) {
    
            Message msg = queue.next(); // might block

            try {
    
                msg.target.dispatchMessage(msg);
            } 

            msg.recycleUnchecked();
        }
    }

//Message.java
    private static Message sPool;
    private static final int MAX_POOL_SIZE = 50;

    void recycleUnchecked() {
    
        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++;
            }
        }
    }

recycleUnchecked方法中,釋放了所有資源,然後將當前的空消息插入到sPool錶頭。

這裏的sPool就是一個消息對象池,它也是一個鏈錶結構的消息,最大長度為50。

那麼Message又是怎麼複用的呢?在Message的實例化方法obtain中:

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

直接複用消息池sPool中的第一條消息,然後sPool指向下一個節點,消息池數量减一。

11、Looper是幹嘛呢?怎麼獲取當前線程的Looper?為什麼不直接用Map存儲線程和對象呢?

在Handler發送消息之後,消息就被存儲到MessageQueue中,而Looper就是一個管理消息隊列的角色。 Looper會從MessageQueue中不斷的查找消息,也就是loop方法,並將消息交回給Handler進行處理。

而Looper的獲取就是通過ThreadLocal機制:

    static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();

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

    public static @Nullable Looper myLooper() {
    
        return sThreadLocal.get();
    }

通過prepare方法創建Looper並且加入到sThreadLocal中,通過myLooper方法從sThreadLocal中獲取Looper。

12、ThreadLocal運行機制?這種機制設計的好處?

下面就具體說說ThreadLocal運行機制。

//ThreadLocal.java
    public T get() {
    
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
    
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
    
                @SuppressWarnings("unchecked")
                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);
    }

ThreadLocal類中的get和set方法可以大致看出來,有一個ThreadLocalMap變量,這個變量存儲著鍵值對形式的數據。

  • key為this,也就是當前ThreadLocal變量。
  • value為T,也就是要存儲的值。

然後繼續看看ThreadLocalMap哪來的,也就是getMap方法:

    //ThreadLocal.java
    ThreadLocalMap getMap(Thread t) {
    
        return t.threadLocals;
    }

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

原來這個ThreadLocalMap變量是存儲在線程類Thread中的。

所以ThreadLocal的基本機制就搞清楚了:

在每個線程中都有一個threadLocals變量,這個變量存儲著ThreadLocal和對應的需要保存的對象。

這樣帶來的好處就是,在不同的線程,訪問同一個ThreadLocal對象,但是能獲取到的值卻不一樣。

挺神奇的是不是,其實就是其內部獲取到的Map不同,Map和Thread綁定,所以雖然訪問的是同一個ThreadLocal對象,但是訪問的Map卻不是同一個,所以取得值也不一樣。

這樣做有什麼好處呢?為什麼不直接用Map存儲線程和對象呢?

打個比方:

  • ThreadLocal就是老師。
  • Thread就是同學。
  • Looper(需要的值)就是鉛筆。

現在老師買了一批鉛筆,然後想把這些鉛筆發給同學們,怎麼發呢?兩種辦法:

  • 1、老師把每個鉛筆上寫好每個同學的名字,放到一個大盒子裏面去(map),用的時候就讓同學們自己來找。

這種做法就是Map裏面存儲的是同學和鉛筆,然後用的時候通過同學來從這個Map裏找鉛筆。

這種做法就有點像使用一個Map,存儲所有的線程和對象,不好的地方就在於會很混亂,每個線程之間有了聯系,也容易造成內存泄漏。

  • 2、老師把每個鉛筆直接發給每個同學,放到同學的口袋裏(map),用的時候每個同學從口袋裏面拿出鉛筆就可以了。

這種做法就是Map裏面存儲的是老師和鉛筆,然後用的時候老師說一聲,同學只需要從口袋裏拿出來就行了。

很明顯這種做法更科學,這也就是ThreadLocal的做法,因為鉛筆本身就是同學自己在用,所以一開始就把鉛筆交給同學自己保管是最好的,每個同學之間進行隔離。

13、還有哪些地方運用到了ThreadLocal機制?

比如:Choreographer。

public final class Choreographer {
    

    // Thread local storage for the choreographer.
    private static final ThreadLocal<Choreographer> sThreadInstance =
            new ThreadLocal<Choreographer>() {
    
        @Override
        protected Choreographer initialValue() {
    
            Looper looper = Looper.myLooper();
            if (looper == null) {
    
                throw new IllegalStateException("The current thread must have a looper!");
            }
            Choreographer choreographer = new Choreographer(looper, VSYNC_SOURCE_APP);
            if (looper == Looper.getMainLooper()) {
    
                mMainInstance = choreographer;
            }
            return choreographer;
        }
    };

    private static volatile Choreographer mMainInstance;

Choreographer主要是主線程用的,用於配合 VSYNC 中斷信號。

所以這裏使用ThreadLocal更多的意義在於完成線程單例的功能。

14、可以多次創建Looper嗎?

Looper的創建是通過Looper.prepare方法實現的,而在prepare方法中就判斷了,當前線程是否存在Looper對象,如果有,就會直接拋出异常:

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

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

所以同一個線程,只能創建一個Looper,多次創建會報錯。

15、Looper中的quitAllowed字段是啥?有什麼用?

按照字面意思就是是否允許退出,我們看看他都在哪些地方用到了:

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

哦,就是這個quit方法用到了,如果這個字段為false,代錶不允許退出,就會報錯。

但是這個quit方法又是幹嘛的呢?從來沒用過呢。 還有這個safe又是啥呢?

其實看名字就差不多能了解了,quit方法就是退出消息隊列,終止消息循環。

  • 首先設置了mQuitting字段為true。
  • 然後判斷是否安全退出,如果安全退出,就執行removeAllFutureMessagesLocked方法,它內部的邏輯是清空所有的延遲消息,之前沒處理的非延遲消息還是需要取處理,然後設置非延遲消息的下一個節點為空(p.next=null)。
  • 如果不是安全退出,就執行removeAllMessagesLocked方法,直接清空所有的消息,然後設置消息隊列指向空(mMessages = null)

然後看看當調用quit方法之後,消息的發送和處理:

//消息發送
    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;
            }
        }

當調用了quit方法之後,mQuitting為true,消息就發不出去了,會報錯。

再看看消息的處理,loop和next方法:

    Message next() {
    
        for (;;) {
    
            synchronized (this) {
    
                if (mQuitting) {
    
                    dispose();
                    return null;
                } 
            }  
        }
    }

    public static void loop() {
    
        for (;;) {
    
            Message msg = queue.next();
            if (msg == null) {
    
                // No message indicates that the message queue is quitting.
                return;
            }
        }
    }

很明顯,當mQuitting為true的時候,next方法返回null,那麼loop方法中就會退出死循環。

那麼這個quit方法一般是什麼時候使用呢?

  • 主線程中,一般情况下肯定不能退出,因為退出後主線程就停止了。所以是當APP需要退出的時候,就會調用quit方法,涉及到的消息是EXIT_APPLICATION,大家可以搜索下。
  • 子線程中,如果消息都處理完了,就需要調用quit方法停止消息循環。

16、Looper.loop方法是死循環,為什麼不會卡死(ANR)?

我大致總結下:

  • 1、主線程本身就是需要一只運行的,因為要處理各個View,界面變化。所以需要這個死循環來保證主線程一直執行下去,不會被退出。
  • 2、真正會卡死的操作是在某個消息處理的時候操作時間過長,導致掉幀、ANR,而不是loop方法本身。
  • 3、在主線程以外,會有其他的線程來處理接受其他進程的事件,比如Binder線程(ApplicationThread),會接受AMS發送來的事件
  • 4、在收到跨進程消息後,會交給主線程的Hanlder再進行消息分發。所以Activity的生命周期都是依靠主線程的Looper.loop,當收到不同Message時則采用相應措施,比如收到msg=H.LAUNCH_ACTIVITY,則調用ActivityThread.handleLaunchActivity()方法,最終執行到onCreate方法。
  • 5、當沒有消息的時候,會阻塞在loop的queue.next()中的nativePollOnce()方法裏,此時主線程會釋放CPU資源進入休眠狀態,直到下個消息到達或者有事務發生。所以死循環也不會特別消耗CPU資源。

17、Message是怎麼找到它所屬的Handler然後進行分發的?

在loop方法中,找到要處理的Message,然後調用了這麼一句代碼處理消息:

msg.target.dispatchMessage(msg);

所以是將消息交給了msg.target來處理,那麼這個target是啥呢?

找找它的來頭:

//Handler
    private boolean enqueueMessage(MessageQueue queue,Message msg,long uptimeMillis) {
    
        msg.target = this;

        return queue.enqueueMessage(msg, uptimeMillis);
    }

在使用Hanlder發送消息的時候,會設置msg.target = this,所以target就是當初把消息加到消息隊列的那個Handler。

18、Handler 的 post(Runnable) 與 sendMessage 有什麼區別

Hanlder中主要的發送消息可以分為兩種:

  • post(Runnable)
  • sendMessage
    public final boolean post(@NonNull Runnable r) {
    
       return  sendMessageDelayed(getPostMessage(r), 0);
    }
    private static Message getPostMessage(Runnable r) {
    
        Message m = Message.obtain();
        m.callback = r;
        return m;
    }

通過post的源碼可知,其實post和sendMessage的區別就在於:

post方法給Message設置了一個callback

那麼這個callback有什麼用呢?我們再轉到消息處理的方法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();
    }

這段代碼可以分為三部分看:

  • 1、如果msg.callback不為空,也就是通過post方法發送消息的時候,會把消息交給這個msg.callback進行處理,然後就沒有後續了。
  • 2、如果msg.callback為空,也就是通過sendMessage發送消息的時候,會判斷Handler當前的mCallback是否為空,如果不為空就交給Handler.Callback.handleMessage處理。
  • 3、如果mCallback.handleMessage返回true,則無後續了。
  • 4、如果mCallback.handleMessage返回false,則調用handler類重寫的handleMessage方法。

所以post(Runnable) 與 sendMessage的區別就在於後續消息的處理方式,是交給msg.callback還是 Handler.Callback或者Handler.handleMessage

19、Handler.Callback.handleMessage 和 Handler.handleMessage 有什麼不一樣?為什麼這麼設計?

接著上面的代碼說,這兩個處理方法的區別在於Handler.Callback.handleMessage方法是否返回true:

  • 如果為true,則不再執行Handler.handleMessage
  • 如果為false,則兩個方法都要執行。

那麼什麼時候有Callback,什麼時候沒有呢?這涉及到兩種Hanlder的 創建方式:

    val handler1= object : Handler(){
    
        override fun handleMessage(msg: Message) {
    
            super.handleMessage(msg)
        }
    }

    val handler2 = Handler(object : Handler.Callback {
    
        override fun handleMessage(msg: Message): Boolean {
    
            return true
        }
    })

常用的方法就是第1種,派生一個Handler的子類並重寫handleMessage方法。 而第2種就是系統給我們提供了一種不需要派生子類的使用方法,只需要傳入一個Callback即可。

20、Handler、Looper、MessageQueue、線程是一一對應關系嗎?

  • 一個線程只會有一個Looper對象,所以線程和Looper是一一對應的。
  • MessageQueue對象是在new Looper的時候創建的,所以Looper和MessageQueue是一一對應的。
  • Handler的作用只是將消息加到MessageQueue中,並後續取出消息後,根據消息的target字段分發給當初的那個handler,所以Handler對於Looper是可以多對一的,也就是多個Hanlder對象都可以用同一個線程、同一個Looper、同一個MessageQueue。

總結:Looper、MessageQueue、線程是一一對應關系,而他們與Handler是可以一對多的。

21、ActivityThread中做了哪些關於Handler的工作?(為什麼主線程不需要單獨創建Looper)

主要做了兩件事:

  • 1、在main方法中,創建了主線程的LooperMessageQueue,並且調用loop方法開啟了主線程的消息循環。
public static void main(String[] args) {
    

        Looper.prepareMainLooper();

        if (sMainThreadHandler == null) {
    
            sMainThreadHandler = thread.getHandler();
        }

        Looper.loop();

        throw new RuntimeException("Main thread loop unexpectedly exited");
    }
  • 2、創建了一個Handler來進行四大組件的啟動停止等事件處理
final H mH = new H();

class H extends Handler {
    
        public static final int BIND_APPLICATION        = 110;
        public static final int EXIT_APPLICATION        = 111;
        public static final int RECEIVER                = 113;
        public static final int CREATE_SERVICE          = 114;
        public static final int STOP_SERVICE            = 116;
        public static final int BIND_SERVICE            = 121;

22、IdleHandler是啥?有什麼使用場景?

之前說過,當MessageQueue沒有消息的時候,就會阻塞在next方法中,其實在阻塞之前,MessageQueue還會做一件事,就是檢查是否存在IdleHandler,如果有,就會去執行它的queueIdle方法。

    private IdleHandler[] mPendingIdleHandlers;

    Message next() {
    
        int pendingIdleHandlerCount = -1;
        for (;;) {
    
            synchronized (this) {
    
                //當消息執行完畢,就設置pendingIdleHandlerCount
                if (pendingIdleHandlerCount < 0
                        && (mMessages == null || now < mMessages.when)) {
    
                    pendingIdleHandlerCount = mIdleHandlers.size();
                }

                //初始化mPendingIdleHandlers
                if (mPendingIdleHandlers == null) {
    
                    mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)];
                }
                //mIdleHandlers轉為數組
                mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);
            }

            // 遍曆數組,處理每個IdleHandler
            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);
                }

                //如果queueIdle方法返回false,則處理完就删除這個IdleHandler
                if (!keep) {
    
                    synchronized (this) {
    
                        mIdleHandlers.remove(idler);
                    }
                }
            }

            // Reset the idle handler count to 0 so we do not run them again.
            pendingIdleHandlerCount = 0;
        }
    }

當沒有消息處理的時候,就會去處理這個mIdleHandlers集合裏面的每個IdleHandler對象,並調用其queueIdle方法。 最後根據queueIdle返回值判斷是否用完删除當前的IdleHandler

然後看看IdleHandler是怎麼加進去的:

Looper.myQueue().addIdleHandler(new IdleHandler() {
      
    @Override  
    public boolean queueIdle() {
      
        //做事情
        return false;    
    }  
});

    public void addIdleHandler(@NonNull IdleHandler handler) {
    
        if (handler == null) {
    
            throw new NullPointerException("Can't add a null IdleHandler");
        }
        synchronized (this) {
    
            mIdleHandlers.add(handler);
        }
    }

ok,綜上所述,IdleHandler就是當消息隊列裏面沒有當前要處理的消息了,需要堵塞之前,可以做一些空閑任務的處理。

常見的使用場景有:啟動優化

我們一般會把一些事件(比如界面view的繪制、賦值)放到onCreate方法或者onResume方法中。 但是這兩個方法其實都是在界面繪制之前調用的,也就是說一定程度上這兩個方法的耗時會影響到啟動時間。

所以我們可以把一些操作放到IdleHandler中,也就是界面繪制完成之後才去調用,這樣就能减少啟動時間了。

但是,這裏需要注意下可能會有坑。

如果使用不當,IdleHandler會一直不執行,比如在View的onDraw方法裏面無限制的直接或者間接調用View的invalidate方法

其原因就在於onDraw方法中執行invalidate,會添加一個同步屏障消息,在等到异步消息之前,會阻塞在next方法,而等到FrameDisplayEventReceiver异步任務之後又會執行onDraw方法,從而無限循環。

23、HandlerThread是啥?有什麼使用場景?

直接看源碼:

public class HandlerThread extends Thread {
    
    @Override
    public void run() {
    
        Looper.prepare();
        synchronized (this) {
    
            mLooper = Looper.myLooper();
            notifyAll();
        }
        Process.setThreadPriority(mPriority);
        onLooperPrepared();
        Looper.loop();
    }

哦,原來如此。HandlerThread就是一個封裝了Looper的Thread類。

就是為了讓我們在子線程裏面更方便的使用Handler。

這裏的加鎖就是為了保證線程安全,獲取當前線程的Looper對象,獲取成功之後再通過notifyAll方法喚醒其他線程,那哪裏調用了wait方法呢?

    public Looper getLooper() {
    
        if (!isAlive()) {
    
            return null;
        }

        // If the thread has been started, wait until the looper has been created.
        synchronized (this) {
    
            while (isAlive() && mLooper == null) {
    
                try {
    
                    wait();
                } catch (InterruptedException e) {
    
                }
            }
        }
        return mLooper;
    }

就是getLooper方法,所以wait的意思就是等待Looper創建好,那邊創建好之後再通知這邊正確返回Looper。

24、IntentService是啥?有什麼使用場景?

老規矩,直接看源碼:

public abstract class IntentService extends Service {
    

    private final class ServiceHandler extends Handler {
    
        public ServiceHandler(Looper looper) {
    
            super(looper);
        }

        @Override
        public void handleMessage(Message msg) {
    
            onHandleIntent((Intent)msg.obj);
            stopSelf(msg.arg1);
        }
    }

    @Override
    public void onCreate() {
    
        super.onCreate();
        HandlerThread thread = new HandlerThread("IntentService[" + mName + "]");
        thread.start();

        mServiceLooper = thread.getLooper();
        mServiceHandler = new ServiceHandler(mServiceLooper);
    }

    @Override
    public void onStart(@Nullable Intent intent, int startId) {
    
        Message msg = mServiceHandler.obtainMessage();
        msg.arg1 = startId;
        msg.obj = intent;
        mServiceHandler.sendMessage(msg);
    }

理一下這個源碼:

  • 首先,這是一個Service
  • 並且內部維護了一個HandlerThread,也就是有完整的Looper在運行。
  • 還維護了一個子線程的ServiceHandler
  • 啟動Service後,會通過Handler執行onHandleIntent方法。
  • 完成任務後,會自動執行stopSelf停止當前Service。

所以,這就是一個可以在子線程進行耗時任務,並且在任務執行後自動停止的Service。

25、BlockCanary使用過嗎?說說原理

BlockCanary是一個用來檢測應用卡頓耗時的三方庫。

上文說過,View的繪制也是通過Handler來執行的,所以如果能知道每次Handler處理消息的時間,就能知道每次繪制的耗時了? 那Handler消息的處理時間怎麼獲取呢?

再去loop方法中找找細節:

public static void loop() {
    
    for (;;) {
    
        // This must be in a local variable, in case a UI event sets the logger
        Printer logging = me.mLogging;
        if (logging != null) {
    
            logging.println(">>>>> Dispatching to " + msg.target + " " +
                    msg.callback + ": " + msg.what);
        }

        msg.target.dispatchMessage(msg);

        if (logging != null) {
    
            logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
        }
    }
}

可以發現,loop方法內有一個Printer類,在dispatchMessage處理消息的前後分別打印了兩次日志。

那我們把這個日志類Printer替換成我們自己的Printer,然後統計兩次打印日志的時間不就相當於處理消息的時間了?

    Looper.getMainLooper().setMessageLogging(mainLooperPrinter);

    public void setMessageLogging(@Nullable Printer printer) {
    
        mLogging = printer;
    }

這就是BlockCanary的原理。

26、說說Hanlder內存泄露問題。

這也是常常被問的一個問題,Handler內存泄露的原因是什麼?

"內部類持有了外部類的引用,也就是Hanlder持有了Activity的引用,從而導致無法被回收唄。"

其實這樣回答是錯誤的,或者說沒回答到點子上。

我們必須找到那個最終的引用者,不會被回收的引用者,其實就是主線程,這條完整引用鏈應該是這樣:

主線程 —> threadlocal —> Looper —> MessageQueue —> Message —> Handler —> Activity

27、利用Handler機制設計一個不崩潰的App?

主線程崩潰,其實都是發生在消息的處理內,包括生命周期、界面繪制。

所以如果我們能控制這個過程,並且在發生崩潰後重新開啟消息循環,那麼主線程就能繼續運行。

Handler(Looper.getMainLooper()).post {
    
        while (true) {
    
            //主線程异常攔截
            try {
    
                Looper.loop()
            } catch (e: Throwable) {
    
            }
        }
    }

總結

大家應該可以發現,有一個問題常被問,但是全篇都沒有提,那就是:

Hanlder機制的運行原理。

之所以不提這個問題,是因為要回答好這個問題需要大量知識儲備,希望屏幕前的你在讀完這篇之後,再結合自己的知識庫,形成自己的“完美答案”

最後

小編在網上收集了一些 Android 開發相關的學習文檔、面試題、Android 核心筆記等等文檔,希望能幫助到大家學習提昇,如有需要學習參考的可以直接去我 GitHub地址:https://github.com/733gh/Android-T3訪問查閱。


版權聲明
本文為[塗程]所創,轉載請帶上原文鏈接,感謝
https://cht.chowdera.com/2022/01/202201272308059007.html

隨機推薦