Android消息機制之Message解析(面試)

在android的消息機制中,Message其充當著信息載體的一個角色,通俗的來說,我們看作消息機制就是個工廠的流水線,message就是流水線上的產品,messageQueue就是流水線的傳送帶。之前做面試官的時候,經常會問面試者關于message的問題,如:

1.聊一下你對Message的了解。
2.如何獲取message對象
3.message的復用(如果以上問題能答對,加分)

在下面我帶著這三個問題,從這段代碼開始逐一解析。

/**
 * 創建一個handler
 */
Handler handler = new Handler();

/**
 * 模擬開始
 */
private void doSth() {
    //開啟個線程,處理復雜的業務業務
    new Thread(new Runnable() {
        @Override
        public void run() {
            //模擬很復雜的業務,需要1000ms進行操作的業務
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            handler.post(new Runnable() {
                @Override
                public void run() {
                    //在這里可以更新ui
                    mTv.setText("在這個點我更新了:" + System.currentTimeMillis());
                }
            });        
        }
    }).start();
}

我們創建了一個handler,在doSth()中開啟線程模擬處理復雜業務,最后通過handler的post返回結果進行UI操作(子線程不能進行操作UI,后話),我們先從handler的post開始看起,

Handler.java:

/**
 * Causes the Runnable r to be added to the message queue.
 * The runnable will be run on the thread to which this handler is
 * attached.
 *
 * @param r The Runnable that will be executed.
 * @return Returns true if the Runnable was successfully placed in to the
 * message queue.  Returns false on failure, usually because the
 * looper processing the message queue is exiting.
 */
public final boolean post(Runnable r) {   
     //通過getPostMessage獲取了message,再往下看
    return sendMessageDelayed(getPostMessage(r), 0);
}

在post中,我們傳進一個Runnable參數,我們發現有一個getPostMessage(r)函數,我們先從getPostMessage()下手。

Handler.java:

private static Message getPostMessage(Runnable r) {
    //在這里,獲取一個message,把我們的任務封裝進message
    Message m = Message.obtain();
    m.callback = r;
    return m;
}

從getPostMessage函數可得,我們把參數Runnable封裝進去message的callback變量中,在這里埋伏一個很重要的概念,在Handler的源碼中,是如何獲取message對象的。顧名思義,在getPostMessage中,我們就是為了獲取把runnable封裝好的message。這樣,我們可以返回上一層,繼續看函數sendMessageDelayed(Message,long)。

Handler.java:

/**
 * Enqueue a message into the message queue after all pending messages
 * before (current time + delayMillis). You will receive it in
 * {@link #handleMessage}, in the thread attached to this handler.
 *
 * @return Returns true if the message was successfully placed in to the
 * message queue.  Returns false on failure, usually because the
 * looper processing the message queue is exiting.  Note that a
 * result of true does not mean the message will be processed -- if
 * the looper is quit before the delivery time of the message
 * occurs then the message will be dropped.
 */
public final boolean sendMessageDelayed(Message msg, long delayMillis) {   
    //顧名思義的delay,也就是延遲,在上一層我們看到了post里傳參是0,繼續往下看
    if (delayMillis < 0) {
        delayMillis = 0;
    }
    return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
}

    
    
/**
 * Enqueue a message into the message queue after all pending messages
 * before the absolute time (in milliseconds) <var>uptimeMillis</var>.
 * <b>The time-base is {@link android.os.SystemClock#uptimeMillis}.</b>
 * Time spent in deep sleep will add an additional delay to execution.
 * You will receive it in {@link #handleMessage}, in the thread attached
 * to this handler.
 *
 * @param uptimeMillis The absolute time at which the message should be
 *                     delivered, using the
 *                     {@link android.os.SystemClock#uptimeMillis} time-base.
 * @return Returns true if the message was successfully placed in to the
 * message queue.  Returns false on failure, usually because the
 * looper processing the message queue is exiting.  Note that a
 * result of true does not mean the message will be processed -- if
 * the looper is quit before the delivery time of the message
 * occurs then the message will be dropped.
 */
public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
    //在這里判斷handler里的隊列是否為空,如果為空,handler則不能進行消息傳遞,因為生產線的傳送帶都沒有的話,還怎么進行傳送
    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);
}

在handler中,存在著sendMessageDelayed最終會用sendMessageAtTime,只是sendMessageDelayed中傳參為0,使得sendMessageAtTime這函數最大程度能復用,我們繼續往enqueueMessage函數看去。

Handler.java

private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
    //在message中放一個標記
    msg.target = this;
    if (mAsynchronous) {
        msg.setAsynchronous(true);
    }
    //在這里把消息放到隊列里面去
    return queue.enqueueMessage(msg, uptimeMillis);
}

在enqueueMessage函數中,我們發現有個入參queue,這個入參就是消息隊列,也就是之前我所說的流水線的傳送帶,message需要通過傳messagequeue進行傳遞,我們繼續往下探索。

MessageQueue.java:


boolean enqueueMessage(Message msg, long when) {
    //這里通過之前的判斷,之前放的目標,還有這個消息是否已經在使用了,都需要判斷
    //還記得之前我們看到的Message是怎么獲取的嗎?Message.obtain(),這里需要判斷msg.isInUse,是否已經在使用這個消息
    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.");
    }
    //這里就是真正把message放到隊列里面去,并且循環復用。
    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;
}

enqueueMessage先判斷之前的target是否為空,以及這個message是否已使用,后面的代碼則是把message放進隊列中,往下我們就不探究了,我們看到最終返回的結果return true.

我們看回來此段代碼:

/**
 * Causes the Runnable r to be added to the message queue.
 * The runnable will be run on the thread to which this handler is
 * attached.
 *
 * @param r The Runnable that will be executed.
 * @return Returns true if the Runnable was successfully placed in to the
 * message queue.  Returns false on failure, usually because the
 * looper processing the message queue is exiting.
 */
public final boolean post(Runnable r) {
    //通過getPostMessage獲取了message,再往下看
    return sendMessageDelayed(getPostMessage(r), 0);
}

@return Returns true if the Runnable was successfully placed in to the * message queue. Returns false on failure, usually because the * looper processing the message queue is exiting.

我們一層一層往下探索,無非就是把這個這個執行UI操作的Runnable封裝成message,再將這個message放進我們的消息隊列messagequeue中。在post如果返回true則成功添加進去消息隊列,如果返回false則代表失敗。

這個流程相信大家也清晰了吧,現在我之前所說的問題,handler中如何獲取message對象的。

Handler.java:

private static Message getPostMessage(Runnable r) {
    //在這里,獲取一個message,把我們的任務封裝進message
    Message m = Message.obtain();
    m.callback = r;
    return m;
}

在這里,為什么Message不是通過new一個對象,而是通過其靜態函數obtain進行獲取?
我們通過其源碼繼續探索:

Message.java:

/**
 * Return a new Message instance from the global pool. Allows us to
 * avoid allocating new objects in many cases.
 */
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();
}

Return a new Message instance from the global pool. Allows us to * avoid allocating new objects in many cases.

我們從注釋中看到pool這個詞,這個就是池,大家應該也聽過過線程池,對象池,沒錯,我們獲取的message對象優先在這個message池里獲取,如果池里沒有再new一個新的Message.

我們先了解一下,這里面的sPool、next、sPoolSize到底是什么東西。
Message.java:

//池里的第一個對象
private static Message sPool;

//對象池的長度
private static int sPoolSize = 0;

//連接下一個message的成員變量
// sometimes we store linked lists of these things
/*package*/ Message next;

在Message這個類中,存在著一個全局變量sPool,sPoolSize則是對象池中的數量,還有一個成員變量next.我們得理清一下sPool跟next到底存在著什么關系。在這先提出一個問題,我們看了那么久的池,怎么沒看到類似Map這樣的容器呢?Message對象池其實是通過鏈表的結構組合起來的池。

Paste_Image.png

上面有三個message,分別為message1、message2、message3
他們的連接關系分別通過其成員變量next進行銜接,舉個例子:

message1.next=message2
message2.next=message3
......

以此類推,那么我們了解了message的next有什么作用,那么sPool呢?
我們注意到sPool是全局變量,我們又看回obtain函數中,是怎么樣獲取的。

Message.java:

/**
 * Return a new Message instance from the global pool. Allows us to
 * avoid allocating new objects in many cases.
 */
public static Message obtain() {
    synchronized (sPoolSync) {
        if (sPool != null) {
            //在池中獲取message也是從表頭獲取,sPool賦值給message,
           //同時把其連接的next賦值給sPool(這樣,連接起來的message從第二個位置放到表頭上了),賦值后設置next為空
            Message m = sPool;
            sPool = m.next;
            m.next = null;
            m.flags = 0; // clear in-use flag
            sPoolSize--;
            return m;
        }
    }
    return new Message();
}

我們看到源碼中,先判斷sPool為不為空,為空代碼這個池的數量為0,不能從池里獲取到message.那如果不空,先將sPool賦值給message,再將這個message的下一個next賦值給sPool,賦值完后將message的next設為空,這不就是從表頭里獲取數據,sPool就是表頭的第一個message。如:
message1是表頭第一個元素,sPool也是表頭,指向message1。當message1從池中取出來時候,message1連接的message2(通過next),成為了表頭,同時sPool也指向新的表頭,sPoolSize的數量也相應的需要減少。

通過以上例子,我們了解message的結構,也明白了message如何獲取,別忘了我們的message除了在池里獲取,還能通過創建一個新的實例,那么,新的實例是怎么放進池的,下面開始看看message的回收。

Message.java:

/**
 * Return a Message instance to the global pool.
 * <p>
 * You MUST NOT touch the Message after calling this function because it has
 * effectively been freed.  It is an error to recycle a message that is currently
 * enqueued or that is in the process of being delivered to a Handler.
 * </p>
 */
public void recycle() {
    //如果還在使用這個消息,不能進行回收--通過flag進行標示
    if (isInUse()) {
        if (gCheckRecycle) {
            throw new IllegalStateException("This message cannot be recycled because it "
                    + "is still in use.");
        }
        return;
    }
    recycleUnchecked();
}

Recycle()函數是怎樣調用的,暫且先不討論,我們先看看其回收的機制,先判斷這個message是否使用狀態,再調用recycleUnchecked(),我們重點看看這個函數。

Message.java

/**
 * Recycles a Message that may be in-use.
 * Used internally by the MessageQueue and Looper when disposing of queued Messages.
 */
void recycleUnchecked() {
    //這里才是真正回收message的代碼,把message中的狀態還原
    // Mark the message as in use while it remains in the recycled object pool.
    // Clear out all other details.
    flags = FLAG_IN_USE;
    what = 0;
    arg1 = 0;
    arg2 = 0;
    obj = null;
    replyTo = null;
    sendingUid = -1;
    when = 0;
    target = null;
    callback = null;
    data = null;
    //如果message池的數量未超過最大容納量(默認最大50個message容量),將此message回收,以便后期復用(通過obtain)
    //在代碼中可知,message在recycle()中進行回收的
    //假設池中的message數量為0時,sPool全局變量為null
    //當我們把第一個message放進去池的時候,sPool(這個時候還是null)賦值給next,而message本身賦值給全局變量sPool,也就是每次回收的message都會插入表頭
    //這樣一來就形成了鏈表的結構,也就是我們所說的對象池
    synchronized (sPoolSync) {
        if (sPoolSize < MAX_POOL_SIZE) {
            next = sPool;
            sPool = this;
            sPoolSize++;
        }
    }
}

我們看到源碼中的最后幾行,如果池中現有的數量少于最大容納量,則可將message放進池中,我們又看到了頭疼的next跟sPool,我先舉個例子,腦補一下:

1.我有一個message1,我用完了,系統回收這個message1
2.現有的池,表頭是message2。

結合以上兩個條件再根據源碼能得出:
sPool跟message2都指向同一個地址,因為message2是表頭,那么message1回收的時候,sPool賦值給了message1的next. 也就是說,message1成了新的表頭,同時池的數量sPoolSize相應的增加。

message的回收就是將其放到池的表頭,包括獲取message也是從表頭上獲取。

總結:
Android的消息機制都通過message這個載體進行傳遞消息,如果每次我們都通過new這樣的方式獲取對象,那么必然會造成內存占用率高,降低性能。而通過對其源碼的學習,了解message的緩存回收機制,同時也學習其設計模式,這就是我們所說的享元模式,避免創建過多的對象,提高性能。

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

推薦閱讀更多精彩內容