Android中Handler問題匯總【面試必備】

2021-03-02 秦子帥
地址 |  juejin.im/post/5e886bc3e51d4546fc795793

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(Runable)的區別

要看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.Handler機制,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。

那麼,如果在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();

推薦閱讀:

玩玩Flutter的拖拽-實現一款萬能遙控器
快速實現Android版抖音主界面的心得


感興趣的在看走一波哦

---END---



相關焦點

  • 面試必備:異步 Handler 十大必問!
    ,關於 Handler 的問題是必備的,主要是以下十個:Handler 的基本原理子線程中怎麼使用 HandlerMessageQueue 獲取消息是怎麼等待為什麼不用 wait 而用 epoll 呢?
  • Android Handler 由淺入深源碼全解析
    前言    Handler在android開發中可謂隨處可見,不論你是一個剛開始學習android
  • Android開發 面試必問的Handler消息機制
    最近項目提測了也閒了下來看到Handler就想起面試必問,Handler機制相信大家每個人面試的時候都被問到吧,就來總結一下看看,話不多說先看流體圖:
  • Android開發必備的「80」個開源庫
    http://hukai.me/android-performance-patterns/Android 內存洩漏總結https://www.jianshu.com/p/cdc6d2e664f1避免 Android 中 Context 引起的內存洩露https://zhuanlan.zhihu.com/p/24974982?
  • Handler原理,這一篇就夠了
    其實大概都是對的,之前面試的時候,我也都是這麼說,也沒有面試官深入問過,這次正好有時間深入源碼系統學習下,畢竟還是要知其所以然。2.使用方法package com.example.test.myapplication;import android.app.Activity;import android.os.Bundle;import android.os.Handler;import android.os.Message;import android.widget.Toast
  • 重學 Android 之 Handler 機制
    handler 機制的使用幾乎隨處可見,作為面試中的常客,我們真的了解 handler 嗎?想必很多同學會想,當然了,handler 機制不就是 Looper 、handler  MessageQueue 嗎?下面拋磚引玉,我們先來看看這些問題為什么子線程中不可以直接 new Handler() 而主線程中可以?為什麼建議使用 Message.obtain() 來創建 Message 實例?Looper 在主線程中死循環,為啥不會 ANR ?
  • Android開發:HandlerThread是什麼?
    下面源碼會解釋 mHandlerThread.start(); // 3、將handlerThread與Handler綁定在一起。從事軟體開發的我們,絕不能僅僅停留在API使用層面,必須得進入源碼,一探究竟,看看HandlerThread內部到底幹了什麼,這裡有一點需要注意,看HandlerThread的源碼之前,你必須得先搞懂Handler哦,在分析過程中有關handler的機制不會贅述了。
  • Handler 使用詳解
    在Android中UI修改只能通過UI Thread,子線程不能更新UI。如果子線程想更新UI,需要通過 Handler發送消息給主線程,進而達到更新UI的目的。二、Handler 消息處理機制原理當Android 應用程式創建的時候,系統會給每一個進程提供一個Looper,Looper 是一個死循環,它內部維護一個消息隊列,Looper不停的從消息隊列中取Message,取到的消息就發送給handler,最後Handler 根據接收的消息去修改UI等。
  • 助攻面試:圖解Android Binder機制
    這些問題只是了解binder機制是不夠的,需要從Android的整體系統出發來分析,在我找了很多資料後,真正的弄懂了binder機制,相信看完這篇文章大家也可以弄懂binder機制。$ adb shell service list Found 71 services: 0 sip: [android.net.sip.ISipService] 1 phone: [com.android.internal.telephony.ITelephony] … 20 location: [android.location.ILocationManager
  • 介紹一下 Android Handler 中的 epoll 機制?
    介紹一下 Android Handler 中的 epoll 機制?目錄:IO 多路復用IO 多路復用是一種同步 IO 模型,實現一個線程可以監視多個文件句柄。Handler 中的 epoll 源碼分析主要分析 MessageQueue.java 中的三個 native 函數:private native static long nativeInit(); //返回 ptrprivate native void nativePollOnce
  • 【福利大放送】不止是Android,Github超高影響力開源大放送,學習開發必備教科書
    3、awesomehttps://github.com/sindresorhus/awesome        GitHub 上有各種 awesome 系列,簡單來說就是這個系列搜羅整理了 GitHub 上各領域的資源大匯總,比如有 awesome-android, awesome-ios, awesome-java, awesome-python
  • 你真的了解Handler嗎
    這次我們說下Android中最常見的Handler,通過解析面試點或者知識點,帶你領略Handler內部的神奇之處。❞先上一張總結圖,看看你是否了解真正的HandlerHanlder重要知識點.jpg基本的用法和工作流程用法很簡單,定義一個handler,重寫handleMessage方法處理消息,用send系列方法發送消息。
  • 常見面試第四題之requestLayout, invalidate和postInvalidate的異同
    今天我們來講講在面試當中最常見的,最常常被問到的第四題,近期由於小編工作比較忙碌,更新的比較緩慢還請大家見諒。我相信大家在面試當中肯定會經常被問題view的重繪的問題,比如說:怎樣重新自定義一個控制項了?
  • 月薪20+的Android面試都問些什麼?(含答案)
    Android高階面試寶典,供大家學習 !為了避免這個問題,我們可以自定義的Handler聲明為靜態內部類形式,然後通過弱引用的方式,讓Handler持有外部類的引用,從而可避免內存洩漏問題。然後當在子線程中需要進行更新UI的操作,我們就創建一個Message對象,並通過handler發送這條消息出去。之後這條消息被加入到MessageQueue隊列中等待被處理,通過Looper對象會一直嘗試從Message Queue中取出待處理的消息,最後分發會Handler的handler Message()方法中。
  • 5分鐘了解Handler錯誤使用場景
    其次回調handler構造函數中的callback。    最後回調handler handleMessage()。Android系統中所有的操作均通過Handler添加事件到事件隊列,Looper循環去隊列去取事件進行執行。如果主線程事件反饋超過了5秒則提示ANR。    如果沒有事件進來,基於Linux pipe/epoll機制會阻塞loop方法中的queue.next()中的nativePollOnce()不會報ANR。
  • 2020考研:複試網絡面試常見問題匯總
    2020考研:複試網絡面試常見問題匯總 今年受到疫情的影響考研複試的時間都有所調整,
  • Android Handler原理詳解
    如果在子線程中需要更新 UI,一般是通過 Handler 發送消息,主線程接受消息並且進行相應的邏輯處理。除了直接使用 Handler,還可以通過 View 的 post 方法以及 Activity 的 runOnUiThread 方法來更新 UI,它們內部也是利用了 Handler。
  • Android消息機制之Looper、Handler、MessageQueen
    結語前言Android消息機制可以說是我們Android工程師面試題中的必考題,弄懂它的原理是我們避不開的任務,所以長痛不如短痛,花點時間幹掉他,廢話不多說,開車啦Android消息機制的簡介在安卓開發中,常常會遇到獲取數據後更新UI的問題,比如:在獲取網絡信息後,需要彈出一個Toast
  • 安卓架構師必備之Android AOP面向切面編程詳解,超實用!
    而且類似的還有網絡判斷,權限管理,Log日誌的統一管理這樣的問題。那麼,我們也沒有更優雅的方式來解決這一類的問題呢,答案是有的,煩請各位接著往下看。它和我們平時接觸到的OOP都是編程的不同思想,OOP,即『面向對象編程』,它提倡的是將功能模塊化,對象化,而AOP的思想,則不太一樣,它提倡的是針對同一類問題的統一處理,通過預編譯方式和運行期動態代理實現程序功能的統一維護的一種技術。AOP是OOP的延續,是軟體開發中的一個熱點,也是Spring框架中的一個重要內容,是函數式編程的一種衍生範型。
  • Android四大組件的onCreate/onReceiver方法中Thread.sleep(),會產生幾個ANR?
    3、然後問題中的四大組件是否會產生ANR屬於上面場景中的Service Timeout和Broadcast Timeout,所以一共會產生兩個ANR。下面我們從代碼層面去驗證一下這幾個場景:驗證標題的疑問下面我們開始實踐一下,分別在四大組件和Application的onCreate方法中進行Thread.sleep(21_000)。