在之前的面試當中,我被問到了這么一個問題“如果一個Handler收到大量的Message的時候會發(fā)生什么?”最近閑來無事,做了一個Demo實驗了一下,以下是相關的心路歷程。
先說結論
如果一個使用主線程Looper的Handler在一段時間內收到大量的message的時候,消息過多,可能會使得消息處理不及時。帶來的副作用是可能會使得界面的刷新和touch事件的響應延遲
相應的Demo代碼
public class MainActivity extends BaseActivity {
private Handler handler = new Handler(Looper.getMainLooper()) {
@Override
public void handleMessage(@NonNull Message msg) {
if (msg.what == 1) {
String time = (String) msg.obj;
Log.e("Test", time);
}
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
final View circle = findViewById(R.id.circle);
findViewById(R.id.btn).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.e("Testttttttt", "Click!!!!");
((MiFloatWindowCircle) circle).start();
}
});
}
@Override
protected void onResume() {
super.onResume();
new Thread(new Runnable() {
@Override
public void run() {
int i = 0;
while (true) {
String msg = "0 + " + System.currentTimeMillis();
if (i % 50 == 0) {
} else if (i < 2000) {
handler.sendMessage(handler.obtainMessage(1, msg));
}else {
break;
}
i++;
}
}
}).start();
}
}
心路歷程
在尋找這個原因的時候,我首先需要解決的一個問題就是“為什么這么做不會觸發(fā)ANR?”
首先就需要知道ANR發(fā)生的原理
ANR是什么?為什么不會觸發(fā)ANR
這是需要解決的第一個問題。經過查閱大量的資料后大致可以理解為:
在某個方法開始執(zhí)行之前發(fā)送一個延時任務,如果在延時任務觸發(fā)之前執(zhí)行完成,則取消相應的延時任務。那么這個時候就不會觸發(fā)ANR;如果觸發(fā)了這個延時任務,那么就會產生ANR。
經過查閱相關資料和代碼后發(fā)現(xiàn)。
在AMS的UiHandler當中有這么一段代碼
final class UiHandler extends Handler {
public UiHandler() {
super(com.android.server.UiThread.get().getLooper(), null, true);
}
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case SHOW_NOT_RESPONDING_UI_MSG: {
mAppErrors.handleShowAnrUi(msg);
ensureBootCompleted();
} break;
可以看到,當UiHandler收到SHOW_NOT_RESPONDING_UI_MSG就會觸發(fā)展示“程序無響應”的Dialog
到這個時候,似乎可以解釋為什么在收到大量Msg的時候不會觸發(fā)ANR的原因了——(因為Handler的Msg的處理是一個非常典型的生產者——消費者場景)生產者生成了太多的Msg,導致緩存中觸發(fā)ANR的Msg遲遲得不到處理,從而不會觸發(fā)ANR。但是當使用下面一種測試代碼進行測試的時候,仍然觸發(fā)了ANR
@Override
protected void onResume() {
super.onResume();
while (true) {
String msg = "0 + " + System.currentTimeMillis();
handler.sendMessage(handler.obtainMessage(1, msg));
}
}
所以這種解釋似乎就不能成立。
同時經過查看UiHandler所屬的Looper也發(fā)現(xiàn),UiHandler其實并不屬于這個Application中的主線程的Looper,而是使用一個com.android.server.UiThread.get().getLooper()的Looper。
如果ANR延時任務所屬的Looper和我們發(fā)送使用的Looper不是同一個Looper的話,那么這種推測就是不成立的。
同時在查閱資料和實驗后,對于Activity的ANR場景有了進一步的理解——只要用戶不進行輸入操作,其實是不會觸發(fā)ANR的。Activity的ANR是在inputDisptcher在通知inputChannel inputEvent的同時發(fā)送的。所以即使是像上面那樣,在onResume中寫一個死循環(huán),只要在運行的時候不進行輸入的操作。依然不會觸發(fā)ANR。
換一種思路
既然現(xiàn)象是界面無響應,那么就需要看一下View在postInvalidate()的時候做了什么
/**
* <p>Cause an invalidate to happen on a subsequent cycle through the event
* loop. Waits for the specified amount of time.</p>
*
* <p>This method can be invoked from outside of the UI thread
* only when this View is attached to a window.</p>
*
* @param delayMilliseconds the duration in milliseconds to delay the
* invalidation by
*
* @see #invalidate()
* @see #postInvalidate()
*/
public void postInvalidateDelayed(long delayMilliseconds) {
// We try only with the AttachInfo because there's no point in invalidating
// if we are not attached to our window
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
attachInfo.mViewRootImpl.dispatchInvalidateDelayed(this, delayMilliseconds);
}
}
public void dispatchInvalidateDelayed(View view, long delayMilliseconds) {
Message msg = mHandler.obtainMessage(MSG_INVALIDATE, view);
mHandler.sendMessageDelayed(msg, delayMilliseconds);
}
經過一路追蹤以后追蹤到,View在調用postInvalidate()以后會發(fā)送一條msg到ViewRootHandler中,
而這個ViewRootHandler在初始化的時候沒有傳遞它需要使用哪個Looper來進行消息的處理。
根據Android的基礎知識可以得知,View只在主線程進行操作。所以ViewRootHandler的構造方法中的Looper.myLooper()最后得到的是MainThread中的Looper。
到這里似乎可以解釋為什么在主線程短時間內大量發(fā)送msg的時候可能會導致界面卡頓——是因為相關界面刷新的msg排隊相對靠后,無法第一時間進行處理。
但是在使用Demo進行測試的時候除了界面無法刷新這個問題,還有另一個問題——onClick事件也無法進行響應。
有了上面這個思路,這個問題的原因找起來就相對順利多了。
首先,在View體系下,第一個接收到TouchEvent的一定是RootView的dispatchTouchEvent()。所以我們需要知道,是誰調用的dispatchTouchEvent()即可。
經過查找,發(fā)現(xiàn)在ViewRootImpl有上文提到的inputChannel,然后在相關代碼中發(fā)現(xiàn)了這么一段代碼
mInputEventReceiver = new WindowInputEventReceiver(mInputChannel,
Looper.myLooper());
Looper.myLooper()?ViewRootImpl應該也是同屬于View體系下的,所以在這里調用這個方法應該返回的是MainThread的Looper,繼續(xù)跟蹤下去發(fā)現(xiàn)
/**
* Creates an input event receiver bound to the specified input channel.
*
* @param inputChannel The input channel.
* @param looper The looper to use when invoking callbacks.
*/
public InputEventReceiver(InputChannel inputChannel, Looper looper) {
if (inputChannel == null) {
throw new IllegalArgumentException("inputChannel must not be null");
}
if (looper == null) {
throw new IllegalArgumentException("looper must not be null");
}
mInputChannel = inputChannel;
mMessageQueue = looper.getQueue();
mReceiverPtr = nativeInit(new WeakReference<InputEventReceiver>(this),
inputChannel, mMessageQueue);
mCloseGuard.open("dispose");
}
到這里就進入了native方法了,暫時就不能繼續(xù)跟蹤下去了。
但是它既然把主線程的Looper傳進去了,那么就說明點擊事件也與主線程的Looper有關。所以就可以解釋為什么可能會導致主線程的點擊事件可能會出現(xiàn)延遲。同繪制事件的理由一樣。