Handler面試那些事

1、能講講Android的handler機制嗎?

消息處理機制本質:<strong>一個線程開啟循環模式持續監聽并依次處理其它線程發送給它的消息.</strong>
簡單的說:Android應用程序是通過消息來驅動的,系統為每個應用程序維護一個消息隊列(MessageQueue),應用程序的主線程不斷地(Looper)從這個消息隊列中獲取消息(Message),然后對這些消息進行處理(Handler),這樣就實現了通過消息來驅動應用程序的執行.

2、Android消息處理機制的工作原理

3、Handler、Looper、MessageQueue、Message之間的關系.

Handler:

消息的發送和處理者,一般使用sendMessage()和handleMessage()方法來發送和處理消息。

Message:

線程之間傳遞的消息,它可以在內部攜帶少量的信息,用于在不同線程之間交換數據。

MessageQueue:

用于存放所有的Handler發送的消息的隊列(單鏈表),這些消息會一直存在于消息隊列中等待被處理,消息的處理遵循先進先出原則。
每個線程中只有一個MessageQueue對象。

Looper:

循環監聽MessageQueue中是否存在消息,如果存在就會將消息取出并傳遞給Handler處理。
每個線程中只有一個Looper對象。

4、ThreadLocal是什么?Android如何保證一個線程中最多只有一個Looper,一個MessageQueue?

ThreadLocal可以在不同的線程之中互不干擾的存儲并提供數據。
工作原理:每個Thread維護一個ThreadLocalMap映射表,這個映射表的key是ThreadLocal實例本身,value是真正要存儲的Object。每次獲取或者設置value都是對該ThreadLocalMap映射表進行的操作,是與其他線程分開的。

在Handler的實現機制中,默認通過Looper類的prepare()方法來創建Looper對象并將其存儲在ThreadLocal中(實際存儲在了當前Thread所維護的ThreadLocalMap中,key為sThreadLocal,值為新建的Looper對象),這就保證了當前線程中有且僅有一個Looper對象。而Looper對象的創建同時也伴隨著MessageQueeu對象的創建,自然而然也確定了其唯一性。具體代碼如下:

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

private static void prepare(boolean quitAllowed) {
    //如果線程的TLS已有數據,則會拋出異常,一個線程只能有一個Looper,prepare不能重復調用。
    if (sThreadLocal.get() != null) {
        throw new RuntimeException("Only one Looper may be created per thread");
    }
    //往線程的TLS插入數據,簡單理解相當于map.put(sThreadLocal,new Looper(quitAllowed));
    sThreadLocal.set(new Looper(quitAllowed));
}

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

5、怎樣實現一個帶有消息循環(Looper)的線程?

Android系統的UI線程就是一種帶有消息循環(Looper)機制的線程,這種線程可以綁定Handler對象,并通過Handler的sendMessage()函數向線程發送消息。

private Handler mHandler;
private Looper mLooper;

private void createLooperThread(){
    new Thread(new Runnable() {
        @Override
        public void run() {
            // Log.e("MainActivity",Thread.currentThread().getName());
            Looper.prepare(); //創建Looper和MessageQueue對象
            mLooper = Looper.myLooper(); // 獲取當前線程下的Looper對象
            createHandler();
            Looper.loop(); // 開啟Looper循環 由于loop()里面是個死循環,有消息就處理,沒消息就掛起休眠,因此此行代碼之后的代碼是無法運行的。只有調用mLooper.quit()方法后,loop才會中止,其后的代碼才能得以運行。
        }
    }).start();
}

// 創建屬于 mLooper對象所在線程 的Handler對象
private void createHandler(){
    mHandler = new Handler(mLooper){
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            // Log.e("MainActivity",Thread.currentThread().getName());     
        };
    }
}

6、談談對HandlerThread的理解

HandlerThread本質上就是一個普通Thread,只不過在內部建立了Looper循環,它的實現很簡單,就是在run方法中通過Loop.prepare()來創建消息隊列,并通過Loop.loop()來開啟消息循環,這樣在實際的使用中就允許在HandlerThread中創建Handler。
HandlerThread比較適用于單線程+異步隊列的場景,比如一個長時間運行且沒有 UI 交互的任務,就像在將用戶數據上傳到服務器前進行的數據壓縮的操作就適合用HandlerThread。
另:IntentService內部就是通過HandlerThread來實現的。
HandlerThread的使用方法如下:

private HandlerThread mHandlerThread;
private Handler mHandler;

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

    // 創建一個線程
    mHandlerThread = new HandlerThread("handlerthread");
    // 開啟一個線程
    mHandlerThread.start();
    mHandler = new Handler(mHandlerThread.getLooper()){
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            // 這個方法運行在mHandlerThread線程中,可執行耗時操作
            Log.d("handler","消息:" + msg.what + " 線程:" + Thread.currentThread().getName());
        }
    };

    //在主線程給handler發送消息
    mHandler.sendEmptyMessage(1);

    new Thread(new Runnable() {
        @Override
        public void run() {
            //在子線程給handler發送數據
            mHandler.sendEmptyMessage( 2 ) ;
        }
    }).start() ;
}

@Override
protected void onDestroy() {
    super.onDestroy();
    mHandlerThread.quit();
}

7、發送消息有哪些方法?

Handler類 - sendxxx()方法:
public final boolean sendMessage(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(Message msg, long delayMillis) {
    if (delayMillis < 0) {
        delayMillis = 0;
    }
    return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
}

public final boolean sendEmptyMessageAtTime(int what, long uptimeMillis) {
    Message msg = Message.obtain();
    msg.what = what;
    return sendMessageAtTime(msg, uptimeMillis);
}

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

public final boolean sendMessageAtFrontOfQueue(Message msg) {
    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, 0);
}

private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
    msg.target = this;
    if (mAsynchronous) {
        msg.setAsynchronous(true);
    }
    return queue.enqueueMessage(msg, uptimeMillis);
}
Handler類 - postxxx()方法:
public final boolean post(Runnable r) {
   return  sendMessageDelayed(getPostMessage(r), 0);
}

public final boolean postAtTime(Runnable r, long uptimeMillis) {
    return sendMessageAtTime(getPostMessage(r), uptimeMillis);
}

public final boolean postAtTime(Runnable r, Object token, long uptimeMillis) {
    return sendMessageAtTime(getPostMessage(r, token), uptimeMillis);
}

public final boolean postDelayed(Runnable r, long delayMillis) {
    return sendMessageDelayed(getPostMessage(r), delayMillis);
}

public final boolean postAtFrontOfQueue(Runnable r) {
    return sendMessageAtFrontOfQueue(getPostMessage(r));
}

private static Message getPostMessage(Runnable r) {
    Message m = Message.obtain();
    m.callback = r;
    return m;
}

private static Message getPostMessage(Runnable r, Object token) {
    Message m = Message.obtain();
    m.obj = token;
    m.callback = r;
    return m;
}
Activity類 - runOnUiThread()方法:
public final void runOnUiThread(Runnable action) {
    if (Thread.currentThread() != mUiThread) {
        mHandler.post(action);
    } else {
        action.run();
    }
}
View類 - postxxx()方法:
public boolean post(Runnable action) {
    final AttachInfo attachInfo = mAttachInfo;
    if (attachInfo != null) {
        return attachInfo.mHandler.post(action);
    }

    // Postpone the runnable until we know on which thread it needs to run.
    // Assume that the runnable will be successfully placed after attach.
    getRunQueue().post(action);
    return true;
}

public boolean postDelayed(Runnable action, long delayMillis) {
    final AttachInfo attachInfo = mAttachInfo;
    if (attachInfo != null) {
        return attachInfo.mHandler.postDelayed(action, delayMillis);
    }

    // Postpone the runnable until we know on which thread it needs to run.
    // Assume that the runnable will be successfully placed after attach.
    getRunQueue().postDelayed(action, delayMillis);
    return true;
}

8、插入消息的流程?

分析上述發送消息的方法,不難發現消息的發送最終都會到enqueueMessage()方法:

// 向消息隊列中插入消息
boolean enqueueMessage(Message msg, long when) {
     // 判斷消息接收者handler是否為null
    if (msg.target == null) {
        throw new IllegalArgumentException("Message must have a target.");
    }
    if (msg.isInUse()) { // 判斷msg是否正在被使用
        throw new IllegalStateException(msg + " This message is already in use.");
    }

    synchronized (this) {
        if (mQuitting) { // MessageQueue處于quit狀態
            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為使用狀態
        msg.when = when;
        Message p = mMessages;
        boolean needWake;
        if (p == null || when == 0 || when < p.when) { // 若:消息隊列為空 or 欲在消息頭插入消息 or 欲插入的消息先于消息頭的消息執行,則:插入消息
            // 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) { // 若已輪詢到消息隊列尾 or 欲插入的消息先于輪詢到的消息執行 則:跳出循環 -> 插入消息 
                    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;
}

9、取出消息->處理消息的流程?

// 消息隊列的循環遍歷
public static void loop() {
    final Looper me = myLooper();
    if (me == null) {
        throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
    }
    final MessageQueue queue = me.mQueue;

    // Make sure the identity of this thread is that of the local process,
    // and keep track of what that identity token actually is.
    Binder.clearCallingIdentity();
    final long ident = Binder.clearCallingIdentity();

    for (;;) {
        Message msg = queue.next(); // might block
        if (msg == null) {
            // No message indicates that the message queue is quitting.
            return;
        }

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

        final long traceTag = me.mTraceTag;
        if (traceTag != 0) {
            Trace.traceBegin(traceTag, msg.target.getTraceName(msg));
        }
        try {
            msg.target.dispatchMessage(msg); // 關鍵語句 調用handler的dispatchMessage()方法
        } finally {
            if (traceTag != 0) {
                Trace.traceEnd(traceTag);
            }
        }

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

        // Make sure that during the course of dispatching the
        // identity of the thread wasn't corrupted.
        final long newIdent = Binder.clearCallingIdentity();
        if (ident != newIdent) {
            Log.wtf(TAG, "Thread identity changed from 0x"
                    + Long.toHexString(ident) + " to 0x"
                    + Long.toHexString(newIdent) + " while dispatching to "
                    + msg.target.getClass().getName() + " "
                    + msg.callback + " what=" + msg.what);
        }

        msg.recycleUnchecked();
    }
}

public void dispatchMessage(Message msg) {
    if (msg.callback != null) { // 發送消息 如調用了post(Runnable r)等系列方法(會執行 msg.callback = runnable; 操作),則此處不為null
        handleCallback(msg);
    } else {
        if (mCallback != null) { // 創建Handler對象時作為參數傳遞進來,可以用來實現消息的攔截
            if (mCallback.handleMessage(msg)) {
                return;
            }
        }
        handleMessage(msg);
    }
}

private static void handleCallback(Message message) {
    message.callback.run();
}

10、如何實現Handler消息攔截(不接收消息)

private Handler handler = new Handler(new Callback(){
  @Override        
  public boolean handleMessage(Message msg) {            
     return true; // 設置true攔截消息
  }   
 }){        
  @Override        
  public void handleMessage(Message msg) {                
     // 根據消息類型對消息進行處理
  }    
}; 

相信聰明的讀者熟悉問題9中的代碼后自然會一目了然此間原理

11、消息機制中的消息池實現原理,消息池為什么不會引起OOM?

通常,我們使用Message.obtain()從消息池中獲取Message,避免直接構造Message

public static Message obtain() {
    synchronized (sPoolSync) {
        if (sPool != null) { // 消息池鏈表頭部Message不為空  出池
            Message m = sPool; // 取出鏈表頭部Message
            sPool = m.next; // 鏈表的下一個Message為新的鏈表頭
            m.next = null;
            m.flags = 0; // clear in-use flag
            sPoolSize--; // 鏈表池大小 -1
            return m;
        }
    }
    return new Message(); // 消息池中沒有Message則重新構造
}

那么消息池中的消息哪里來的呢?我們知道,消息池的主要作用是消息的復用,那就只有當一個消息被new出來并使用結束后,才會進入消息池,也就是這個消息被回收到池中,等待復用,我們找找消息使用結束被回收的函數recycle()

public void recycle() {
    if (isInUse()) { // 消息處于正在處于使用狀態
        if (gCheckRecycle) {
            throw new IllegalStateException("This message cannot be recycled because it "
                    + "is still in use.");
        }
        return;
    }
    recycleUnchecked();
}

void recycleUnchecked() {
    // Mark the message as in use while it remains in the recycled object pool.
    // 把這個Message所有成員賦值成最初的狀態
    flags = FLAG_IN_USE;
    what = 0;
    arg1 = 0;
    arg2 = 0;
    obj = null;
    replyTo = null;
    sendingUid = -1;
    when = 0;
    target = null;
    callback = null;
    data = null;

    synchronized (sPoolSync) { // 入池
        if (sPoolSize < MAX_POOL_SIZE) {
            next = sPool;
            sPool = this;
            sPoolSize++;
        }
    }
}

看到這,相信你已經知道了消息是怎樣加到消息池和怎樣從消息池中取出:消息在使用結束recycle的時候入池,在下次obtain消息的時候從消息池中取出。

那么Android會因為Message Pool緩存的Message對象而造成OOM嗎?

對于這個問題,我可以明確的說APP不會因Message Pool而OOM
我們知道消息池中保存的最大消息數為MAX_POOL_SIZE表示鏈表的最大長度為50,當我們執行入池操作時:

  • 將待回收的Message對象字段置空(避免因Message過大,使靜態的消息池內存泄漏)。因此無論原先的Message對象有多大,最終被緩存進Message Pool前都被置空,那么這些緩存的Message對象所占內存大小對于一個app內存來說基本可以忽略。所以說,Message Pool并不會造成OOM。
  • 以內置鎖的方式(線程安全),判斷當前線程池的大小是否小于50。若小于50,直接將Mesaage插入到消息池鏈表尾部;若大于等于50,則直接丟棄掉,那么這些被丟棄的Message將交由GC處理。

12、主線程中的Looper.loop()一直無限循環為什么不會造成ANR?

造成ANR的原因一般有兩種:
  1. 當前的事件沒有機會得到處理(即主線程正在處理前一個事件,沒有及時的完成或者looper被某種原因阻塞住了);
  2. 當前的事件正在處理,但沒有及時完成。

因為Android 的是由事件驅動的,looper.loop() 不斷地接收事件、處理事件,每一個點擊觸摸或者說Activity的生命周期都是運行在 Looper.loop() 的控制之下,如果它停止了,應用也就停止了。只能是某一個消息或者說對消息的處理阻塞了 Looper.loop(),而不是 Looper.loop() 阻塞它。
而且主線程Looper從消息隊列讀取消息,當讀完所有消息時,主線程阻塞。子線程往消息隊列發送消息,并且往管道文件寫數據,主線程即被喚醒,從管道文件讀取數據,主線程被喚醒只是為了讀取消息,當消息讀取完畢,再次睡眠。因此loop的循環并不會對CPU性能有過多的消耗。

<b>也就說我們的代碼其實就是在這個循環里面去執行的,當然不會阻塞了。</b>

13、Handler內存泄漏的問題

造成內存泄漏的原因:
  1. Handler的生命周期與Activity不一致;
  2. Handler引用Activity阻止了GC對Activity的回收。
解決方案:

使用顯式引用:1.靜態內部類。 2. 外部類
使用弱引用:WeakReference

具體代碼:
private static class MyHandler extends Handler {

    private final WeakReference<MainActivity> mActivity;  

    public MyHandler(MainActivity activity) {  
        mActivity = new WeakReference< MainActivity >(activity);  
    }  

    @Override  
    public void handleMessage(Message msg) {  
        System.out.println(msg);  
        if (mActivity.get() == null) {  
            return;  
        }  
        mActivity.get().todo();  
    }  
}  

@Override  
public void onDestroy() {  
    //  If null, all callbacks and messages will be removed.  
    mHandler.removeCallbacksAndMessages(null);  
}  

未完待續...

參考文集:
Android 消息處理機制(Looper、Handler、MessageQueue,Message)
Android的消息機制之ThreadLocal的工作原理
Android面試:主線程中的Looper.loop()一直無限循環為什么不會造成ANR?

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,362評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,577評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,486評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,852評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,600評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,944評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,944評論 3 447
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,108評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,652評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,385評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,616評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,111評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,798評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,205評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,537評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,334評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,570評論 2 379

推薦閱讀更多精彩內容

  • 一、提出問題 面試時常被問到的問題: 簡述 Android 消息機制 Android 中 Handler,Loop...
    崽子豬閱讀 1,533評論 0 10
  • 異步消息處理線程啟動后會進入一個無限的循環體之中,每循環一次,從其內部的消息隊列中取出一個消息,然后回調相應的消息...
    cxm11閱讀 6,442評論 2 39
  • 前言 在Android開發的多線程應用場景中,Handler機制十分常用 今天,我將手把手帶你深入分析Handle...
    BrotherChen閱讀 479評論 0 0
  • 系列文章Android面試攻略(1)——Android基礎Android面試攻略(2)——異步消息處理機制Andr...
    黎清海閱讀 1,364評論 0 10
  • 這一夜我睡得很不安穩,醒來時曙光初現,我只歇了兩個時辰,卻再也睡不著。許久未曾做夢,昨夜竟陷入了一個奇異的夢境。 ...
    棲云歌行閱讀 291評論 5 3