原始碼篇:Handler那些事(萬字長文)

xdd666發表於2021-04-12

前言

Handler屬於八股文中非常經典的一個考題了,導致這個知識點很多時候,考官都懶得問了;這玩意很久之前就看過,但是過了一段時間,就很容易忘記,但是處理記憶體洩漏,IdleHandler之類的考點答案肯定很難忘。。。雖然考官很多時候不屑問,但是要是問到了,你忘了且不知道怎麼回答,那就很尷尬了

img

鄙人也來炒個剩飯,力求通俗易懂的來描述下Handler機制的整個流程,相關知識點,畫了一些流程圖,時序圖來展示其執行機制,讓本文圖文並茂!

文章中關鍵方法原始碼,可以直接點選方法名,跳轉檢視對應方法的原始碼

如果看了沒收穫,噴我!

img

總流程

開頭需要建立個handler作用的總體印象,下面畫了一個總體的流程圖

Handler總流程

從上面的流程圖可以看出,總體上是分幾個大塊的

  • Looper.prepare()、Handler()、Looper.loop() 總流程
  • 收發訊息
  • 分發訊息

相關知識點大概涉及到這些,下面詳細講解下!

  • 需要詳細的檢視該思維導圖,請右鍵下載後檢視

Handler流程

使用

先來看下使用,不然原始碼,原理圖搞了一大堆,一時想不起怎麼用的,就尷尬了

使用很簡單,此處僅做個展示,大家可以熟悉下

演示程式碼儘量簡單是為了演示,關於內部類持有弱引用或者銷燬回撥中清空訊息佇列之類,就不在此處展示了

Handler.java
...
public void dispatchMessage(@NonNull Message msg) {
    if (msg.callback != null) {
        handleCallback(msg);
    } else {
        if (mCallback != null) {
            if (mCallback.handleMessage(msg)) {
                return;
            }
        }
        handleMessage(msg);
    }
}
...

從上面原始碼可知,handler的使用總的來說,分倆大類,細分三小類

  • 收發訊息一體
    • handleCallback(msg)
  • 收發訊息分開
    • mCallback.handleMessage(msg)
    • handleMessage(msg)

收發一體

  • handleCallback(msg)
  • 使用post形式,收發都是一體,都在post()方法中完成,此處不需要建立Message例項等,post方法已經完成這些操作
public class MainActivity extends AppCompatActivity {
    private TextView msgTv;
    private Handler mHandler = new Handler();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        msgTv = findViewById(R.id.tv_msg);

      	//訊息收發一體
        new Thread(new Runnable() {
            @Override public void run() {
                String info = "第一種方式";
                mHandler.post(new Runnable() {
                    @Override public void run() {
                        msgTv.setText(info);
                    }
                });
            }
        }).start();
    }
}

收發分開

mCallback.handleMessage(msg)

  • 實現Callback介面
public class MainActivity extends AppCompatActivity {
    private TextView msgTv;
    private Handler mHandler = new Handler(new Handler.Callback() {
        //接收訊息,重新整理UI
        @Override public boolean handleMessage(@NonNull Message msg) {
            if (msg.what == 1) {
                msgTv.setText(msg.obj.toString());
            }
            //false 重寫Handler類的handleMessage會被呼叫,  true 不會被呼叫
            return false;
        }
    });

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        msgTv = findViewById(R.id.tv_msg);

        //傳送訊息
        new Thread(new Runnable() {
            @Override public void run() {
                Message message = Message.obtain();
                message.what = 1;
                message.obj = "第二種方式 --- 1"; 
                mHandler.sendMessage(message);
            }
        }).start();
    }
}

handleMessage(msg)

  • 重寫Handler類的handlerMessage(msg)方法
public class MainActivity extends AppCompatActivity {
    private TextView msgTv;
    private Handler mHandler = new Handler() {
        //接收訊息,重新整理UI
        @Override public void handleMessage(@NonNull Message msg) {
            super.handleMessage(msg);
            if (msg.what == 1) {
                msgTv.setText(msg.obj.toString());
            }
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        msgTv = findViewById(R.id.tv_msg);

        //傳送訊息
        new Thread(new Runnable() {
            @Override public void run() {
                Message message = Message.obtain();
                message.what = 1;
                message.obj = "第二種方式 --- 2";
                mHandler.sendMessage(message);
            }
        }).start();
    }
}

prepare和loop

大家肯定有印象,在子執行緒和子執行緒的通訊中,就必須在子執行緒中初始化Handler,必須這樣寫

  • prepare在前,loop在後,固化印象了
new Thread(new Runnable() {
    @Override public void run() {
        Looper.prepare();
        Handler handler = new Handler();
        Looper.loop();
    }
});
  • 為啥主執行緒不需要這樣寫,聰明你肯定想到了,在入口出肯定做了這樣的事
ActivityThread.java
...
public static void main(String[] args) {
    ...
    //主執行緒Looper
    Looper.prepareMainLooper();
    ActivityThread thread = new ActivityThread();
    thread.attach(false);
    if (sMainThreadHandler == null) {
        sMainThreadHandler = thread.getHandler();
    }
    //主執行緒的loop開始迴圈
    Looper.loop();
	...
}
...

為什麼要使用prepare和loop?我畫了個圖,先讓大家有個整體印象

Handler---Looper

  • 上圖的流程,鄙人感覺整體畫的還是比較清楚的
  • 總結下就是
    • Looper.prepare():生成Looper物件,set在ThreadLocal裡
    • handler建構函式:通過Looper.myLooper()獲取到ThreadLocal的Looper物件
    • Looper.loop():內部有個死迴圈,開始事件分發了;這也是最複雜,幹活最多的方法

具體看下每個步驟的原始碼,這裡也會標定好連結,方便大家隨時過去檢視

  • Looper.prepare()
    • 可以看見,一個執行緒內,只能使用一次prepare(),不然會報異常的
Looper.java
...
 public static void prepare() {
    prepare(true);
}

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));
}
...
  • Handler()
    • 這裡通過Looper.myLooper() ---> sThreadLocal.get()拿到了Looper例項
Handler.java
...
@Deprecated
public Handler() {
    this(null, false);
}

public Handler(@Nullable Callback callback, boolean async) {
    if (FIND_POTENTIAL_LEAKS) {
        final Class<? extends Handler> klass = getClass();
        if ((klass.isAnonymousClass() || klass.isMemberClass() || klass.isLocalClass()) &&
            (klass.getModifiers() & Modifier.STATIC) == 0) {
            Log.w(TAG, "The following Handler class should be static or leaks might occur: " +
                  klass.getCanonicalName());
        }
    }

    mLooper = Looper.myLooper();
    if (mLooper == null) {
        throw new RuntimeException(
            "Can't create handler inside thread " + Thread.currentThread()
            + " that has not called Looper.prepare()");
    }
    mQueue = mLooper.mQueue;
    mCallback = callback;
    mAsynchronous = async;
}
...
Looper.java
...
public static @Nullable Looper myLooper() {
    return sThreadLocal.get();
}
...
  • Looper.loop():該方法分析,在分發訊息裡講
    • 精簡了大量原始碼,詳細的可以點選上面方法名
    • Message msg = queue.next():遍歷訊息
    • msg.target.dispatchMessage(msg):分發訊息
    • msg.recycleUnchecked():訊息回收,進入訊息池
Looper.java
...
public static void loop() {
    final Looper me = myLooper();
    
    ...
    
    final MessageQueue queue = me.mQueue;

   	...

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

        ...
        
        try {
            msg.target.dispatchMessage(msg);
            if (observer != null) {
                observer.messageDispatched(token, msg);
            }
            dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;
        } catch (Exception exception) {
            if (observer != null) {
                observer.dispatchingThrewException(token, msg, exception);
            }
            throw exception;
        } finally {
            ThreadLocalWorkSource.restore(origWorkSource);
            if (traceTag != 0) {
                Trace.traceEnd(traceTag);
            }
        }
     
        ....

        msg.recycleUnchecked();
    }
}
...

收發訊息

收發訊息的操作口都在Handler裡,這是我們最直觀的接觸的點

下方的思維導圖整體做了個概括

Handler-收發訊息

傳送訊息

傳送訊息涉及到倆個方法:post(...)和sendMessage(...)

  • Post(Runnable):傳送和接受訊息都在Post中完成
  • sendMessage(msg):需要自己傳入Message訊息物件
  • 看下原始碼
    • 使用Post會自動會通過getPostMessage方法建立Message物件
    • 在enqueueMessage中將生成的Message加入訊息佇列,注意
      • 此方法給msg的target賦值當前handler之後,才進行將訊息新增的訊息佇列的操作
      • msg.setAsynchronous(true):設定Message屬性為非同步,預設都為同步;設定為非同步的條件,需要手動在Handler構造方法裡面設定
Handler.java
...
//post
public final boolean post(@NonNull Runnable r) {
    return  sendMessageDelayed(getPostMessage(r), 0);
}

//生成Message物件
private static Message getPostMessage(Runnable r) {
    Message m = Message.obtain();
    m.callback = r;
    return m;
}

//sendMessage方法
public final boolean sendMessage(@NonNull Message msg) {
    return sendMessageDelayed(msg, 0);
}

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

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

///將Message加入詳細佇列 
private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,
                               long uptimeMillis) {
    //設定target
    msg.target = this;
    msg.workSourceUid = ThreadLocalWorkSource.getUid();

    if (mAsynchronous) {
        //設定為非同步方法
        msg.setAsynchronous(true);
    }
    return queue.enqueueMessage(msg, uptimeMillis);
}
...
  • enqueueMessage(...):精簡了一些程式碼,完整程式碼,可點選左側方法名
  • Message通過enqueueMessage加入訊息佇列
  • 這地方有倆種情況,記錄下:mMessage為當前訊息分發到的訊息位置
    • mMessage為空,傳入的msg則為訊息連結串列頭,next置空
    • mMessage不為空,從當前分發位置移到連結串列尾,將傳入的msg插到連結串列尾部,next置空
MessageQueue.java
...
boolean enqueueMessage(Message msg, long when) {
   ...

    synchronized (this) {
        ...

        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;
}
...
  • 來看下傳送的訊息插入訊息佇列的圖示

Handler---訊息插入訊息佇列

接收訊息

接受訊息相對而言就簡單多

Handler.java
...
public void dispatchMessage(@NonNull Message msg) {
    if (msg.callback != null) {
        handleCallback(msg);
    } else {
        if (mCallback != null) {
            if (mCallback.handleMessage(msg)) {
                return;
            }
        }
        handleMessage(msg);
    }
} 
...
  • handleCallback(msg)

    • 觸發條件:Message訊息中實現了handleCallback回撥
    • 現在基本上只能使用post()方法了,setCallback(Runnable r) 被表明為@UnsupportedAppUsage,被hide了,沒法呼叫,如果使用反射倒是可以呼叫,但是沒必要。。。
  • mCallback.handleMessage(msg)

    • 觸發條件
      • 使用sendMessage方法傳送訊息
      • 實現Handler的Callback回撥
      • mCallback.handleMessage(msg)返回為false
    • 分發的訊息,會在Handler中實現的回撥中分發
  • handleMessage(msg)

    • 觸發條件
      • 使用sendMessage方法傳送訊息
      • 未實現Handler的Callback回撥或者mCallback.handleMessage(msg)返回為true
    • 需要重寫Handler類的handlerMessage方法

分發訊息

訊息分發是在loop()中完成的,來看看loop()這個重要的方法

  • Looper.loop():精簡了巨量原始碼,詳細的可以點選左側方法名
    • Message msg = queue.next():遍歷訊息
    • msg.target.dispatchMessage(msg):分發訊息
    • msg.recycleUnchecked():訊息回收,進入訊息池
Looper.java
...
public static void loop() {
    final Looper me = myLooper();
    ...
    final MessageQueue queue = me.mQueue;
   	...
    for (;;) {
        //遍歷訊息池,獲取下一可用訊息
        Message msg = queue.next(); // might block
        ...
        try {
            //分發訊息
            msg.target.dispatchMessage(msg);
            ...
        } catch (Exception exception) {
            ...
        } finally {
            ...
        }
        ....
        //回收訊息,進圖訊息池
        msg.recycleUnchecked();
    }
}
...

遍歷訊息

遍歷訊息的關鍵方法肯定是下面這個

  • Message msg = queue.next():Message類中的next()方法;當然這必須要配合外層for(無限迴圈)來使用,才能遍歷訊息佇列

來看看這個Message中的next()方法吧

  • next():精簡了一些原始碼,完整的點選左側方法名
MessageQueue.java
...
Message next() {
    final long ptr = mPtr;
    ...

    int pendingIdleHandlerCount = -1; // -1 only during first iteration
    int nextPollTimeoutMillis = 0;
    for (;;) {
  		...
        //阻塞,除非到了超時時間或者喚醒
        nativePollOnce(ptr, nextPollTimeoutMillis);
        synchronized (this) {
            // Try to retrieve the next message.  Return if found.
            final long now = SystemClock.uptimeMillis();
            Message prevMsg = null;
            Message msg = mMessages;
            // 這是關於同步屏障(SyncBarrier)的知識,放在同步屏障欄目講
            if (msg != null && msg.target == null) {
                do {
                    prevMsg = msg;
                    msg = msg.next;
                } while (msg != null && !msg.isAsynchronous());
            }
            
            if (msg != null) {
                if (now < msg.when) {
                    //每個訊息處理有耗時時間,之間存在一個時間間隔(when是將要執行的時間點)。
                    //如果當前時刻還沒到執行時刻(when),計算時間差值,傳入nativePollOnce定義喚醒阻塞的時間
                    nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
                } else {
                    mBlocked = false;
                    //該操作是把非同步訊息單獨從訊息佇列裡面提出來,然後返回,返回之後,該非同步訊息就從訊息佇列裡面剔除了
                    //mMessage仍處於未分發的同步訊息位置
                    if (prevMsg != null) {
                        prevMsg.next = msg.next;
                    } else {
                        mMessages = msg.next;
                    }
                    msg.next = null;
                    if (DEBUG) Log.v(TAG, "Returning message: " + msg);
                    msg.markInUse();
                    //返回符合條件的Message
                    return msg;
                }
            } else {
                // No more messages.
                nextPollTimeoutMillis = -1;
            }

            //這是處理呼叫IdleHandler的操作,有幾個條件
        	//1、當前訊息佇列為空(mMessages == null)
            //2、已經到了可以分發下一訊息的時刻(now < mMessages.when)
            if (pendingIdleHandlerCount < 0
                && (mMessages == null || now < mMessages.when)) {
                pendingIdleHandlerCount = mIdleHandlers.size();
            }
            if (pendingIdleHandlerCount <= 0) {
                // No idle handlers to run.  Loop and wait some more.
                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 {
                keep = idler.queueIdle();
            } catch (Throwable t) {
                Log.wtf(TAG, "IdleHandler threw exception", t);
            }

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

        // Reset the idle handler count to 0 so we do not run them again.
        pendingIdleHandlerCount = 0;

        // While calling an idle handler, a new message could have been delivered
        // so go back and look again for a pending message without waiting.
        nextPollTimeoutMillis = 0;
    }
}

總結下原始碼裡面表達的意思

  1. next()內部是個死迴圈,你可能會疑惑,只是拿下一節點的訊息,為啥要死迴圈?
    • 為了訊息執行耗時以及同步屏障等等,這個死迴圈是必要的
  2. nativePollOnce阻塞方法:到了超時時間(nextPollTimeoutMillis)或者通過喚醒方式(nativeWake),會解除阻塞狀態
    • nextPollTimeoutMillis大於等於零,會規定在此段時間內休眠,然後喚醒
    • 訊息佇列為空時,nextPollTimeoutMillis為-1,進入阻塞;重新有訊息進入佇列,插入頭結點的時候會觸發nativeWake喚醒方法
  3. 如果 msg.target == null為零,會進入同步屏障狀態
    • 會將msg訊息死迴圈到末尾節點,除非碰到非同步方法
    • 如果碰到同步屏障訊息,理論上會一直死迴圈上面操作,並不會返回訊息,除非,同步屏障訊息被移除訊息佇列
  4. 如果當前時刻和返回訊息的超時時刻判定
    • 當前時刻小於返回訊息超時時間:進入阻塞,計算時間差,給nativePollOnce設定超時時間,超時時間一到,解除阻塞,重新迴圈取訊息
    • 當前時刻大於返回訊息超時時間:獲取可用訊息返回
    • when這個變數可能比較疑惑,這玩意是怎麼來的,初始值不都是0嗎?
      • 實際上我們傳送訊息的時候,可以傳送延時訊息:sendMessageDelayed(@NonNull Message msg, long delayMillis)
      • MessageQueue類中的enqueueMessage方法,會把延時時間( SystemClock.uptimeMillis() + delayMillis)賦值給Message物件的when欄位
  5. 訊息返回後,會將mMessage賦值為返回訊息的下一節點(只針對不涉及同步屏障的同步訊息)

這裡簡單的畫了個流程圖

Handler---訊息遍歷

分發訊息

分發訊息主要的程式碼是: msg.target.dispatchMessage(msg);

也就是說這是Handler類中的dispatchMessage(msg)方法

public void dispatchMessage(@NonNull Message msg) {
    if (msg.callback != null) {
        handleCallback(msg);
    } else {
        if (mCallback != null) {
            if (mCallback.handleMessage(msg)) {
                return;
            }
        }
        handleMessage(msg);
    }
}

可以看到,這裡的程式碼,在收發訊息欄目的接受訊息那塊已經說明過了,這裡就無須重複了

訊息池

msg.recycleUnchecked()是處理完成分發的訊息,完成分發的訊息並不會被回收掉,而是會進入訊息池,等待被複用

  • recycleUnchecked():回收訊息的程式碼還是蠻簡單的,來分析下
    • 首先會將當前已經分發處理的訊息,相關屬性全部重置,flags也標誌可用
    • 訊息池的頭結點會賦值為當前回收訊息的下一節點,當前訊息成為訊息池頭結點
    • 簡言之:回收訊息插入訊息池,當做頭結點
    • 需要注意的是:訊息池有最大的容量,如果訊息池大於等於預設設定的最大容量,將不再接受回收訊息入池
      • 預設最大容量為50: MAX_POOL_SIZE = 50
Message.java
...
void recycleUnchecked() {
    // 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 = UID_NONE;
    workSourceUid = UID_NONE;
    when = 0;
    target = null;
    callback = null;
    data = null;

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

來看下訊息池回收訊息圖示

Handler---訊息池(存)

既然有將已使用的訊息回收到訊息池的操作,那肯定有獲取訊息池裡面訊息的方法了

  • obtain():程式碼很少,來看看
    • 如果訊息池不為空:直接取訊息池的頭結點,被取走頭結點的下一節點成為訊息池的頭結點
    • 如果訊息池為空:直接返回新的Message例項
Message.java
...
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();
}

來看下從訊息池取一個訊息的圖示

Handler---訊息池(取)

IdleHandler

在MessageQueue類中的next方法裡,可以發現有關於對IdleHandler的處理,大家可千萬別以為它是什麼Handler特殊形式之類,這玩意就是一個interface,裡面抽象了一個方法,結構非常的簡單

  • next():精簡了大量原始碼,只保留IdleHandler處理的相關邏輯;完整的點選左側方法名
MessageQueue.java
...
Message next() {
    final long ptr = mPtr;
    ...

    int pendingIdleHandlerCount = -1; // -1 only during first iteration
    int nextPollTimeoutMillis = 0;
    for (;;) {
  		...
        //阻塞,除非到了超時時間或者喚醒
        nativePollOnce(ptr, nextPollTimeoutMillis);
        synchronized (this) {
            // Try to retrieve the next message.  Return if found.
            final long now = SystemClock.uptimeMillis();
            Message prevMsg = null;
            Message msg = mMessages;
			...
            //這是處理呼叫IdleHandler的操作,有幾個條件
        	//1、當前訊息佇列為空(mMessages == null)
            //2、未到到了可以分發下一訊息的時刻(now < mMessages.when)
			//3、pendingIdleHandlerCount < 0表明:只會在此for迴圈裡執行一次處理IdleHandler操作
            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 {
                keep = idler.queueIdle();
            } catch (Throwable t) {
                Log.wtf(TAG, "IdleHandler threw exception", t);
            }

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

        pendingIdleHandlerCount = 0;
        nextPollTimeoutMillis = 0;
    }
}

實際上從上面的程式碼裡面,可以分析出很多資訊

IdleHandler相關資訊

  • 呼叫條件

    • 當前訊息佇列為空(mMessages == null) 或 未到分發返回訊息的時刻
    • 在每次獲取可用訊息的死迴圈中,IdleHandler只會被處理一次:處理一次後pendingIdleHandlerCount為0,其迴圈不可再被執行
  • 實現了IdleHandler中的queueIdle方法

    • 返回false,執行後,IdleHandler將會從IdleHandler列表中移除,只能執行一次:預設false
    • 返回true,每次分發返回訊息的時候,都有機會被執行:處於保活狀態
  • IdleHandler程式碼

    MessageQueue.java
    ...
    /**
     * Callback interface for discovering when a thread is going to block
     * waiting for more messages.
     */
    public static interface IdleHandler {
        /**
         * Called when the message queue has run out of messages and will now
         * wait for more.  Return true to keep your idle handler active, false
         * to have it removed.  This may be called if there are still messages
         * pending in the queue, but they are all scheduled to be dispatched
         * after the current time.
         */
        boolean queueIdle();
    }
    
    public void addIdleHandler(@NonNull IdleHandler handler) {
        if (handler == null) {
            throw new NullPointerException("Can't add a null IdleHandler");
        }
        synchronized (this) {
            mIdleHandlers.add(handler);
        }
    }
    
    public void removeIdleHandler(@NonNull IdleHandler handler) {
        synchronized (this) {
            mIdleHandlers.remove(handler);
        }
    }
    
  • 怎麼使用IdleHandler呢?

    • 這裡簡單寫下用法,可以看看,留個印象
    public class MainActivity extends AppCompatActivity {
        private TextView msgTv;
        private Handler mHandler = new Handler();
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            msgTv = findViewById(R.id.tv_msg);
            //新增IdleHandler實現類
            mHandler.getLooper().getQueue().addIdleHandler(new InfoIdleHandler("我是IdleHandler"));
            mHandler.getLooper().getQueue().addIdleHandler(new InfoIdleHandler("我是大帥比"));
    
            //訊息收發一體
            new Thread(new Runnable() {
                @Override public void run() {
                    String info = "第一種方式";
                    mHandler.post(new Runnable() {
                        @Override public void run() {
                            msgTv.setText(info);
                        }
                    });
                }
            }).start();
        }
    
        //實現IdleHandler類
        class InfoIdleHandler implements MessageQueue.IdleHandler {
            private String msg;
    
            InfoIdleHandler(String msg) {
                this.msg = msg;
            }
    
            @Override
            public boolean queueIdle() {
                msgTv.setText(msg);
                return false;
            }
        }
    }
    

總結

  • 通俗的講:當所有訊息處理完了 或者 你傳送了延遲訊息,在這倆種空閒時間裡,都滿足執行IdleHandler的條件
    • 這地方需要說明下,如果延遲訊息時間設定過短的;IdleHandler可能會在傳送訊息後執行,畢竟執行到next這步也需要一點時間,延遲時間設定長點,你就可以很明顯得發現,IdleHandler在延遲的空隙間執行了!
  • 從其原始碼上,可以看出來,IdlerHandler是在訊息處理的空閒時刻,專門用來處理相關事物的

同步屏障

來到最複雜的模組了

在理解同步屏障的概念前,我們需要先搞懂幾個前置知識

前置知識

同步和非同步訊息

什麼是同步訊息?什麼是非同步訊息?

  • 講真的,非同步訊息和同步訊息界定,完成是通過一個方法去界定的
  • isAsynchronous():來分析下
    • FLAG_ASYNCHRONOUS = 1 << 1:所以FLAG_ASYNCHRONOUS為2
    • 同步訊息:flags為0或者1的時候,isAsynchronous返回false,此時該訊息標定為同步訊息
      • flags為0,1:同步訊息
    • 非同步訊息:理論上只要按照位操作,左往右,第二位為1的數,isAsynchronous返回true;但是,Message裡面基本只使用了:0,1,2,可得出結論
      • flags為2:非同步訊息
public boolean isAsynchronous() {
    return (flags & FLAG_ASYNCHRONOUS) != 0;
}
  • setAsynchronous(boolean async):這個方法會影響flags的值
    • 因為flags是int型別,沒有賦初值,故其初始值為0
    • setAsynchronous傳入true的話,或等於操作,會將flags數值改成2
msg.setAsynchronous(true);

public void setAsynchronous(boolean async) {
    if (async) {
        flags |= FLAG_ASYNCHRONOUS;
    } else {
        flags &= ~FLAG_ASYNCHRONOUS;
    }
}
  • 怎麼生成非同步訊息?so easy
Message msg = Message.obtain();
//設定非同步訊息標記
msg.setAsynchronous(true);
  • 一般來說:預設訊息不做設定,flags都為0,故預設為同步訊息,下面欄目將分析下setAsynchronous在何處使用了

預設訊息型別

我們正常情況下,很少會使用setAsynchronous方法的,那麼在不使用該方法的時候,訊息的預設型別是什麼呢?

  • 在生成訊息,然後傳送訊息的時候,都會經過下述方法
  • enqueueMessage:正常傳送訊息(post、延遲和非延遲之類),都會經過此方法
    • 因為傳送的所有訊息都會經過enqueueMessage方法,然後加入訊息佇列,可以看見所有的訊息都被處理過
    • msg.target = this
      • 這地方給Message類的target賦值了!
      • 說明:只要使用post或sendMessage之類傳送訊息,其訊息就絕不可能是同步屏障訊息!
    • 關於同步非同步,可以看見和mAsynchronous息息相關
      • 只要mAsynchronous為true的話,我們的訊息都會非同步訊息
      • 只要mAsynchronous為false的話,我們的訊息都會同步訊息
private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,long uptimeMillis) {
    msg.target = this;
    msg.workSourceUid = ThreadLocalWorkSource.getUid();

    if (mAsynchronous) {
        msg.setAsynchronous(true);
    }
    return queue.enqueueMessage(msg, uptimeMillis);
}
  • mAsynchronous在哪設定的呢?
    • 這是在構造方法裡面給mAsynchronous賦值了
public Handler(@Nullable Callback callback, boolean async) {
    if (FIND_POTENTIAL_LEAKS) {
        final Class<? extends Handler> klass = getClass();
        if ((klass.isAnonymousClass() || klass.isMemberClass() || klass.isLocalClass()) &&
            (klass.getModifiers() & Modifier.STATIC) == 0) {
            Log.w(TAG, "The following Handler class should be static or leaks might occur: " +
                  klass.getCanonicalName());
        }
    }

    mLooper = Looper.myLooper();
    if (mLooper == null) {
        throw new RuntimeException(
            "Can't create handler inside thread " + Thread.currentThread()
            + " that has not called Looper.prepare()");
    }
    mQueue = mLooper.mQueue;
    mCallback = callback;
    mAsynchronous = async;
}

public Handler(@NonNull Looper looper, @Nullable Callback callback, boolean async) {
    mLooper = looper;
    mQueue = looper.mQueue;
    mCallback = callback;
    mAsynchronous = async;
}
  • 看看一些通用的構造方法
public Handler() {
    this(null, false);
}

public Handler(@NonNull Looper looper) {
    this(looper, null, false);
}

public Handler(@NonNull Looper looper, @Nullable Callback callback) {
    this(looper, callback, false);
}
  • 總結下
    • 這下清楚了!如果不做特殊設定的話:預設訊息都是同步訊息
    • 預設訊息都會給其target熟悉賦值:預設訊息都不是同步屏障訊息

生成同步屏障訊息

在next方法中發現,target為null的訊息被稱為同步屏障訊息,那他為啥叫同步屏障訊息呢?

  • postSyncBarrier(long when)
    • sync:同步 barrier:屏障,障礙物 ---> 同步屏障
    • 同步屏障實際挺能代表其含義的,它能遮蔽訊息佇列中後續所有的同步方法分發
MessageQueue.java
...
@UnsupportedAppUsage
@TestApi
public int postSyncBarrier() {
    return postSyncBarrier(SystemClock.uptimeMillis());
}
    
private int postSyncBarrier(long when) {
    // Enqueue a new sync barrier token.
    // We don't need to wake the queue because the purpose of a barrier is to stall it.
    synchronized (this) {
        final int token = mNextBarrierToken++;
        final Message msg = Message.obtain();
        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) { // invariant: p == prev.next
            msg.next = p;
            prev.next = msg;
        } else {
            msg.next = p;
            mMessages = msg;
        }
        return token;
    }
}
  • mMessage這個變數,表明是將要被處理的訊息,將要被返回的訊息,也可以認為,他是未處理訊息佇列的頭結點訊息

  • 關於同步屏障訊息

    • 從訊息池取一個可用訊息
    • 頭結點(mMessage)是否為空
      • 不為空:插到頭結點的下一節點位置
      • 為空:成為頭結點
    • 同步屏障訊息是直接插到訊息佇列,他沒有設定target屬性且不經過enqueueMessage方法,故其target屬性為null
  • 同步屏障訊息插入訊息佇列流程圖

Handler---同步屏障入訊息佇列

同步屏障流程

  • next():精簡了大量原始碼碼,只保留和同步屏障有關的程式碼;完整的點選左側方法名
MessageQueue.java
...
Message next() {
    final long ptr = mPtr;
    ...

    int pendingIdleHandlerCount = -1; // -1 only during first iteration
    int nextPollTimeoutMillis = 0;
    for (;;) {
  		...
        //阻塞,除非到了超時時間或者喚醒
        nativePollOnce(ptr, nextPollTimeoutMillis);
        synchronized (this) {
            // Try to retrieve the next message.  Return if found.
            final long now = SystemClock.uptimeMillis();
            Message prevMsg = null;
            Message msg = mMessages;
            // 這是關於同步屏障(SyncBarrier)的邏輯塊
            if (msg != null && msg.target == null) {
                do {
                    prevMsg = msg;
                    msg = msg.next;
                } while (msg != null && !msg.isAsynchronous());
            }
            
            if (msg != null) {
                if (now < msg.when) {
                    //每個訊息處理有耗時時間,之間存在一個時間間隔(when是將要執行的時間點)。
                    //如果當前時刻還沒到執行時刻(when),計算時間差值,傳入nativePollOnce定義喚醒阻塞的時間
                    nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
                } else {
                    mBlocked = false;
                    //該操作是把非同步訊息單獨從訊息佇列裡面提出來,然後返回,返回之後,該非同步訊息就從訊息佇列裡面剔除了
                    //mMessage仍處於未分發的同步訊息位置
                    if (prevMsg != null) {
                        prevMsg.next = msg.next;
                    } else {
                        mMessages = msg.next;
                    }
                    msg.next = null;
                    if (DEBUG) Log.v(TAG, "Returning message: " + msg);
                    msg.markInUse();
                    //返回符合條件的Message
                    return msg;
                }
            } else {
                // No more messages.
                nextPollTimeoutMillis = -1;
            }
            ...
        }
        ...
    }
}

去掉大量我們無需關注的程式碼,發現這也沒啥嘛,就是一堆if eles for之類的,來​分析分析

  1. Message msg = mMessages:這步賦值是非常重要的,表示即使我們對msg一頓操作,mMessage還是保留訊息佇列頭結點訊息的位置
  2. msg.target == null:遇到同步屏障訊息
    1. 首先是一個while迴圈,內部邏輯,不斷將msg節點的位置後移
    2. 結束while的倆個條件
      • msg移到尾結點,也就是移到了訊息佇列尾結點,將自身賦值為null(尾結點的next)
      • 遇上標記為非同步的訊息,放行該訊息進行後續分發
  3. 分析下,倆個放行條件產生的不同影響
    1. 訊息佇列不含非同步訊息
      • 當我們在同步屏障邏輯裡面,將msg自身移到尾結點,並賦值為null(尾結點的next)
      • msg為null,是無法進行後續分發操作,會重新進行迴圈流程
      • mMessage頭結點重新將自身位置賦值給msg,繼續上述的重複過程
      • 可以發現,上述邏輯確實起到了同步屏障的作用,遮蔽了其所有後續同步訊息的分發;只有移除訊息佇列中的該條同步屏障訊息,才能繼續進行同步訊息的分發
    2. 訊息佇列含有非同步訊息
      • 訊息佇列中如果有非同步訊息,同步屏障的邏輯會放行非同步訊息
      • 同步屏障裡面堆prevMsg賦值了!請記住在整個方法裡面,只有同步屏障邏輯裡面堆prevMsg賦值了!這個引數為null與否,對訊息佇列節點影響很大
      • prevMsg為空:會直接將msg的next賦值給mMessage;說明分發完訊息後,會直接移除頭結點,將頭結點的下一節點賦值為頭結點
      • prevMsg不為空:不會對mMessage投節點操作;會將分發訊息的上一節點的下一節點位置,換成分發節點的下一節點,有點繞
      • 通過上面分析,可知;非同步訊息分發完後,會將其直接從訊息佇列中移除,頭結點位置不變

文字寫了一大堆,我也是儘可能詳細描述,同步屏障邏輯程式碼塊會產生的影響,整個圖,加深下印象!

同步屏障流程

同步屏障作用

那麼這個同步屏障有什麼作用呢?

有個急需的問題,就是什麼地方用到了postSyncBarrier(long when)方法,這個方法對外是不暴露的,只有內部包能夠呼叫

搜尋了整個原始碼包,發現只有幾個地方使用了它,剔除測試類,MessageQueue類,有作用的就是:ViewRootImpl類和Device類

Device類

  • pauseEvents():Device內部涉及的是開啟裝置的時候,會新增一個同步屏障訊息,遮蔽後續所有的同步訊息處理

    • pauseEvents()是Device類中私有內部類DeviceHandler的方法
      • 這說明,我們無法呼叫這個方法;事實上,我們連Device類都無法呼叫,Device屬於被隱藏的類,和他同一目錄的還有Event和Hid,這些類系統都不想對外暴露
      • 這就很雞賊了,說明插入同步屏障的訊息的方法,系統確實不想對外暴露;當然不包括非常規方法:反射
  • 同步屏障新增:開機時,新增同步屏障

    Device.java
    ...
    private class DeviceHandler extends Handler {
        ...
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case MSG_OPEN_DEVICE:
                    ...
                    pauseEvents();
                    break;
                ...
            }
        }
    
        public void pauseEvents() {
            mBarrierToken = getLooper().myQueue().postSyncBarrier();
        }
    
        public void resumeEvents() {
            getLooper().myQueue().removeSyncBarrier(mBarrierToken);
            mBarrierToken = 0;
        }
    }
    
  • 同步屏障移除:完成開機後,移除同步屏障

    Device.java
    ...
    private class DeviceHandler extends Handler {
        ...
        public void pauseEvents() {
            mBarrierToken = getLooper().myQueue().postSyncBarrier();
        }
    
        public void resumeEvents() {
            getLooper().myQueue().removeSyncBarrier(mBarrierToken);
            mBarrierToken = 0;
        }
    }
    
    private class DeviceCallback {
        public void onDeviceOpen() {
            mHandler.resumeEvents();
        }
        ....
    }
    
  • Device中使用同步屏障整體過程比較簡單,這裡簡單描述下

    • 開啟裝置時,在訊息佇列頭結點下一節點位置插入同步屏障訊息,遮蔽後續所有同步訊息
    • 完成開機後,移除同步屏障訊息
    • 總結:很明顯,這是儘量的提升開啟裝置速度,不被其它次等重要的事件干擾

ViewRootImpl類

該欄目的分析,必須引用一個非常重要的結論,給出該結論的文章:原始碼分析_Android UI何時重新整理_Choreographer

  • scheduleTraversals():非常重要的方法

    ViewRootImpl.java
    ...
    void scheduleTraversals() {
        if (!mTraversalScheduled) {
            mTraversalScheduled = true;
            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
            mChoreographer.postCallback(
                Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
            notifyRendererOfFramePending();
            pokeDrawLockIfNeeded();
        }
    }
    
  • 結論:原始碼分析_Android UI何時重新整理_Choreographer

    • 關於上面的方法的分析,整體流程比較麻煩,涉及到整個重新整理過程的分析
    • 這邊前輩的文章分析完UI重新整理流程,給出了一個非常重要的結論

    我們呼叫View的requestLayout或者invalidate時,最終都會觸發ViewRootImp執行scheduleTraversals()方法。這個方法中ViewRootImp會通過Choreographer來註冊個接收Vsync的監聽,當接收到系統體層傳送來的Vsync後我們就執行doTraversal()來重新繪製介面。通過上面的分析我們呼叫invalidate等重新整理操作時,系統並不會立即重新整理介面,而是等到Vsync訊息後才會重新整理頁面。

我們這邊已經有了前輩給出的結論,我們知道了介面重新整理(requestLayout或者invalidate)的過程一定會觸發scheduleTraversals()方法,這說明會新增同步屏障訊息,那肯定有移除同步屏障訊息的步驟,這個步驟很有可能存在doTraversal()方法中,來看下這個方法

  • doTraversal():removeSyncBarrier!我giao!果然在這地方!
    • 這地方做了倆件事:移除同步屏障(removeSyncBarrier)、繪製介面(performTraversals)
void doTraversal() {
    if (mTraversalScheduled) {
        mTraversalScheduled = false;
        mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);

        if (mProfile) {
            Debug.startMethodTracing("ViewAncestor");
        }

        performTraversals();

        if (mProfile) {
            Debug.stopMethodTracing();
            mProfile = false;
        }
    }
}
  • doTraversal()是怎麼被呼叫呢?

    • 呼叫:mTraversalRunnable在scheduleTraversals()中使用了

      final TraversalRunnable mTraversalRunnable = new TraversalRunnable();
      
      void scheduleTraversals() {
          if (!mTraversalScheduled) {
              ...
              mChoreographer.postCallback(
                  Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
            ...
          }
      

    }

    final class TraversalRunnable implements Runnable {
    @Override
    public void run() {
    doTraversal();
    }
    }

    
    
  • postCallback是Choreographer類中方法,該類涉及巨多的訊息傳遞,而且都是使用了非同步訊息setAsynchronous(true),這些都是和介面重新整理相關,所以都是優先處理,完整的流程可以看上面貼的文章

  • postCallback的核心就是讓DisplayEventReceiver註冊了個Vsync的通知,後期收到送來的Vsync後,我們就執行doTraversal()來重新繪製介面

總結

  • 通過上面的對ViewRootImpl說明,需要來總結下同步屏障對介面繪製過程的影響
  • 詳細版總結(不講人話版)

呼叫View的requestLayout或者invalidate時,最終都會執行scheduleTraversals(),此時會在主執行緒訊息佇列中插入一個同步屏障訊息(停止所有同步訊息分發),會將mTraversalRunnable新增到mCallbackQueues中,並註冊接收Vsync的監聽,當接受到Vsync通知後,會傳送一個非同步訊息,觸發遍歷執行mCallbackQueues的方法,這會執行我們新增的回撥mTraversalRunnable,從而執行doTraversal(),此時會移除主執行緒訊息佇列中同步屏障訊息,最後執行繪製操作

  • 通俗版總結

呼叫requestLayout或者invalidate時,會在主執行緒訊息佇列中插入一個同步屏障訊息,同時註冊接收Vsync的監聽;當接受到Vsync通知,會傳送一個非同步訊息,執行真正的繪製事件:此時會移除訊息佇列中的同步屏障訊息,然後才會執行繪製操作

  • 下面給不講人話版畫了個流轉圖示

Handler---同步屏障作用

總結

訊息插入區別

  • 有個很重要的事情,我們再來看下:正常傳送訊息和同步屏障訊息插入訊息佇列直接的區別,見下圖
    • 取訊息:關於取訊息,都是取的mMessage,可以理解為,取訊息佇列的頭結點
    • 正常傳送訊息:插入到訊息佇列的尾結點
    • 訊息屏障訊息:插入到訊息佇列頭結點的下一節點

正常發訊息和同步屏障

Vsync

  • 關於Vsync

    • Vsync 訊號一般是由硬體產生的,現在手機一般為60hz~120hz,每秒重新整理60到120次,一個時間片算一幀
    • 每個 Vsync 訊號之間的時間就是一幀的時間段
  • 來看下執行同步訊息時間片:這圖真吉兒不好畫,吐血

同步屏障在ViewRootImpl中作用

  • 由上圖可知:某種極端情況,你所傳送的訊息,在分發的時候,可能存在一幀的延時

總結

相關總結

  • 同步屏障能確保訊息佇列中的非同步訊息,會被優先執行
  • 鑑於正常訊息和同步屏障訊息插入訊息佇列的區別:同步屏障能夠及時的屏障佇列中的同步訊息
  • 某些極端場景:傳送的訊息,在分發的時候,可能會存一幀延時
    • 極端場景:Vsync訊號到來之後,立馬執行了RequestLayout等操作
  • 同步屏障能確保在UI重新整理中:Vsync訊號到來後,能夠立馬執行真正的繪製頁面操作

同步訊息和非同步訊息使用建議

在正常的情況,肯定不建議使用非同步訊息,此處假設一個場景:因為某種需求,你傳送了大量的非同步訊息,由於訊息進入訊息佇列的特殊性,系統傳送的非同步訊息,也只能乖乖的排在你的非同步訊息後面,假設你的非同步訊息佔據了大量的時間片,甚至佔用了幾幀,導致系統UI重新整理的非同步訊息無法被及時執行,此時很有可能發生掉幀

當然,如果你能看明白這個同步屏障欄目所寫的東西,相信什麼時候設定訊息為非同步,心中肯定有數

  • 正常情況,請繼續使用同步訊息
  • 特殊情況,必須需要自己的訊息被第一時間被處理:可以使用非同步訊息

考點

上面原始碼基本就分析到這邊了,我們們看看能根據這些知識點,能提一些什麼問題呢?

1、先來個自己想的問題:Handler中主執行緒的訊息佇列是否有數量上限?為什麼?

這問題整的有點雞賊,可能會讓你想到,是否有上限這方面?而不是直接想到到上限數量是多少?

解答:Handler主執行緒的訊息佇列肯定是有上限的,每個執行緒只能例項化一個Looper例項(上面講了,Looper.prepare只能使用一次),不然會拋異常,訊息佇列是存在Looper()中的,且僅維護一個訊息佇列

重點:每個執行緒只能例項化一次Looper()例項、訊息佇列存在Looper中

擴充:MessageQueue類,其實都是在維護mMessage,只需要維護這個頭結點,就能維護整個訊息連結串列

2、Handler中有Loop死迴圈,為什麼沒有卡死?為什麼沒有發生ANR?

先說下ANR:5秒內無法響應螢幕觸控事件或鍵盤輸入事件;廣播的onReceive()函式時10秒沒有處理完成;前臺服務20秒內,後臺服務在200秒內沒有執行完畢;ContentProvider的publish在10s內沒進行完。所以大致上Loop死迴圈和ANR聯絡不大,問了個正確的廢話,所以觸發事件後,耗時操作還是要放在子執行緒處理,handler將資料通訊到主執行緒,進行相關處理。

執行緒實質上是一段可執行的程式碼片,執行完之後,執行緒就會自動銷燬。當然,我們肯定不希望主執行緒被over,所以整一個死迴圈讓執行緒保活。

為什麼沒被卡死:在事件分發裡面分析了,在獲取訊息的next()方法中,如果沒有訊息,會觸發nativePollOnce方法進入執行緒休眠狀態,釋放CPU資源,MessageQueue中有個原生方法nativeWake方法,可以解除nativePollOnce的休眠狀態,ok,我們們在這倆個方法的基礎上來給出答案

  • 當訊息佇列中訊息為空時,觸發MessageQueue中的nativePollOnce方法,執行緒休眠,釋放CPU資源

  • 訊息插入訊息佇列,會觸發nativeWake喚醒方法,解除主執行緒的休眠狀態

    • 當插入訊息到訊息佇列中,為訊息佇列頭結點的時候,會觸發喚醒方法
    • 當插入訊息到訊息佇列中,在頭結點之後,鏈中位置的時候,不會觸發喚醒方法
  • 綜上:訊息佇列為空,會阻塞主執行緒,釋放資源;訊息佇列為空,插入訊息時候,會觸發喚醒機制

    • 這套邏輯能保證主執行緒最大程度利用CPU資源,且能及時休眠自身,不會造成資源浪費
  • 本質上,主執行緒的執行,整體上都是以事件(Message)為驅動的

3、為什麼不建議在子執行緒中更新UI?

多執行緒操作,在UI的繪製方法表示這不安全,不穩定。

假設一種場景:我會需要對一個圓進行改變,A執行緒將圓增大倆倍,B改變圓顏色。A執行緒增加了圓三分之一體積的時候,B執行緒此時,讀取了圓此時的資料,進行改變顏色的操作;最後的結果,可能會導致,大小顏色都不對。。。

4、可以讓自己傳送的訊息優先被執行嗎?原理是什麼?

這個問題,我感覺只能說:在有同步屏障的情況下是可以的。

同步屏障作用:在含有同步屏障的訊息佇列,會及時的遮蔽訊息佇列中所有同步訊息的分發,放行非同步訊息的分發。

在含有同步屏障的情況,我可以將自己的訊息設定為非同步訊息,可以起到優先被執行的效果。

5、子執行緒和子執行緒使用Handler進行通訊,存在什麼弊端?

子執行緒和子執行緒使用Handler通訊,某個接受訊息的子執行緒肯定使用例項化handler,肯定會有Looper操作,Looper.loop()內部含有一個死迴圈,會導致執行緒的程式碼塊無法被執行完,該執行緒始終存在。

如果在完成通訊操作,我們一般可以使用: mHandler.getLooper().quit() 來結束分發操作

  • 說明下quit()方法本質上是清空訊息佇列,讓該執行緒進入休眠,確保所有事務都處理完,可以使用thread.interrupt()中斷執行緒

6、Handler中的阻塞喚醒機制?

這個阻塞喚醒機制是基於 Linux 的 I/O 多路複用機制 epoll 實現的,它可以同時監控多個檔案描述符,當某個檔案描述符就緒時,會通知對應程式進行讀/寫操作.

MessageQueue 建立時會呼叫到 nativeInit,建立新的 epoll 描述符,然後進行一些初始化並監聽相應的檔案描述符,呼叫了epoll_wait方法後,會進入阻塞狀態;nativeWake觸發對操作符的 write 方法,監聽該操作符被回撥,結束阻塞狀態

詳細請檢視:同步屏障?阻塞喚醒?和我一起重讀 Handler 原始碼

7、什麼是IdleHandler?什麼條件下觸發IdleHandler?

IdleHandler的本質就是介面,為了在訊息分發空閒的時候,能處理一些事情而設計出來的

具體條件:訊息佇列為空的時候、傳送延時訊息的時候

8、訊息處理完後,是直接銷燬嗎?還是被回收?如果被回收,有最大容量嗎?

Handler存在訊息池的概念,處理完的訊息會被重置資料,採用頭插法進入訊息池,取的話也直接取頭結點,這樣會節省時間

訊息池最大容量為50,達到最大容量後,不再接受訊息進入

9、不當的使用Handler,為什麼會出現記憶體洩漏?怎麼解決?

先說明下,Looper物件在主執行緒中,整個生命週期都是存在的,MessageQueue是在Looper物件中,也就是訊息佇列也是存在在整個主執行緒中;我們知道Message是需要持有Handler例項的,Handler又是和Activity存在強引用關係

存在某種場景:我們關閉當前Activity的時候,當前Activity傳送的Message,在訊息佇列還未被處理,Looper間接持有當前activity引用,因為倆者直接是強引用,無法斷開,會導致當前Activity無法被回收

思路:斷開倆者之間的引用、處理完分發的訊息,訊息被處理後,之間的引用會被重置斷開

解決:使用靜態內部類弱引Activity、清空訊息佇列

最後

寫這篇文章加上思維導圖,也大概整了十三來張圖,我真的盡力了!

image-20210411191511065

相關文章