Android必知必會——消息機制

概述

相信不論是出入Android,仍是已開發多年的老司機們,確定都對Android的Handler不會陌生,而它就是今天要介紹的Android消息機制中的一部分。html

在Android系統中,有兩大特點利劍:Binder IPC機制和消息機制。Android也由大量的消息驅動方式來交互,大到四大組件的工做流程,小到異步回調更新UI等等,各處都有消息機制的存在。git

角色

在對消息機制進行分析以前,先來看一下消息機制中,都含有哪些角色以及他們各自的做用又是什麼:bash

  • Message異步

    消息本體,一切邏輯都圍繞它來展開。ide

  • MessageQueueoop

    消息隊列,管理消息的入隊和出隊。post

  • Handlerui

    消息機制的兩端,可做爲消息產生端,也可做爲消息消費端。this

  • Looperspa

    消息機制運轉的動力,不斷的循環執行,取出消息、分發消息。

他們之間的關係,能夠經過一個簡單圖來表示一下:

Handler

在消息機制的四個角色中,咱們常用和見到的就是Handler了,那就先從Handler看起。

構造

Handler有不少構造方法,可是可用開發中使用的只有以下幾個:

  • Handler()

  • Handler(@Nullable Callback callback)

  • Handler(@NonNull Looper looper)

  • Handler(@NonNull Looper looper, @Nullable Callback callback)

這樣來看無非是可設置兩個參數:Looper和Callback。

若是不指定Looper,在構造時會經過Looper.myLooper()獲取當前線程的Looper,若是當前線程沒有Looper那麼會拋出異常。

mLooper = Looper.myLooper();
if (mLooper == null) {
    throw new RuntimeException(
        "Can't create handler inside thread " + Thread.currentThread()
        + " that has not called Looper.prepare()");
}
複製代碼

至於Callback,後面會進行分析。

做爲生產者生產Message

Handler的sendEmptyMessage、sendEmptyMessageAtTime、sendEmptyMessageDelayed、sendMessage和sendMessageDelayed,最終都會調用sendMessageAtTime:

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

這其中的uptimeMillis = SystemClock.uptimeMillis() + delayMillis,之因此採用SystemClock.uptimeMillis(),是由於它是已開機時間,而若是使用System.currentTimeMillis()在用戶修改手機時間時,該值就會發生變化。

除了上面的sendMessageAtTime,還有一個特殊的方法sendMessageAtFrontOfQueue:

public final boolean sendMessageAtFrontOfQueue(@NonNull 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;
        }
        //固定uptimeMillis爲0
        return enqueueMessage(queue, msg, 0);
    }
複製代碼

能夠發現這兩類方法最終都會經過調用方法:

private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,
            long uptimeMillis) {
    //將msg的target屬性指向當前Handler
    msg.target = this;
    msg.workSourceUid = ThreadLocalWorkSource.getUid();

    if (mAsynchronous) {
        msg.setAsynchronous(true);
    }
    //轉到MessageQueue,消息入隊
    return queue.enqueueMessage(msg, uptimeMillis);
}
複製代碼

做爲消費者消費Message

當Looper在經過MessageQueue讀取到下一條消息時,就會經過handler的dispatchMessage分發給目標Handler來消費這條消息:

public void dispatchMessage(@NonNull Message msg) {
    if (msg.callback != null) {
        handleCallback(msg);
    } else {
        if (mCallback != null) {
            //若是配置了Callback,就再也不走Handler的handleMessage
            if (mCallback.handleMessage(msg)) {
                return;
            }
        }
        handleMessage(msg);
    }
}
複製代碼

Looper

既然在建立Handler時須要制定或從當前線程獲取Looper,那麼接下來就看一下Looper。

構造

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

從構造方法能夠看出:

1.私有方法,不容許外部直接經過構造方法建立

2.初始化時會初始化MessageQueue

3.初始化時會記錄當前線程

在線程中建立Looper,可使用prepare:

public static void prepare() {
    //必須可退出
    prepare(true);
}

//quitAllowed是否容許退出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));
}

複製代碼

Android系統在建立主線程Looper時,是經過prepareMainLooper:

public static void prepareMainLooper() {
    //不容許退出
    prepare(false);
    synchronized (Looper.class) {
        if (sMainLooper != null) {
            throw new IllegalStateException("The main Looper has already been prepared.");
        }
        sMainLooper = myLooper();
    }
}
複製代碼

Looper在建立成功後會經過ThreadLocal與當前線程綁定,而且從源碼中能夠看出,每一個線程只能存在一個Looper。

開啓死循環

Looper既然是經過死循環,爲消息機制提供運轉動力,那麼在建立Looper以後,就要適時的開啓死循環:

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;
    //省略部分代碼
    for (;;) {
        //從queue中取消息,可能會阻塞當前線程
        Message msg = queue.next();
        if (msg == null) {
            // 取出null,說明消息機制退出,那麼跳出循環
            return;
        }
        final 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);
        }
        
        //省略部分代碼
        
        //回收這條消息
        msg.recycleUnchecked();
    }
}
複製代碼

MessageQueue

在Looper初始化時,會初始化MessageQueue,接下來就看一它有哪些內容。

構造

//quitAllowed是否容許退出
MessageQueue(boolean quitAllowed) {
    mQuitAllowed = quitAllowed;
    mPtr = nativeInit();
}
複製代碼

不容許退出(quitAllowed傳false)會怎樣:

void quit(boolean safe) {
    if (!mQuitAllowed) {
        throw new IllegalStateException("Main thread not allowed to quit.");
    }
    
    //省略部分代碼
}
複製代碼

消息入隊

在前面分析Handler時,最終發送消息都會經過MessageQueue的enqueueMessage:

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) {
            //沒有準備要分發的消息
            //或者這條消息是sendMessageAtFrontOfQueue發送的 
            //或者這條消息要發送的時間比下一條要早
            //那麼下一條就是你了
            msg.next = p;
            mMessages = msg;
            needWake = mBlocked;
        } else {
        
            //省略部分代碼
            
        }

        //省略部分代碼
        
    }
    return true;
}
複製代碼

消息出隊

在Looper中,會經過死循環的方式調用queue.next()來獲取下一條消息:

Message next() {

    //省略部分代碼

    for (;;) {
        
        //省略部分代碼
        
        synchronized (this) {
            final long now = SystemClock.uptimeMillis();
            Message prevMsg = null;
            Message msg = mMessages;
            
            //省略部分代碼
            
            if (msg != null) {
                if (now < msg.when) {
                    
                    // 省略部分代碼
                        
                } else {
                    // 取到了一個消息
                    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();
                    //返給looper
                    return msg;
                }
            }
            
            //省略部分代碼
            
        }
    }
}
複製代碼

Handler引發的內存泄漏

衆所周知,若是在Activity內部使用非靜態內部類的形式聲明Handler,就會默認持有Activity的引用,而進一步致使內存泄漏。

但這裏有一個點須要知道,引發內存泄漏的真正緣由是——Activity間接被GCRoots持有。當一個對象,被GCRoots直接或間接引用時,是不會被GC回收的。

那Handler背後的GCRoots又是哪位大仙呢?咱們知道,當經過handler發送消息時,會將msg.target指向handler,在looper中循環遍歷獲得msg後,會經過msg.target.hanleMessage分發消息。而Looper是跟具體的某一個線程綁定的,若是咱們在初始化Handler時,不傳或傳入mainLooper,那麼looper綁定的就是主線程,主線程在程序運行過程當中是不會退出的,那麼就意味着若是隊列中還存在此Handler發送出去的消息(多是延遲消息還沒到分發時間),消息就存在於messagequeue隊列中,被Looper綁定的線程即主線程間接持有,而msg.target又指向了handler,handler持有Activity的引用,最終致使內存泄漏。

大體能夠用下圖示意:

解決辦法:

  • 在Activity聲明週期onDestroy的時候,經過handler.removeCallbacks(myRunnable),能夠將msg.target爲當前Handler,而且msg.callback == myRunnable的msg從隊列中移除:

    //handler
    public final void removeCallbacks(@NonNull Runnable r) {
          mQueue.removeMessages(this, r, null);
    }
    
    //MessageQueue
    void removeMessages(Handler h, Runnable r, Object object) {
          if (h == null || r == null) {
              return;
          }
    
          synchronized (this) {
              Message p = mMessages;
    
              // Remove all messages at front.
              while (p != null && p.target == h && p.callback == r
                     && (object == null || p.obj == object)) {
                  Message n = p.next;
                  mMessages = n;
                  p.recycleUnchecked();
                  p = n;
              }
    
              // Remove all messages after front.
              while (p != null) {
                  Message n = p.next;
                  if (n != null) {
                      if (n.target == h && n.callback == r
                          && (object == null || n.obj == object)) {
                          Message nn = n.next;
                          n.recycleUnchecked();
                          p.next = nn;
                          continue;
                      }
                  }
                  p = n;
              }
          }
      }
    複製代碼
  • 在Activity聲明週期onDestroy的時候,經過handler.removeMessages(int what, @Nullable Object object),還能夠將msg.what = what && msg.obj == object的消息移除

  • 若是發送的Msg比較多且狀況各異(有的是runnable,有的有特定what,有的有特定的obj),那麼能夠經過靜態內部類的方式,不讓Handler持有外部Activity強引用,改用弱引用,這裏再也不進行代碼示意

我是主線程,我要作的事不少,但有優先級機制

咱們知道,在Android中,主線程不僅是要完成開發者寫代碼邏輯需求,還要完成系統對它的指示,好比刷新頁面。

對於同一個Looper來講,是能夠同時存在多個Handler,能夠同時向Looper中發送消息,這其中既有Android系統中定義的各類Handler,又有開發者編寫的Handler,那麼如何才能讓MessageQueue首先將系統發佈的msg分發出來,可以被率先執行呢?

MessageQueue.postSyncBarrier(long when)

往消息隊列頭部,放入一個系統的「告示」,告知MessageQueue接下來我會發送一些優先級高的指令,務必先執行我接下來的優先指令。

//相似於enqueueMessage,根據when合理的插入這個「告示」
//此方法被hide標記
//添加成功後會返回一個惟一的token,標識該屏障
public int postSyncBarrier() {
    return postSyncBarrier(SystemClock.uptimeMillis());
}

private int postSyncBarrier(long when) {
    synchronized (this) {
        final int token = mNextBarrierToken++;
        final Message msg = Message.obtain();
        //未給該msg設置target,是「告示」的身份特徵
        msg.markInUse();
        msg.when = when;
        msg.arg1 = token;

        Message prev = null;
        Message p = mMessages;
        if (when != 0) {
            while (p != null && p.when <= when) {
                prev = p;
                p = p.next;
            }
        }
        if (prev != null) {
            msg.next = p;
            prev.next = msg;
        } else {
            msg.next = p;
            mMessages = msg;
        }
        return token;
    }
}
複製代碼

再來MessageQueue是怎麼識別「告示」的:

Message next() {

    //省略部分代碼
    
    for (;;) {
    
        //省略部分代碼
    
        synchronized (this) {
            final long now = SystemClock.uptimeMillis();
                Message prevMsg = null;
                Message msg = mMessages;
                if (msg != null && msg.target == null) {
                    // msg.target == null 說明是系統「告示」,讓我先進行優先消息的分發
                    do {
                        prevMsg = msg;
                        msg = msg.next;
                        //當找到第一個msg.isAsynchronous() = true的消息時,就會跳出循環,首先分發這個消息
                    } while (msg != null && !msg.isAsynchronous());
                }
                
                //省略部分代碼
                
        }
    
    //省略部分代碼
    
    }

}
複製代碼

Message.setAsynchronous(true)

經過postSyncBarrier,系統告知MessageQueue接下來先執行的事,那麼哪些纔是要先執行的事呢?就是經過msg.setAsynchronous(true)方法,標記爲true的事。

MessageQueue.removeSyncBarrier(int token)

系統除了能夠在特定的場合(如刷新屏幕)添加同步屏障,告知MessageQueue先執行特定的優先級消息以外,還能夠取消同步屏障,讓MessageQueue回覆正常排隊執行。好比原本須要刷新下一幀,可是頁面在下一幀刷新時間前被關閉了,那麼就移除以前的「告知」。

一些思考

沒有這個「告知」,始終在MessageQueue.next中判斷msg.isAsynchronous爲true,那麼就優先分發它行不行?

若是沒有這個添加「告知」和移除「告知」的存在,那麼有些消息,包括系統發出的普通消息(msg.isAsynchronous = false),就可能永遠不會被執行了(開發者把全部消息都進行msg.setAsynchronous(true))。必需要在合適的時機,讓queue按照時間順序,依次執行消息的分發,而不是始終將isAsynchronous標誌放在第一位。

這裏放出一個問題,能夠思考一下:若是將Message的setAsynchronous進行hide處理,在MessageQueue.next中始終判斷msg.isAsynchronous,優先分發msg.isAsynchronous == true的消息,又是否可行呢?

MessageQueue.IdleHandler

再來回看MessageQueue.next方法:

Message next() {

//省略部分代碼

int pendingIdleHandlerCount = -1; 
int nextPollTimeoutMillis = 0;
for (;;) {

    if (msg != null) {
        if (now < msg.when) {
            //省略部分代碼
        }else{
        
            //省略部分代碼
        
            //若是找到了要分發的msg,而且到了分發時間,那麼就返回給looper
            return msg;
        }
    }

    //省略部分代碼
    
    //如下代碼執行條件,是未找到下一條msg或下一條msg還未到分發時間
    
    if (pendingIdleHandlerCount < 0
                        && (mMessages == null || now < mMessages.when)) {
        pendingIdleHandlerCount = mIdleHandlers.size();
    }

    if (pendingIdleHandlerCount <= 0) {
        mBlocked = true;
        continue;
    }

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

    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 {
            //執行idler.queueIdle,保存返回結果
            keep = idler.queueIdle();
        } catch (Throwable t) {
            Log.wtf(TAG, "IdleHandler threw exception", t);
        }

        if (!keep) {
            synchronized (this) {
                //若是不保留當前idler,那麼移除
                mIdleHandlers.remove(idler);
            }
        }
    }    

}

}
複製代碼

再來看一下接口IdleHandler:

public static interface IdleHandler {
    boolean queueIdle();
}
複製代碼

使用場景:

  • 在Activity繪製完成後,作一些事情

  • 結合HandlerThread, 用於單線程消息通知器

關於使用場景,更詳細的內容,能夠參考這篇文章

死循環爲何不會阻塞App

阻塞App時,每每是不能在繼續處理後續的邏輯,可是Android的消息機制,雖然是死循環,可是依然在有條不紊的接收和處理任務。這跟業務代碼中,錯誤書寫的一個局部小的死循環不一樣,正是因爲存在這個死循環的存在,主線程才能一直輪詢處理新的任務,保持應用的生機。

在消息機制中,取消息時,若是沒有可分發消息或下一條要分發的消息還未到分發時間,就會進行適當的阻塞;在有消息傳入時,會根據目標線程的阻塞狀態,決定是否進行喚醒,已使其可以順利的處理接下來的消息分發。

消息機制具體的native層機制,可跳轉Gityuan大佬的文章——Android消息機制2-Handler(Native層)

相關文章
相關標籤/搜索