理解 Android 訊息機制

我愛宋慧喬發表於2019-03-26

本人只是Android小菜一個,寫技術文章只是為了總結自己最近學習到的知識,從來不敢為人師,如果裡面有不正確的地方請大家盡情指出,謝謝!

本文基於原生 Android 9.0 原始碼來解析 Android 訊息機制:

frameworks/base/core/java/android/os/Handler.java
frameworks/base/core/java/android/os/Looper.java
frameworks/base/core/java/android/os/MessageQueue.java
frameworks/base/core/java/android/os/Message.java
frameworks/base/core/java/android/app/ActivityThread.java
複製程式碼

1. 概述

我們知道在Android的主執行緒中不能進行耗時操作,例如網路訪問、資料處理等,因為一旦主執行緒的任務處理時間超過系統規定的限制就會出現應用不響應的情況。但在實際工作中,處理耗時任務是不可避免的,而且經常需要在處理完耗時任務後更新某些UI控制元件,以顯示處理結果。在這種場景下,最常用方案就是在新執行緒中進行耗時操作,處理完成後通知主執行緒進行相關UI的更新,這時就需要使用到Android訊息機制了。

其實在前面幾篇文章中,小菜講解過IntentServiceAsyncTaskHandlerThread的使用方法和實現原理,它們都是Android訊息機制的具體應用。

到底什麼是訊息機制呢?簡單來說,Android訊息機制是一套以“訊息”為中介來實現執行緒之間的任務切換或同一執行緒中任務的按需執行的機制,其中涉及到訊息的傳送、儲存訊息、訊息迴圈以及訊息的分發和處理。

本文將先通過一個簡單的示例演示如何使用Android訊息機制,再通過分析原始碼來進一步瞭解訊息機制的內部實現方式,最後會講解一些使用Android訊息機制的注意點。

2. 初見 Android 訊息機制

先用一個簡單示例來展示下Android訊息機制在實際工作中如何使用,就直接利用前面提到的場景,即子執行緒處理耗時任務並在任務處理完畢後通知主執行緒進行UI的更新,示例程式碼如下:

public class MainActivity extends Activity {
    // 定義 Handler 和 Thread 區域性變數
    private Handler mHandler;
    private Thread mThread;

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

        mHandler = new Handler() {
            @Override
            public void handleMessage (Message msg) {
                // 4. 根據不同的訊息型別進行不同的處理
                switch (msg.what) {
                    // 在這裡進行和 UI 相關的操作,例如顯示處理結果。
                }
            }
        };

        mThread = new Thread() {
            @Override
            public void run() {
                // 2. 休眠一段時間,模擬子執行緒在處理耗時任務。
                try {
                    Thread.sleep(30000);
                } catch (InterruptedException ie) {
                    ie.printStackTrace();
                }
                // 3. 傳送訊息
                mHandler.sendEmptyMessage(0);
            }
        };

        // 1. 開啟子執行緒
        mThread.start();
    }
}
複製程式碼

小菜在示例程式碼裡通過序號標註了邏輯流程,即先開啟子執行緒並線上程內部處理任務,任務處理完成後通過Handler向主執行緒傳送訊息,最後在主執行緒中處理訊息並更新UI。

看起來Android訊息機制很簡單嘛,只要利用Handler傳送訊息並處理其中的訊息就可以了嘛。真的這麼簡單嗎?當然不是!前面提到過在訊息機制中涉及到幾個關鍵點:傳送訊息、儲存訊息、訊息迴圈和分發處理訊息,在這個示例中我們只看到了傳送訊息和處理訊息,並沒有看到儲存訊息和訊息迴圈。

這是因為這個例子中的Handler使用的訊息是傳送和儲存在主執行緒中的訊息佇列中,這個訊息佇列的建立和迴圈都是在主執行緒建立的時候系統自動進行的,對我們是透明的,不利於理解訊息機制的整體流程。

現在給出一個更為通用的示例,從這個例子中可以清楚地看到訊息佇列的建立和訊息迴圈的開啟:

class LooperThread extends Thread {
    public Handler mHandler;
    
    public void run() {
        // 初始化 Looper 物件,其內部會建立訊息佇列。
        Looper.prepare();
  
        mHandler = new Handler() {
            public void handleMessage(Message msg) {
            // 處理訊息佇列中的訊息。
            }
        };
        // 開啟訊息迴圈,會從訊息佇列中取出訊息,沒有訊息時等待新訊息的到來。
        Looper.loop();
    }
}
複製程式碼

綜合這兩個示例,我們瞭解了Android訊息機制的使用方法,也看到了傳送訊息、建立訊息佇列、開啟訊息迴圈以及處理訊息的過程,下面給出一個更直觀的“訊息傳遞流程圖”:

理解 Android 訊息機制

通過流程圖可以看到整個訊息傳遞過程,也可以看到在不同的階段涉及的類:

  • 訊息傳送:通過 Handler向關聯的MessageQueue傳送訊息;
  • 訊息儲存: 把傳送的訊息以Message的形式儲存在MessageQueue中;
  • 訊息迴圈:通過Looper不停地從MessageQueue中獲取訊息,佇列中沒有訊息時就阻塞等待新訊息;
  • 訊息分發和處理:Looper獲取訊息後分發給Handler進行處理。

3. 理解 Android 訊息機制

前面提到訊息傳遞流程主要分為“傳送訊息”、“儲存訊息”、“訊息迴圈”和“訊息分發和處理”幾個不同階段,小菜本打算按照這個流程來分別講解每個階段,但是在具體行文的時候發現每個階段並不是完全分割開來的,比如在講“傳送訊息”之前要先了解“訊息的儲存結構”和“訊息迴圈的開啟”,而“訊息的分發”又是屬於“訊息迴圈”的功能。

正是由於這幾個階段之間的相互關係,導致沒有辦法嚴格按照訊息傳遞的順序講解Android訊息機制。思慮再三,小菜決定通過上面講解的Android訊息機制通用示例來一步步解析其背後的邏輯流程。

再來看下通用示例:

class LooperThread extends Thread {
    public Handler mHandler;
    
    public void run() {
        // 1. 初始化 Looper 物件,其內部會建立訊息佇列。
        Looper.prepare();
  
        mHandler = new Handler() {
            public void handleMessage(Message msg) {
            // 4. 處理訊息佇列中的訊息。
            }
        };
        // 2. 開啟訊息迴圈,會從訊息佇列中取出訊息,沒有訊息時阻塞等待新訊息的到來。
        Looper.loop();
    }
    
    // 3. 傳送訊息
    mHandler.sendEmptyMessage(0);
}
複製程式碼

在示例程式碼中用不同的序號標註了“訊息傳遞機制”的各個關鍵點,以下的內容也都是根據這些關鍵節點進行講解的。

3.1 訊息載體

“訊息”是Android訊息機制中資訊的載體,它包含了在整個訊息傳遞過程中想要傳送的資料,要理解“訊息機制”就要先了解這個訊息載體類Message:

/**
 * Defines a message containing a description and arbitrary data object that can be
 * sent to a {@link Handler}.  This object contains two extra int fields and an
 * extra object field that allow you to not do allocations in many cases.
 *
 * <p class="note">While the constructor of Message is public, the best way to get
 * one of these is to call {@link #obtain Message.obtain()} or one of the
 * {@link Handler#obtainMessage Handler.obtainMessage()} methods, which will pull
 * them from a pool of recycled objects.</p>
 */
public final class Message implements Parcelable { ... }
複製程式碼

Android框架中對訊息載體Message的宣告雖然簡短,卻傳達了最核心最重要的兩點資訊:

  1. Message的作用:Message是包含了描述資訊和資料物件並且在訊息機制中傳送給Handler的物件,其中的資料物件主要包括兩個整型域和一個物件域,通過這些域可以傳遞資訊。整型的代價是最小的,所以儘量使用整型域傳遞資訊。
/**
 * User-defined message code so that the recipient can identify
 * what this message is about. Each {@link Handler} has its own name-space
 * for message codes, so you do not need to worry about yours conflicting
 * with other handlers.
 */
public int what;

/**
 * arg1 and arg2 are lower-cost alternatives to using
 * {@link #setData(Bundle) setData()} if you only need to store a
 * few integer values.
 */
public int arg1;
public int arg2;

/**
 * An arbitrary object to send to the recipient.  When using
 * {@link Messenger} to send the message across processes this can only
 * be non-null if it contains a Parcelable of a framework class (not one
 * implemented by the application).   For other data transfer use
 * {@link #setData}.
 *
 * <p>Note that Parcelable objects here are not supported prior to
 * the {@link android.os.Build.VERSION_CODES#FROYO} release.
 */
public Object obj;
複製程式碼
  1. Message的建立方式:雖然Message有公有建構函式,但是建議使用其提供的obtain系列函式來獲取Message物件,這種建立方式會重複利用快取池中的物件而不是直接建立新的物件,從而避免在記憶體中建立太多物件,避免可能的效能問題。
/**
 * 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) {
        // 快取池中存在可用物件時去快取池獲取 Message 物件。
        if (sPool != null) {
            // 獲取快取中的物件,並把快取池指標後移。 
            Message m = sPool;
            sPool = m.next;
            
            m.next = null;
            // 清除標誌位
            m.flags = 0; // clear in-use flag
            // 更新當前快取池大小
            sPoolSize--;
            return m;
        }
    }
    // 快取池中沒有可用物件時直接建立一個新的 Message 物件。
    return new Message();
}
複製程式碼

Message中有一系列obtain函式用以在不同場景中獲取物件,但這個是最核心的,其他函式都會在其內部呼叫它,有興趣的同學可以自行檢視原始碼,考慮到篇幅問題,這裡就不再一一列舉說明了。

看到這裡,相信大家都會有一個疑問:obtain函式是從快取池中獲取Message物件,那快取池中的物件是什麼時候被新增進去的呢?既然快取池中的物件都是一些可以被重複使用的物件,很明顯是在Message物件不再被需要的時候,即從MessageQueue中取出並分發給Handler的時候,被新增到快取中的,使用的是recycleUnchecked函式:

/**
 * Recycles a Message that may be in-use.
 * Used internally by the MessageQueue and Looper when disposing of queued Messages.
 */
void recycleUnchecked() {
    // 設定標誌位為“使用中”,在從快取中取出時會清除這個標誌位。
    flags = FLAG_IN_USE;
    // Message 物件中的資訊都不再有意義,在放入快取池前直接清空。
    what = 0;
    arg1 = 0;
    arg2 = 0;
    obj = null;
    replyTo = null;
    sendingUid = -1;
    when = 0;
    target = null;
    callback = null;
    data = null;

    synchronized (sPoolSync) {
        // 快取池中只快取一定數量的 Message 物件,預設是 50 個。
        if (sPoolSize < MAX_POOL_SIZE) {
            // 把物件放在快取池的連結串列首部。 
            next = sPool;
            sPool = this;
            // 及時更新快取池大小。
            sPoolSize++;
        }
    }
}
複製程式碼

3.2 建立訊息佇列

訊息佇列的建立對訊息傳遞至關重要,它決定了訊息在傳遞過程中的存取方式。但是執行緒在預設情況下是沒有訊息佇列的,也無法在其內部進行訊息迴圈。如果想為執行緒開啟訊息迴圈就需要使用到Looper類,它可以為關聯的執行緒建立訊息佇列並開啟訊息迴圈,建立訊息佇列的方式是呼叫prepare()介面:

/**
  * Class used to run a message loop for a thread.  Threads by default do
  * not have a message loop associated with them; to create one, call
  * {@link #prepare} in the thread that is to run the loop, and then
  * {@link #loop} to have it process messages until the loop is stopped.
  */
public final class Looper {
    // 省略無關程式碼
    
    // sThreadLocal.get() will return null unless you've called prepare().
    static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();

    // 內部的訊息佇列和關聯的執行緒
    final MessageQueue mQueue;
    final Thread mThread;
    
    // 省略無關程式碼
    
    /** Initialize the current thread as a looper.
      * This gives you a chance to create handlers that then reference
      * this looper, before actually starting the loop. Be sure to call
      * {@link #loop()} after calling this method, and end it by calling
      * {@link #quit()}.
      */
    public static void prepare() {
        // 建立可退出的訊息迴圈,主執行緒的訊息迴圈是不可退出的。
        prepare(true);
    }

    private static void prepare(boolean quitAllowed) {
        // 如果當前執行緒已經有了 Looper 物件就直接丟擲異常,
        // 因為一個執行緒只能有一個訊息佇列。
        if (sThreadLocal.get() != null) {
            throw new RuntimeException("Only one Looper may be created per thread");
        }
        // 建立 Looper 物件並和執行緒關聯。
        sThreadLocal.set(new Looper(quitAllowed));
    }
    
    // 私有建構函式,建立訊息佇列並獲取當前執行緒物件。
    private Looper(boolean quitAllowed) {
        mQueue = new MessageQueue(quitAllowed);
        mThread = Thread.currentThread();
    }
複製程式碼

可以看到Looper.prepare()只是在內部建立了一個MessageQueue物件並和當前執行緒關聯起來,同時還保證了每個執行緒只能有一個訊息佇列。

很顯然MessageQueue就是用來儲存訊息物件的結構了,看下它的宣告:

/**
 * Low-level class holding the list of messages to be dispatched by a
 * {@link Looper}.  Messages are not added directly to a MessageQueue,
 * but rather through {@link Handler} objects associated with the Looper.
 *
 * <p>You can retrieve the MessageQueue for the current thread with
 * {@link Looper#myQueue() Looper.myQueue()}.
 */
public final class MessageQueue { ... }
複製程式碼

MessageQueue是一個持有訊息物件列表的類,而這些訊息物件通過和Looper關聯的Handler新增並最終由Looper進行分發,其中有個關鍵資訊需要引起我們的格外關注:list of messages,這是不是告訴我們雖然這個類的名字是queue但是其內部並不是佇列而是列表呢?確實如此,MessageQueue的內部是使用單向連結串列的方法進行存取的,這點在後面解析Message的存取過程中會看到,在這裡就不詳細講述了。

3.3 開啟訊息迴圈

“訊息佇列”建立完成了,是不是就可以直接向其中新增訊息物件了呢?還不到時候,還需要先開啟訊息迴圈,來監聽訊息佇列的情況,這時需要使用Looper.loop()介面:

/**
 * Run the message queue in this thread. Be sure to call
 * {@link #quit()} to end the loop.
 */
public static void loop() {
    // 獲取當前執行緒的 Looper 物件,獲取失敗時丟擲異常。
    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 (;;) {
        // 獲取訊息佇列中的訊息物件,如果沒有訊息物件就阻塞等待。
        Message msg = queue.next(); // might block
        if (msg == null) {
            // 訊息佇列正在退出時就終止監聽並退出迴圈
            return;
        }

        // 省略無關程式碼
        
        try {
            // 分發訊息,把訊息傳送合適的處理物件。
            msg.target.dispatchMessage(msg);
            dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;
        } finally {
            if (traceTag != 0) {
                Trace.traceEnd(traceTag);
            }
        }
        
        // 省略無關程式碼
        
        // 回收訊息物件,放入訊息快取池中以待後續複用。
        msg.recycleUnchecked();
    }
}
複製程式碼

這段程式碼本身比較複雜,小菜省略了其中和核心邏輯無關的部分程式碼,以方便大家閱讀和理解,其核心邏輯就是利用一個“無限迴圈”來監聽訊息佇列,當發現有可用訊息就取出並分發處理,如果沒有就一直等待。

3.4 傳送和儲存訊息

“訊息佇列”已經建立完成,“訊息迴圈”也已經開啟,終於可用傳送訊息了。

要傳送訊息,就要使用到Handler類了,其中的sendpost系列方法都可以進行“訊息的傳送”,核心方法都是一樣的,在這裡就以post方法來講解下傳送訊息的過程:

/**
 * 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) {
    return  sendMessageDelayed(getPostMessage(r), 0);
}
    
private static Message getPostMessage(Runnable r) {
    // 把 Runnable 物件封裝成 Message 並設定 callback,
    // 這個 callback 會在後面訊息的分發處理中起到作用。
    Message m = Message.obtain();
    m.callback = r;
    return m;
}
    
public final boolean sendMessageDelayed(Message msg, long delayMillis) {
    if (delayMillis < 0) {
        delayMillis = 0;
    }
    // 把延遲時間轉換為絕對時間,方便後續執行。
    return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
}
    
public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
    // 訊息佇列,即通過 Looper.prepare() 建立的訊息佇列。
    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);
}
    
private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
    // 設定訊息佇列的目標,用於後續的訊息分發過程。
    msg.target = this;
    if (mAsynchronous) {
        msg.setAsynchronous(true);
    }
    // 訊息物件入隊
    return queue.enqueueMessage(msg, uptimeMillis);
}
複製程式碼

通過一系列的呼叫過程,Handler最終會通過 MessageQueue.enqueueMessage()把訊息儲存到訊息佇列中,MessageQueue內部又是如何儲存這個傳送過來的訊息物件的呢?

boolean enqueueMessage(Message msg, long when) {
    // 訊息物件的目標是 null 時直接丟擲異常,因為這意味這個訊息無法進行分發處理,
    // 是不合法的訊息物件。
    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;
}
複製程式碼

3.5 訊息分發處理

當訊息佇列中有新的訊息並且訊息迴圈被喚醒後,訊息佇列中的訊息就可以被取出並分發給合適的處理者了,這點可以在“開啟訊息迴圈”一節中看到,利用的是msg.target.dispatchMessage(msg),而target就是Handler物件,直接看具體的分發過程:

public void dispatchMessage(Message msg) {
    // Message 物件是從 Runnable 封裝形成的時候,callback 不為空。
    if (msg.callback != null) {
        handleCallback(msg);
    } else {
        // mCallback 是在 Handler 的建構函式中設定的,也可以不設定。
        if (mCallback != null) {
            // 呼叫 Handler 的 callback 處理訊息
            if (mCallback.handleMessage(msg)) {
                // 可以攔截訊息,之後 Handler.handleMessage 將無法繼續處理這個訊息。
                return;
            }
        }
        // 呼叫 Handler 的 handleMessage 處理訊息,子類會實現這個方法。
        handleMessage(msg);
    }
}

private static void handleCallback(Message message) {
    // Message 中的 callback 是 Runnable,直接執行 Runnable.run()。
    message.callback.run();
}

/**
 * Callback interface you can use when instantiating a Handler to avoid
 * having to implement your own subclass of Handler.
 */
 public interface Callback {
    /**
     * @param msg A {@link android.os.Message Message} object
     * @return True if no further handling is desired
     */
    // Handler 的回撥方法,通過返回值可以進行訊息攔截。
    public boolean handleMessage(Message msg);
}
    
/**
 * Subclasses must implement this to receive messages.
 */
// Handler 的處理訊息回撥,子類需要實現。
public void handleMessage(Message msg) {
}
複製程式碼

訊息的分發是有一定優先順序的:

  1. 首先會考慮交給Message.callback來處理,如果是通過post系列函式傳送的訊息會走到這裡進行處理,而通過send系列函式傳送的訊息預設是沒有這個回撥介面的;
  2. 如果Message.callback不存在就考慮交給Handler.callback來處理,在處理過程中可以通過返回值攔截訊息;
  3. 如果Handler.callback不存在或者存在但是在處理訊息過程中沒有進行攔截,就會交給Handler.handleMessage來處理,這個介面需要子類實現,也是在實際工作中最常用的處理訊息的地方。

到這裡,訊息的傳遞過程就基本講完了,大家可以結合之前的流程圖仔細揣摩,相信可以對Android訊息機制有更深刻的理解。

4. 延伸知識點

4.1 主執行緒訊息迴圈的建立

前面講到一個執行緒預設是沒有訊息佇列的,也無法在其內部開啟訊息迴圈,但是我們在實際工作中經常會直接在主執行緒中使用Handler來進行訊息的傳送和處理,並且執行正常,這是因為主執行緒在啟動的時候就已經建立了訊息佇列並開啟了訊息迴圈,只是這個過程是透明的,我們沒有感知到。

瞭解Activity啟動過程的同學應該已經想到了這個建立過程是在哪裡了,沒錯,就是在ActivityThread,不瞭解啟動過程的同學也不要擔心,後續我會講解具體的啟動過程。在這裡,大家只要簡單地把ActivityThread當做Activity的啟動入口即可,直接來看入口函式:

/**
 * This manages the execution of the main thread in an
 * application process, scheduling and executing activities,
 * broadcasts, and other operations on it as the activity
 * manager requests.
 *
 * {@hide}
 */
public final class ActivityThread extends ClientTransactionHandler {
    public static void main(String[] args) {
        // 記錄開始,用於後續通過 systrace 檢查和除錯效能問題。
        Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "ActivityThreadMain");
        // 省略無關程式碼

        // 為主執行緒建立訊息佇列
        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"));
        }

        // 記錄結束,後續可以通過 systrace 觀察這段程式碼的執行情況。
        // End of event ActivityThreadMain.
        Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
        
        // 開啟訊息迴圈
        Looper.loop();

        // 主執行緒訊息迴圈不會退出,如果走到這意味著發生意外,丟擲異常。
        throw new RuntimeException("Main thread loop unexpectedly exited");
    }
}
複製程式碼

程式碼結構和Android訊息機制的通用示例很像,在裡面看到了訊息佇列的建立和訊息迴圈的開啟,不同之處在於主執行緒中建立訊息佇列使用的是Looper.prepareMainLooper

/**
 * Initialize the current thread as a looper, marking it as an
 * application's main looper. The main looper for your application
 * is created by the Android environment, so you should never need
 * to call this function yourself.  See also: {@link #prepare()}
 */
public static void prepareMainLooper() {
    // 啟動一個無法退出的訊息迴圈
    prepare(false);
    synchronized (Looper.class) {
        if (sMainLooper != null) {
            throw new IllegalStateException("The main Looper has already been prepared.");
        }
        // 返回主執行緒 looper 物件
        sMainLooper = myLooper();
    }
}
複製程式碼

為主執行緒建立的訊息迴圈是無法退出的,因為這個訊息迴圈要處理很多重要事務,比如Activity生命週期的回撥等,如果退出將導致異常,這點在後續講解Activity啟動過程的時候再詳細解析。

4.2 記憶體洩露

Java垃圾回收機制對於每個從事Java的開發者應該都不陌生,我們也清楚並不是所有物件佔用的記憶體都可以被及時回收,如果垃圾回收器準備回收某些物件,但是由於它們還被其他物件引用,那麼這些物件就無法被回收,這也是記憶體洩漏的主要原因。

使用Android訊息機制時會不會導致記憶體洩漏呢?首先來看一種常見的使用方法:

public class MainActivity extends Activity {
    private TextView mTextView = null;
    private Handler mMyHandler = null;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        // 初始化控制元件
        mTextView = (TextView) findViewById(R.id.sample_text);
        // 初始化 Handler 物件
        mMyHandler = new MyHandler();
        // 啟動一個延遲訊息,在 3000ms 後有 mMyHandler 執行。
        mMyHandler.sendEmptyMessageDelayed(0, 3000);
    }

    private class MyHandler extends Handler {
        @Override
        public void handleMessage(Message msg) {
            // 執行訊息,更新主執行緒中的控制元件。
            if (mTextView != null) {
                mTextView.setText("execute message");
            }
        }
    };

    @Override
    public void onDestroy() {
        super.onDestroy();
    }
}
複製程式碼

在這個示例中,MyHandler是以Activity內部類的形式存在的,所以mMyHandler是需要持有外部類物件引用的,而mMyHandler又被其傳送的Message物件以target的方式引用,最終的結果就是Activity間接被Message引用。由於這個Message需要在一定的延遲後被執行,如果在這之前Activity退出,但是由於其引用被Message持有,導致無法被系統回收,進而導致記憶體洩露。

既然ActivityMessage引用導致記憶體洩露,那有沒有辦法不讓其持有引用呢?當然可以,使用“靜態內部類”就可以避免這種情況,因為“靜態內部類”不需要持有外部類物件的引用,來看示例程式碼:

public class MainActivity extends Activity {
    private TextView mTextView = null;
    private Handler mMyHandler = null;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mTextView = (TextView) findViewById(R.id.sample_text);

        // 初始化 Handler 物件,並把主執行緒控制元件作為引數傳入。
        mMyHandler = new MyHandler(mTextView);
        // 啟動一個延遲訊息,在 3000ms 後有 mMyHandler 執行。
        mMyHandler.sendEmptyMessageDelayed(0, 3000);
    }

    private static class MyHandler extends Handler {
        // 通過弱引用的方式持有外部物件的變數。
        private WeakReference<TextView> mTextViewRef = null;

        // 初始化弱引用物件,此後就持有了正確的物件引用。
        public MyHandler(TextView textView) {
            mTextViewRef = new WeakReference<>(textView);
        }
        @Override
        public void handleMessage(Message msg) {
            // 執行訊息,更新主執行緒中的控制元件。
            if (mTextViewRef != null && mTextViewRef.get() != null) {
                mTextViewRef.get().setText("execute message");
            }
        }
    };

    @Override
    public void onDestroy() {
        super.onDestroy();
        // 退出時情況訊息佇列中的訊息
        mMyHandler.removeCallbacksAndMessages(null);
    }
}
複製程式碼

通過“靜態內部類”和“弱引用”的結合,既可以不持有外部類物件引用又可以訪問外部類物件的變數,並在Activity退出時又移除訊息佇列中的訊息,進一步避免了記憶體洩露的風險。

這只是其中一中避免記憶體洩露的方法,肯定還有其他方法也可以達到目的,有興趣的同學可以自行研究。

5. 總結

本文講解了Android訊息機制的使用方法、整體流程和每個階段的實現原理,在最後還提到主執行緒訊息迴圈的建立以及錯誤使用導致的記憶體洩漏及避免方法,希望能對大家學習訊息機制有所幫忙。

相關文章