面試Handler都沒答上來,你真的瞭解Handler嗎?Handler全面解析來了!

提到handler,大家都想到些什麼呢,切換線程?延時操作? 那麼你是否瞭解「IdleHandler,同步屏障,死循環」的設計原理?以及由Handler機制衍生的「IntentService,BlockCanary」? 這次我們說下Android中最常見的Handler,通過解析面試點或者知識點,帶你領略Handler內部的神奇之處。

先上一張總結圖,看看你是否瞭解真正的Handler

基本的用法和工作流程

用法很簡單,定義一個handler,重寫handleMessage方法處理消息,用send系列方法發送消息。 但是主線程和新建線程用法卻有點不一樣!其實新線程裏面的用法纔是表達出完整流程的。

Handler handler = new Handler() {
    @Override
    public void handleMessage(@NonNull Message msg) {
        super.handleMessage(msg);
    }
};
handler.sendEmptyMessage(0);
handler.sendEmptyMessageDelayed(0, 1000);

new Thread(new Runnable() {
    @Override
    public void run() {
        Looper.prepare();
        Handler handler = new Handler() {
            @Override
            public void handleMessage(@NonNull Message msg) {
                super.handleMessage(msg);
            }
        };
        Looper.loop();
    }
}).start();

Looper.prepare()方法就是創建looper對象,並且綁定到該線程。 然後定義一個handler,loop()方法中主要是looper對象會不斷從MessageQueue中獲取message並且發送給對應的hander,並且通過handleMessage方法處理。

所以looper相當於一個郵遞員,負責從郵局(MessageQueue)獲取信件(Message),並將信件傳遞給收件人(Handler)。

「handler相關四大天王」

  • looper,關聯線程並且負責從消息隊列拿出消息分發給handler
  • MessageQueue,消息隊列,負責消息存儲管理
  • Message,消息對象
  • handler,負責發送和處理消息

「用法和流程就這麼多,下面開始常見面試點講解並附上簡單的源碼解析,具體剖析Handler內部奧祕」

知識點,面試點

「面試題1:主線程的looper呢??」

看下源碼,這裏需要涉及到的一個類android.app.ActivityThread,這個類中的main方法是整個app的最開始執行的方法,是app的入口,看下「main方法源碼」

Looper.prepareMainLooper();
// ***
ActivityThread thread = new ActivityThread();
thread.attach(false, startSeq);
if (sMainThreadHandler == null) {
    sMainThreadHandler = thread.getHandler();
}
if (false) {
    Looper.myLooper().setMessageLogging(new
    LogPrinter(Log.DEBUG, "ActivityThread"));
}
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
Looper.loop();

throw new RuntimeException("Main thread loop unexpectedly exited");
Looper.prepareMainLooper();
Looper.loop();

其中最重要的就是這兩句,調用了prepareMainLooper方法創建了主線程的Looper,然後調用loop方法開始死循環

ok,loop方法是找到了。那Looper爲什麼可以一直取消息呢?看看源碼

//Looper
public static void loop() {
    //...
    for (; ; ) {
        // 不斷從 MessageQueue 獲取 消息
        Message msg = queue.next();
        if (msg == null) {
            // No message indicates that the message queue is quitting.
            return;
        }
        //...
    }
}

找到原因了。其實就是一個死循環,所以Looper可以一直執行工具人的工作

「面試題2:爲什麼有死循環呢?這種寫法科學嗎?不會oom嗎??」

說白了,其實死循環也是有意爲之,線程在可執行代碼執行完後,就會終止,而主線程肯定需要一直運行,所以死循環就能保證這一點。

「死循環之外怎麼處理事務?」

既然主線程是死循環,那麼如果有其他事務該怎麼處理呢?創建新線程唄,在主線程創建之前會創建一些其他的binder線程,比如ApplicationThraed

「死循環是不是會浪費cpu資源」

主線程的messageQueue在沒有消息的時候,會阻塞在loop的queue.next方法中,此時主線程會釋放CPU資源,進入休眠狀態,直到下個消息來到,所以不會一直消耗CPU資源。

「而activity的生命週期是怎麼實現在死循環體外正常執行的呢?」

其實就是通過這個「handler」,比如onPause方法,當主線程Looper在loop的時候,收到暫停的消息,就會把消息分發給主線程的handleMessage處理,然後最後會調用到activity的onPause方法。 那主線程的消息又是哪裏來的呢?剛纔說到主線程之外還會創建一些binder線程,比如app線程,系統線程,一般是系統線程比如ApplicationThreadProxy傳消息給APP線程ApplicationThread,然後再傳給主線程,也就是ActivityThread所在的線程。

「面試題3:內存泄漏??」

首先爲什麼會發送內存泄漏?handler作爲內部類會持有外部類的引用,當發送延遲消息時,就有可能發生處理消息的時候,activity已經銷燬了,從而導致內存泄漏

怎麼解決?定義靜態內部類,並且在ondestory裏面移除所有消息

直接移除不就行了?還需要靜態內部類?onDestory方法不一定執行哦。如果你的Activity不在棧頂,然後app被後臺強殺,那麼onDestory就不會被執行了。

上代碼:

private static class MemoryLeakSafeHandler extends Handler {

    private WeakReference<HandlerInfoActivity> ref;

    public MemoryLeakSafeHandler(HandlerInfoActivity activity) {
        this.ref = new WeakReference(activity);
    }

    @Override
    public void handleMessage(final Message msg) {
        HandlerInfoActivity activity = ref.get();
        if (activity != null) {
            activity.handleMessage(msg);
        }
    }
}

MemoryLeakSafeHandler handler;

public void handleMessage(Message msg) {

}

@Override
protected void onDestroy() {
    handler.removeCallbacksAndMessages(null);
    super.onDestroy();
}

「面試題4:IdleHandler是什麼,有什麼用呢?」

IdleHandler是在Hanlder空閒時,也就是沒有可處理的消息時候,用來處理空閒任務的一種機制。 有什麼作用呢?主要是用於提升性能,可以在消息隊列空閒時做一些事情,從而不影響到主線程的任務處理。(卑微小弟,你們重要的大哥們先用,我最後再用)。

用法如下:

Looper.myQueue().addIdleHandler(new IdleHandler() {  
    @Override  
    public boolean queueIdle() {  
        //do something
        return false;    
    }  
});

這裏queueIdle方法的返回參數是bool類型,true代表執行一次後不刪除,下次進入空閒時還會調用該回掉方法。false代表執行一次後該IdleHandler就會被刪除。 源碼在MessageQueue類的next方法,其實就是在消息隊列裏面沒有消息的時候會去查詢mIdleHandlers隊列,mIdleHandlers隊列有數據,也就是有IdleHandler就會去處理執行。 還是簡單放下源碼吧:

Message next() {
        for (;;) {
            synchronized (this) {
                // If first time idle, then get the number of idlers to run.
                // Idle handles only run if the queue is empty or if the first message
                // in the queue (possibly a barrier) is due to be handled in the future.
                if (pendingIdleHandlerCount < 0
                        && (mMessages == null || now < mMessages.when)) {
                    pendingIdleHandlerCount = mIdleHandlers.size();
                }
                if (pendingIdleHandlerCount <= 0) {
                    // No idle handlers to run.  Loop and wait some more.
                    mBlocked = true;
                    continue;
                }

                if (mPendingIdleHandlers == null) {
                    mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)];
                }
                mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);
            }

            // Run the idle handlers.
            // We only ever reach this code block during the first iteration.
            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);
                }

                if (!keep) {
                    synchronized (this) {
                        mIdleHandlers.remove(idler);
                    }
                }
            }

        }
    }

有人可能要問了,這玩意真的有用嗎?確實有用,只是你沒用到而已。下面舉例兩個場景

  • 比如我要提升頁面的啓動速度,就可以把onCreate,onResume裏面的一些操作放到IdleHandler裏面執行,減少啓動時間。
  • Leakcanary等三方庫也用到了這個類,用來幹嘛呢?監聽主線程的UI操作已完成。既然都執行到我這裏來了,就說明UI操作都完成了是吧。

「面試題5:同步屏障機制是什麼,有什麼用呢?」

還是看這個next獲取消息的方法:

Message next() {
        for (; ; ) {
            synchronized (this) {
                if (msg != null && msg.target == null) {
                    // Stalled by a barrier.  Find the next asynchronous message in the queue.
                    do {
                        prevMsg = msg;
                        msg = msg.next;
                    } while (msg != null && !msg.isAsynchronous());
                }
                if (msg != null) {
                    if (now < msg.when) {
                        // Next message is not ready.  Set a timeout to wake up when it is ready.
                        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;
                        return msg;
                    }
                } 
            }
        }
    }

可以看到一開始就會判斷這個消息兩個條件:

  • msg不爲空
  • msg的target爲空

那麼這種消息就是屬於同步屏障消息,如果遇到這種消息,就會進入一個dowhile循環,找出消息隊列中的異步消息並返回。 所以這個同步屏障機制就是爲了讓handler能夠先執行異步消息,再去執行同步消息,直到屏障被移除。 慢着,我之前咋沒聽過還有異步消息?哈哈。確實是有的,Message有個setAsynchronous方法,如果傳入true,就代表這個消息是個異步消息,在同步屏障發生後就可以先執行。目的是爲了插入一些比較重要的消息需要先行處理。

具體使用方法就是:

  • postSyncBarrier方法加入屏障
  • removeSyncBarrier移除屏障 但是這兩個方法都已經標記爲hide了,要使用的話必須使用反射。

ok,瞭解完之後又該有人問了,有什麼用呢?這個同步屏障。如果你看過view繪製的源碼,你就會發現ViewRootImpl類中就用到了這個,由於view繪製的優先級比較高,所以開啓同步屏障後,就可以選擇讓某些操作先執行,提高了優先級,比如這個view的繪製處理操作。

咦,這個機制感覺跟之前的IdleHandler是對應關係啊,一個是卑微小弟?一個是在線大哥?

「面試題6:Handler機制還還還有什麼其他的用處或者實際應用嗎?」

當然有啦,舉🌰:

BlockCanary

一個用來檢測應用卡頓耗時的三方庫。它的原理就在於Handler相關的Looper類裏面,上面說過,Activity的生命週期都是依靠主線程的Looper.loop(),所以主線程的操作基本都是在handler處理中發生的。那有沒有什麼辦法可以查看handler處理的耗時時間呢?如果知道了耗時時間不就知道哪裏卡頓了?上源碼:

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

        ...
    }
}

    /**
     * Handle system messages here.
     */
    public void dispatchMessage(@NonNull Message msg) {
        if (msg.callback != null) {
            handleCallback(msg);
        } else {
            if (mCallback != null) {
                if (mCallback.handleMessage(msg)) {
                    return;
                }
            }
            handleMessage(msg);
        }
    }

在loop方法內,有個Printer類,用來打印日誌。我們可以看到在dispatchMessage方法前後分別打印了一次日誌,而dispatchMessage方法就是Handler用來處理消息的地方。那麼,我們如果能獲取到這兩次打印日誌的時間差,不就可以得到Handler處理消息的耗時時間了?所以我們直接替換這個Printer就可以達到我們的目的了:

Looper.getMainLooper().setMessageLogging(mainLooperPrinter);

這麼簡單有效的方法,讓我不得不給BlockCanary作者點個👍

IntentService

IntentService 是一個繼承自Service,自帶工作線程,並且線程任務結束後自動銷燬的一個類。Service是啥?可以統一管理後臺任務,運行在前臺,所以可以獲取到上下文。 而IntentService同樣具有這些特性,並且可以在新線程管理任務,工作執行完自動銷燬。就線程池來說區別就在與IntentService擁有Service的特性,所以在需要用到上下文的時候就可以選擇IntentService。而IntentService內部其實就是用到了HandlerThread,也就是帶有Handler機制的線程。還是來點源碼:

@Override
    public void onCreate() {
        super.onCreate();
        //創建新線程並start
        HandlerThread thread = new HandlerThread("IntentService[" + mName + "]");
        thread.start();
        mServiceLooper = thread.getLooper();
        //創建新線程對應的handler
        mServiceHandler = new ServiceHandler(mServiceLooper);
    }

    @Override
    public void onStart(@Nullable Intent intent, int startId) {
       //service啓動後發送消息給handler
        Message msg = mServiceHandler.obtainMessage();
        msg.arg1 = startId;
        msg.obj = intent;
        mServiceHandler.sendMessage(msg);
    }

    private final class ServiceHandler extends Handler {
        public ServiceHandler(Looper looper) {
            super(looper);
        }
        @Override
        public void handleMessage(Message msg) {
                //handler收到消息後調用onHandleIntent方法
            onHandleIntent((Intent)msg.obj);
            stopSelf(msg.arg1);
        }
    }

源碼解析

經過上面的知識點講解,大家應該都大致瞭解了Handler內部原理,最後我們就跟隨源碼再複習一遍。 我們之前瞭解到,其實Handler真正做事其實就是兩個方法:

  • sendEmptyMessage(Handler發送消息)
  • loop (Looper開始循環獲取消息)

sendMessage

上源碼:

public final boolean sendMessage(@NonNull Message msg) {
        return sendMessageDelayed(msg, 0);
    }

    public final boolean sendEmptyMessage(int what)
    {
        return sendEmptyMessageDelayed(what, 0);
    }

    public final boolean sendEmptyMessageDelayed(int what, long delayMillis) {
        Message msg = Message.obtain();
        msg.what = what;
        return sendMessageDelayed(msg, delayMillis);
    }

    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;
        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方法中,那我們就繼續看看enqueueMessage`方法

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

enqueueMessage`方法有兩個重要的點:

  • msg.target = this,指定了消息的target對象爲handler本身
  • queue.enqueueMessage(msg, uptimeMillis),執行了MessageQueueenqueueMessage方法。

ok,繼續往下看

//MessageQueue.java
    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) {
                // 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;
    }

enqueueMessage`這個方法主要做了兩件事:

1、插入消息,msg。通過一個循環,找出msg應該插入的位置(按照時間排序),然後插入msg到mMessages(消息隊列)

2、喚醒消息隊列。消息隊列在沒有消息的時候,會阻塞在queue.next()方法這裏,所以來了消息就要喚醒線程。這裏的阻塞和喚醒主要依靠底層的epoll 機制,具體我也不太懂,有懂得大神可以在評論區留言😁

既然有了消息,那麼Looper那端就要取消息了,怎麼取的?就是我們要說的第二個重要方法loop

loop

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

            try {
                msg.target.dispatchMessage(msg);
            } catch (Exception exception) {
                throw exception;
            } finally {
            }

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

        }
    }

     /**
     * Handle system messages here.
     */
    public void dispatchMessage(@NonNull Message msg) {
        if (msg.callback != null) {
            handleCallback(msg);
        } else {
            if (mCallback != null) {
                if (mCallback.handleMessage(msg)) {
                    return;
                }
            }
            handleMessage(msg);
        }
    }

這裏截取了部分代碼,可以看到,loop方法通過一個死循環,不斷的從MessageQueue獲取消息,並且通過msg.target的dispatchMessage方法進行處理,target上文說過也就是消息對應的Handler。 而dispatchMessage方法最後也會調用到handler的handleMessage方法了。至此,流程已走通。

ok,還剩最後一個重要的點沒說了。就是到底MessageQueue是怎麼取出消息的呢?

  • 死循環獲取消息
  • 遇到同步屏障消息,就優先處理異步消息(上文知識點)
  • 隊列空閒時就開啓IdleHandler機制處理任務。(上文知識點)

代碼貼上

//MessageQueue.java
    Message next() {
        for (;;) {
            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) {
                    // Stalled by a barrier.  Find the next asynchronous message in the queue.
                    do {
                        prevMsg = msg;
                        msg = msg.next;
                    } while (msg != null && !msg.isAsynchronous());
                }
                if (msg != null) {
                    if (now < msg.when) {
                        // Next message is not ready.  Set a timeout to wake up when it is ready.
                        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;
                        if (DEBUG) Log.v(TAG, "Returning message: " + msg);
                        msg.markInUse();
                        return msg;
                    }
                } else {
                    // No more messages.
                    nextPollTimeoutMillis = -1;
                }

                // If first time idle, then get the number of idlers to run.
                // Idle handles only run if the queue is empty or if the first message
                // in the queue (possibly a barrier) is due to be handled in the future.
                if (pendingIdleHandlerCount < 0
                        && (mMessages == null || now < mMessages.when)) {
                    pendingIdleHandlerCount = mIdleHandlers.size();
                }
                if (pendingIdleHandlerCount <= 0) {
                    // No idle handlers to run.  Loop and wait some more.
                    mBlocked = true;
                    continue;
                }

                if (mPendingIdleHandlers == null) {
                    mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)];
                }
                mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);
            }

            // Run the idle handlers.
            // We only ever reach this code block during the first iteration.
            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);
                }

                if (!keep) {
                    synchronized (this) {
                        mIdleHandlers.remove(idler);
                    }
                }
            }

        }
    }

至此,Handler的大概已經瞭解的差不多了,是不是覺得Handler太神奇了,你也忍不住想去好好看看它的源碼了呢?也許還有一些功能沒被利用起來,等着你去發現🚀🚀🚀

有說的不對的地方望指正,謝謝。🙏

面試前的知識梳理,儲備提升

自己的知識準備得怎麼樣,這直接決定了你能否順利通過一面和二面,所以在面試前來一個知識梳理,看需不需要提升自己的知識儲備是很有必要的。

關於知識梳理,這裏再分享一下我面試這段時間的複習路線:(以下體系的複習資料是我從各路大佬收集整理好的)

  • 架構師築基必備技能:深入Java泛型+註解深入淺出+併發編程+數據傳輸與序列化+Java虛擬機原理+反射與類加載+動態代理+高效IO

  • Android高級UI與FrameWork源碼:高級UI晉升+Framework內核解析+Android組件內核+數據持久化

  • 360°全方面性能調優:設計思想與代碼質量優化+程序性能優化+開發效率優化

  • 解讀開源框架設計思想:熱修復設計+插件化框架解讀+組件化框架設計+圖片加載框架+網絡訪問框架設計+RXJava響應式編程框架設計+IOC架構設計+Android架構組件Jetpack

  • NDK模塊開發:NDK基礎知識體系+底層圖片處理+音視頻開發

  • 微信小程序:小程序介紹+UI開發+API操作+微信對接

  • Hybrid 開發與Flutter:Html5項目實戰+Flutter進階

知識梳理完之後,就需要進行查漏補缺,所以針對這些知識點,我手頭上也準備了不少的電子書和筆記,這些筆記將各個知識點進行了完美的總結。

3.項目覆盤

實際上,面試的一二輪所問到的技術問題,很多都是圍繞着你的項目展開,因此在面試前最後要做好的一件事情就是項目覆盤。關於項目覆盤,我個人的思路如下,可供參考:

  • 你在這個項目中承擔了什麼樣的角色?
  • 這個項目的背景是什麼,如果是技術項目,爲什麼要做?
  • 有哪些技術難點,是怎麼解決的,是否還有更好的方案?
  • 你認爲項目中是否有可以改進的點?
  • 這個項目解決了什麼問題,最好用數據說話,這個數據又是怎麼得出來的?

提前把思路捋一捋,上面這些問題好好思考或準備一下,做到心中有譜以後,自然能夠面試官聊得融洽,保持一個好的心態,通過的機率就會更大一些。

資料太多,全部展示會影響篇幅,暫時就先列舉這些部分截圖,以上資源均免費分享,以上內容均放在了開源項目:github 中已收錄,大家可以自行獲取(或者關注主頁掃描加微信獲取)。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章