知識梳理系列之二——消息機制中的若干重要問題


這是一個老生常談的知識,本文不是全面文章,主要記錄一些非常有用的原理性知識點,方便深入理解。

輪詢器Looper

子線程爲何要手動準備和輪詢?

一、MainThread的Looper創建和準備

Android 的啓動過程是: 創建init進程 --> Zygote進程 --> SystemServer進程 --> 各個應用進程
在SystemServer進程啓動後(由Zygote進程fork出)在調用run方法時,調用了Looper.prepareMainLooper();,在老版本的則是在ActivityThread.main()中調用的prepareMainLooper

// SystemServer.java
package com.android.server;

public final class SystemServer {
	...
	public static void main(String[] args) {
        new SystemServer().run();
    }

	private void run() {
		...
		Looper.prepareMainLooper();
		...
		startBootstrapService();
		startCoreService();
		startOtherService();
		...
		Looper.loop();
		...
	}
	...
}

於是,主線程在啓動後,Activity#onCreate(Bundle) 調用時候就已經準備好Looper了
而工作線程Looper是沒有調用prepare()/loop()的,因此需要自己手動調用或者使用HandlerThread/IntentService

如何保證線程Looper唯一?

遇到一個問Looper存儲在哪裏的問題

我們來看下Looper.java

// Looper.java
	...
	
	public static void prepare() {
		// 工作線程的prepare傳入了true,表示允許消息隊列退出
        prepare(true);
    }

    private static void prepare(boolean quitAllowed) {
        if (sThreadLocal.get() != null) {
        	// 在調用prepare的時候如果ThreadLocal中就已經存在Looper了
        	// 則拋出異常,避免重複prepare和同一個線程出現多個Looper對象
            throw new RuntimeException("Only one Looper may be created per thread");
        }
        // 向ThreadLocal 中存入創建的Looper對象
        sThreadLocal.set(new Looper(quitAllowed));
    }

	public static void prepareMainLooper() {
		// 主線程的prepare傳入了false,表示主線程的消息隊列是不允許對出的!!
        prepare(false);
        synchronized (Looper.class) {
            if (sMainLooper != null) {
                throw new IllegalStateException("The main Looper has already been prepared.");
            }
            // 此處爲Looper對象的成員變量sMainLooper初始化
            // myLooper()方法實際還是調用ThreadLocal.get()
            sMainLooper = myLooper();
        }
    }

所以我們有答案了,Looper是保存在ThreadLocal這個線程本地存儲裏的,每一個線程只有一個線程本地存儲,所以確保了同一個線程只有一個Looper!

附:ThreadLocal是怎樣的數據結構:

// Looper.java
// Looper類中有靜態常量ThreadLocal對象,通過ThreadLocal.get()獲取Looper實例(線程唯一)
static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();

// ThreadLocal.java
// get方法獲取了當前線程,通過線程取得線程的ThreadLocalMap引用,然後根據指定泛型對象獲取實例
// 簡單說就是從Map中獲取了ThreadLocal<Looper>的鍵值對對象Entry,最後返回Entry.value即Looper對象
// 而這個Map是一個哈希數組,key是ThreadLocal<?>對象,就是說只有唯一的一個ThreadLocal<Looper>,
// 因爲ThreadLocal<Looper>與別的泛型的ThreadLocal對象的hash值不同,因此確保了一個線程只有一個Looper
	public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }
	
	ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

// Thread.java
ThreadLocal.ThreadLocalMap threadLocals = null;

總結:
1. 主線程Looper在啓動時就已經prepare/loop,子線程需要手動執行或使用HandlerThread/IntentService;
2. Looper被存儲在相應Thread的ThreadLocal對象中,這個對象是一個哈希數組,保證了Looper線程唯一;

Looper.loop中是死循環爲什麼MainLooper不會阻塞主線程

在loop方法中,實際是一個死循環,即不斷的從消息隊列中獲取消息,並分發給target(Handler)來處;
那麼主線程是如何不會被這個死循環卡死的?

簡化loop方法如下:

//Looper.java
	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.");
        }
        // 獲取Looper 持有的消息隊列
        final MessageQueue queue = me.mQueue;

        ...

        for (;;) {
        	// 進入死循環 不斷從消息隊列中獲取下一個消息 
        	// 官方在這裏註釋了might block表示獲取下一個消息有可能會阻塞
            Message msg = queue.next(); // might block
            if (msg == null) {// 拿到空消息退出循環 這時候loop也退出了
                // No message indicates that the message queue is quitting.
                return;
            }
            
			...
			
            try {
            	// 把非空消息分發給目標Handler
                msg.target.dispatchMessage(msg);
            } 
            ...
            // 對消息進行回收
            msg.recycleUnchecked();
        }
    }

通過loop方法可以看出端倪,死循環在 queue.next() 方法處阻塞
再看簡化的MessageQueue.next方法

// MessageQueue.java
	Message next() {
        // MessageQueue持有Native消息隊列的指針,如果已經退出了就會返回空消息
        final long ptr = mPtr;
        if (ptr == 0) {
            return null;
        }
		int pendingIdleHandlerCount = -1; // -1 only during first iteration
        int nextPollTimeoutMillis = 0;
        for (;;) {
        	// 再進入一個死循環,調用本地方法去檢測描述符有沒有新的消息可以poll出來
        	// 參數nextPollTimeOutMillis表示的是沒有檢測到新消息時的超時阻塞時間,-1表示一直阻塞
        	nativePollOnce(ptr, nextPollTimeoutMillis);
        	
        	// 休眠被喚醒後執行同步代碼塊
        	synchronized (this) {
                // 獲取系統時間 和 消息隊列頭部Message
                final long now = SystemClock.uptimeMillis();
                Message prevMsg = null;
                Message msg = mMessages;
                // 省略同步屏障有關處理
                ...
                if (msg != null) {
                    if (now < msg.when) {
                        // 如果消息的時間沒有到(比如延時消息),重新計算nextPollTimeoutMillis
                        // 下一次循環,nativePollOnce阻塞的時間就是確定的
                        nextPollTimeoutMillis = (int) Math.min(
                        msg.when - now, Integer.MAX_VALUE);
                    } else {
                        mBlocked = false;
                        if (prevMsg != null) {
                        	// 有同步屏障纔會走這裏
                            prevMsg.next = msg.next;
                        } else {
                        	// 更新消息隊列對頭
                            mMessages = msg.next;
                        }
                        // 修改use標記和從消息隊列中剝離出msg,最後返回msg
                        msg.next = null;
                        if (DEBUG) Log.v(TAG, "Returning message: " + msg);
                        msg.markInUse();
                        return msg;
                    }
                } else {
                    // 取到空消息,重置nextPollTimeoutMillis 繼續循環
                    nextPollTimeoutMillis = -1;
                }
                ...
            }
           	...
        }

至此,已經找到問題的關鍵部分了。
Looper.loop()方法的死循環是在MessageQueue.next()獲取隊列中下一個消息時,阻塞的,並且如果主線程消息隊列一直沒有新消息時,就會一直阻塞,是被一個nativePollOnce()的本地方法阻塞的。

回到問題,阻塞了爲什麼沒有導致主線程卡死。由於 Linux pipe/epoll機制,主線程會在此時(沒有新的消息和事件進入),釋放CPU資源,進入休眠狀態,等待新的消息或者事件喚醒。

Linux pipe/epoll 機制是一種IO多路複用的機制,基於事件驅動,監測了多個文件描述符。即使沒有消息,如果有其他的操作(比如事件)喚醒了主線程,主線程就退出休眠的狀態,繼續工作,而消息Looper的繼續阻塞不會導致主線程卡死。

而在消息機制中,來了新的消息,也會調用一個本地方法喚醒休眠中的主線程

// MessageQueue.java
private native static void nativeWake(long ptr);

消息與隊列 Message/MessageQueue

引:

一個線程不僅只有一個Looper,並且也只有一個MessageQueue,這是如何保證的;

如果向隊列中發送大量消息,消息又一直在不斷被處理,那麼爲什麼不會頻繁GC Message導致內存抖動?

帶着這兩個問題來看源碼

線程MessageQueue唯一

// Looper.java
	private static void prepare(boolean quitAllowed) {
        if (sThreadLocal.get() != null) {
            throw new RuntimeException("Only one Looper may be created per thread");
        }
        // 這裏創建並向ThreadLocal中存入了Looper實例
        sThreadLocal.set(new Looper(quitAllowed));
    }
 
 	// 構造方法中創建了MessageQueue實例
 	private Looper(boolean quitAllowed) {
        mQueue = new MessageQueue(quitAllowed);
        mThread = Thread.currentThread();
    }

第一個問題已經有答案了:
因爲Looper由線程私有存儲ThreadLocal的哈希數組結構保證線程唯一,MessageQueue又是由Looper實例化的成員變量,那麼也是線程唯一的。

IdleHandler的作用

在MessageQueue中有個接口IdleHandler,顧名思義是一個空閒時處理任務的處理器。

public static interface IdleHandler {
	boolean queueIdle();
}

這個接口提供了一個返回boolean值的方法。
在MessageQueue中有一個ArrayList<IdleHandler>的集合
此外,還提供了addIdle、removeIdle等方法用於向集合添加和移除IdleHandler。
那麼這個IdleHandler有什麼作用呢?在何時調用queueIdle方法?
在MessageQueue#next()中有了答案:

Message next() {
		// 省略了nativePollOnce等邏輯
		...
		// 在阻塞結束(消息隊列中的消息執行完了,而隊列中下一個要執行的消息還沒有達到可執行的時間)時
		if (pendingIdleHandlerCount < 0
        	&& (mMessages == null || now < mMessages.when)) {
        	pendingIdleHandlerCount = mIdleHandlers.size();
        } // 獲取列表中IdleHandler個數
        if (pendingIdleHandlerCount <= 0) {
        	// 沒有IdleHandler直接進入下一次循環
            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 {
        	// 在這裏調用了queueIdle方法!!!
            keep = idler.queueIdle();
        } catch (Throwable t) {
            Log.wtf(TAG, "IdleHandler threw exception", t);
        }

        if (!keep) {
            synchronized (this) {
                mIdleHandlers.remove(idler);
            }
        }
    }
}

也就是當消息隊列空閒時,可以執行IdleHandler的一些任務,這樣可以提升性能。
比如在onResume方法中,使用主線程的消息隊列的IdleHandler做一些準備工作,或者單純的想獲取UI繪製完成的回調,在繪製完成以後立即可以執行一些操作。

Message如何避免內存抖動——消息複用

在使用Message時,通常建議使用Message.obtain()系列的方法,這是有原因的。
當然也可以通過new Message() 來創建消息,只是更推薦上面的方式,並且new的方式最終也會被做一些響應處理。

幾個重要變量的含義:

// Message.java
public final class Message implements Parcelable {
	...
	
	Message next;// 消息單向鏈表的下一個消息對象
    /** @hide */
    public static final Object sPoolSync = new Object();// 同步鎖
    private static Message sPool;// 私有靜態成員,消息池
    private static int sPoolSize = 0;// 消息池的大小,即消息對象個數
    private static final int MAX_POOL_SIZE = 50;// 消息池最多可容納50個消息對象
    
	...
}

由此可以看出消息池最多可以服用的消息對象是50個,這個池是一個單向鏈表的數據結構;

從obtain()看消息複用

// Message.java
	public static Message obtain() {
        synchronized (sPoolSync) {// 同步鎖
            if (sPool != null) {
            	// 當池可以複用時,取出單向鏈表隊頭的消息對象使用,
            	// 然後把隊頭重置爲next,並維護池中可用的消息對象的個數
                Message m = sPool;
                sPool = m.next;
                m.next = null;
                m.flags = 0; // clear in-use flag
                sPoolSize--;
                return m;
            }
        }
        // 消息池被清空了智能創建新的消息對象
        return new Message();
    }

答案就是,消息對象維護了一個消息池,由於消息本身是一種單向鏈表的結構,維護對頭來複用消息對象,並且在Looper.loop()方法中通過recycleUnchecked()方法來回收消息對象。


消息處理者Handler

Looper獲取了消息隊列中的消息後,通過Handler#dispathMessage()方法來分發給Handler,就會重發handleMessage方法,於是被重寫的handleMessage中的業務邏輯就被執行了。

延時消息是怎樣被入隊和分發處理的?

在日常使用中,經常使用sendMessageDelay/postDelay/sendMesageAtTime等方法,那麼延時消息是怎麼入隊的呢?

// Handler.java
	public final boolean sendMessageDelayed(@NonNull Message msg, long delayMillis) {
        if (delayMillis < 0) {
            delayMillis = 0;
        }
        return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
    }

首先,這些方法最終都是調用sendMessageAtTime實現的!

// Handler.java
	public boolean sendMessageAtTime(@NonNull Message msg, long uptimeMillis) {
        MessageQueue queue = mQueue;
       	...
        return enqueueMessage(queue, msg, uptimeMillis);// 
    }

其次,直接調用了enqueueMessage進行入隊!這個方法最終調用了MessageQueue#enqueueMessage()方法

那麼是怎麼實現延時呢?
下面是簡化後的MessageQueue#enqueueMessage()方法

// MessageQueue.java
	boolean enqueueMessage(Message msg, long when) {
        // 一些可用非空判斷
		...
		
        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;
            }

			// 標記入隊的消息inUse 獲取時間 獲取消息隊列對頭
            msg.markInUse();
            msg.when = when;
            Message p = mMessages;
            boolean needWake;
            if (p == null || when == 0 || when < p.when) {
                // 當消息隊列中沒有消息或者新入隊的消息時間小或爲0,需要先執行入隊的新消息
                // 變更了對頭
                msg.next = p;
                mMessages = msg;
                needWake = mBlocked;
            } else {
                // 同步屏障有關省略
                ...
                Message prev;
                for (;;) {
                    prev = p;
                    p = p.next;
                    // 找到第一個時間比要入隊新消息的消息然後放入,或者沒有就放隊尾
                    // 實質上是消息隊列用when這個時間來排序了,時間小的在隊頭,從小到大的順序排列
                    // 新消息找到正確的入隊位置就退出此循環
                    if (p == null || when < p.when) {
                        break;
                    }
                    // 同步屏障有關省略
                	...
                }
                // 然後這一步就是真正的入隊
                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;
    }

答案有了:延時消息也是在觸發時就入隊了,只是消息隊列裏面會依據when這個時間標誌從小到大屏排序;
那怎樣保證延時處理呢?答案就在Looper.loop()方法中,阻塞的過程中有一個nextPollTimeoutMillis參數,如果消息隊列中隊頭就是一個延時消息,那麼loop在取消息的時候,會計算一個新的nextPollTimeoutMillis阻塞超時時間,來在delay的時間向Handler交付Message,於是就實現了延時消息!!!

題外話:Handler內存泄露的注意項

Handler在Activity中使用時常常要注意避免內存泄露,這裏泄露的對象就是Activity,常常在使用延時消息時,需要注意如果延時消息還沒有處理,Activity就銷燬了,那麼就會出問題。

Handler內存泄露的引用鏈

MainThread --> ThreadLocal<Looper> --> Looper --> MessageQueue --> Message --> Handler --> Activity

匿名內部類Handler持有Activity的引用,Message.target.dispatchMessage()持有了target(Handler)的引用,就是延時消息持有了Handler的引用,延時消息又enqueue在MessageQueue中,MessageQueue由是由Looper創建的,最終存放在主線程ThreadLocal中了。

解決方法

只要打破引用鏈的環節就解決了

  1. 讓Handler作爲靜態內部類或者外部類,不持有Activity引用,即可避免Activity泄露,但是此時沒有Activity的引用了,可以引入弱引用、軟引用;
  2. 讓Handler作爲普通內部類使用,但是在Activity生命週期結束時,移除消息隊列中的所有回調和消息;(即onDestroy中 調用Handler#removeCallbacksAndMessages一類的remove方法)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章