由View的onAttachedToWindow引發(fā)的圖片輪播問題探究

前言
本篇文章是在View的postDelayed方法深度思考這篇文章的所有的基礎(chǔ)理論上進行研究的,可以說是對于View的postDelayed方法深度思考這篇文章知識點的實踐。
某天同事某進在做一個列表頁添加輪播Banner的需求的時候,發(fā)下偶爾會出現(xiàn)輪播間隔時間錯亂的問題。
我看了他的輪播的實現(xiàn)方案:利用Handle.postDelayed間隔輪播時長每次執(zhí)行完輪播之后再次循環(huán)發(fā)送;

代碼貌似沒有太大問題,但通過現(xiàn)象看來應(yīng)該是removeCallbacks失效了~!
Handle#removeCallbacks
在stackoverflow上找了相關(guān)資料Why to use removeCallbacks() with postDelayed()?,之后嘗試將postDelayed不靠譜那么改為post,發(fā)現(xiàn)貌似輪播間隔時間錯亂的問題解決了~!
雖然不清楚什么原因?qū)е聠栴}不再出現(xiàn),但后續(xù)因為其他工作打斷未能繼續(xù)排查下去。
若干天之后,再次發(fā)現(xiàn)輪播間隔時間錯亂的問題有一次出現(xiàn)了。
這次我們使用自定
Handler進行removeCallBacks和postDelayed,完美的解決了問題。
下面記錄一下整問題解決過程中的思考~!
待解決問題
-
View.removeCallbacks是否真的可靠; -
View.post和View.postDelayed相比為什么bug復現(xiàn)頻率更低;
View#dispatchAttachedToWindow
Handle的removeCallBacks移除方法是不可靠的么?如果當前的任務(wù)不是在執(zhí)行中,那么該任務(wù)一定會被移除。
換句話說,Handle#removeCallBacks移除的就是在隊列中等待被執(zhí)行的Message。
那么問題到底出在哪里,而且為什么
postDelayed替換為post問題的復現(xiàn)概率降低了?
這次有些時間,跟了一下源碼發(fā)現(xiàn)使用View#postDelayed發(fā)送的消息不一定會立即被放在消息隊列。
回顧之前View的postDelayed方法深度思考這篇文章中關(guān)于View.postDelayed小結(jié)中的描述:
postDelayed方法調(diào)用的時候,如果當前的View沒有依附在Window上的時候,先將Runnable緩存在RunQueue隊列中。等到View.dispatchAttachedToWindow調(diào)用之后,再被ViewRootHandler進行一次postDelayed。這個過程中相同的Runnable只會被postDelay一次。
我們打印stopTimer和startTimer方法執(zhí)行的時ViewPager#getHandler的Handler實例,發(fā)現(xiàn)在列表快速滑動時大部分為null。
好吧,之前忽略了這個Banner在滑動過程中的被View#dispatchDetachedFromWindow。這個方法的調(diào)用會導致View內(nèi)部的Handle為null。
如果View的Handle為null,那么Message的執(zhí)行可能會收到影響。
在View的postDelayed方法深度思考這篇文章中關(guān)于mAttachInfo對于View.postDelayed的影響,也都進行了分析。這里我們撿主要的源碼閱讀一下。
//View.java
void dispatchAttachedToWindow(AttachInfo info, int visibility) {
mAttachInfo = info;
/****部分代碼省略*****/
// Transfer all pending runnables.
if (mRunQueue != null) {
mRunQueue.executeActions(info.mHandler);
mRunQueue = null;
}
performCollectViewAttributes(mAttachInfo, visibility);
onAttachedToWindow();
/****部分代碼省略*****/
}
public boolean postDelayed(Runnable action, long delayMillis) {
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
return attachInfo.mHandler.postDelayed(action, delayMillis);
}
// Postpone the runnable until we know on which thread it needs to run.
// Assume that the runnable will be successfully placed after attach.
getRunQueue().postDelayed(action, delayMillis);
return true;
}
public boolean post(Runnable action) {
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
return attachInfo.mHandler.post(action);
}
// Postpone the runnable until we know on which thread it needs to run.
// Assume that the runnable will be successfully placed after attach.
getRunQueue().post(action);
return true;
}
public boolean removeCallbacks(Runnable action) {
if (action != null) {
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
attachInfo.mHandler.removeCallbacks(action);
attachInfo.mViewRootImpl.mChoreographer.removeCallbacks(
Choreographer.CALLBACK_ANIMATION, action, null);
}
getRunQueue().removeCallbacks(action);
}
return true;
}
post和postDelayed在View的postDelayed方法深度思考這篇文章中進行過講解,會在View執(zhí)行dispatchAttachedToWindow方法的時候執(zhí)行RunQueue中存放的Message。
RunQueue.executeActions是在ViewRootImpl.performTraversal當中進行調(diào)用;
RunQueue.executeActions是在執(zhí)行完host.dispatchAttachedToWindow(mAttachInfo, 0);之后調(diào)用;
RunQueue.executeActions是每次執(zhí)行ViewRootImpl.performTraversal都會進行調(diào)用;
RunQueue.executeActions的參數(shù)是mAttachInfo中的Handler也就是ViewRootHandler;
從這里看也是沒有任何問題的,我們使用View#post的消息都會在View被Attached的時候進行執(zhí)行;
一般程序在開發(fā)的過程中,如果涉及容器的使用那么必然需要考慮的生產(chǎn)和消費兩個情況。
上面的源碼我們是看了到了消息被執(zhí)行的邏輯(最終所有的消息都會被放在MainLooper中被消費),如果涉及消息被移除呢?
public class HandlerActionQueue {
public void removeCallbacks(Runnable action) {
synchronized (this) {
final int count = mCount;
int j = 0;
final HandlerAction[] actions = mActions;
for (int i = 0; i < count; i++) {
if (actions[i].matches(action)) {
// Remove this action by overwriting it within
// this loop or nulling it out later.
continue;
}
if (j != i) {
// At least one previous entry was removed, so
// this one needs to move to the "new" list.
actions[j] = actions[i];
}
j++;
}
// The "new" list only has j entries.
mCount = j;
// Null out any remaining entries.
for (; j < count; j++) {
actions[j] = null;
}
}
}
}
移除消息的時候如果當前View的mAttahInfo為空,那么我們只會移除RunQuque中換緩存的消息。。。
哦哦
原來是這樣啊~!
確實只能這樣~!
總結(jié)一下,如果View#mAttachInfo不為空那么你好,我好,大家好。否則View#post的消息會在緩存隊列中等待被添加,但移除的消息卻只能移除RunQueue中緩存的消息。如果此時RunQueue中的消息已經(jīng)被同步到MainLooper中那么,抱歉沒有View#mAttachInfo臣妾移除不了呀。
按照之前的業(yè)務(wù)代碼,如果當前
View被dispatchDetachedFromWindow之后執(zhí)行消息的移除操作,那么已經(jīng)在MainLooper隊列中的消息是無法被移除且如果繼續(xù)添加輪播消息,那么就會造成輪播代碼塊的頻繁執(zhí)行。
文字描述可能一時間不太容易理解,下面是一次超預期之外的輪播(為什么會有多個輪播消息)流程簡單的分析圖:

再說post和postDelayed
如果只看相關(guān)源碼我感覺是發(fā)現(xiàn)不了問題了,因為post最后執(zhí)行的也是postDelayed方法。所以兩者相比只不過時間差而已,這個時間差能造成什么影響呢?
回頭看了看自己之前寫的文章又一年對Android消息機制(Handler&Looper)的思考,其中有一個名詞叫做同步屏障。
同步屏障:忽略所有的同步消息,返回異步消息。再換句話說,同步屏障為
Handler消息機制增加了一種簡單的優(yōu)先級機制,異步消息的優(yōu)先級要高于同步消息。
而同步屏障用的最多的就是頁面的刷新(ViewRootImpl#mTraversalRunnable)相關(guān)文章可以閱讀Android系統(tǒng)的編舞者Choreographer,而ViewRootImpl的獨白,我不是一個View(布局篇)這篇文章講述了View#dispatchAttachedToWindow的方法就是由ViewRootImpl#performTraversals觸發(fā)的。
為什么要說同步屏障呢?上面的超預期輪播的流程圖中可以看出View#dispatchAttachedToWindow的方法調(diào)用對于整個流程非常重要。移除和添加兩個消息兩個如果由于postDelayed導致中間有其他消息的插入,而同步屏障是最有可能被插入的消息且這條消息會使View#mAttachInfo產(chǎn)生變化。
這就使原來有些小問題的代碼雪上加霜,bug更容易復現(xiàn)。
話說RecycleView
為什么要提到這個問題,因為好多時候我們使用View.post執(zhí)行任務(wù)是沒有問題(PS:我感覺這個觀點也是這個問題產(chǎn)生的最初的源頭)。
我們知道RecycleView的內(nèi)部子View僅僅是比屏幕大小多出一條預加載View,超過這個范圍或者進入這個范圍都會導致View被添加和移除。
public class RecyclerView extends ViewGroup implements ScrollingView, NestedScrollingChild2 {
/***部分代碼省略***/
private void initChildrenHelper() {
this.mChildHelper = new ChildHelper(new Callback() {
public int getChildCount() {
return RecyclerView.this.getChildCount();
}
public void addView(View child, int index) {
RecyclerView.this.addView(child, index);
RecyclerView.this.dispatchChildAttached(child);
}
public int indexOfChild(View view) {
return RecyclerView.this.indexOfChild(view);
}
public void removeViewAt(int index) {
View child = RecyclerView.this.getChildAt(index);
if (child != null) {
RecyclerView.this.dispatchChildDetached(child);
child.clearAnimation();
}
RecyclerView.this.removeViewAt(index);
}
}
/***部分代碼省略***/
}
/***部分代碼省略***/
}

如果我們頻繁來回滑動列表,那么這個Banner會不斷的被執(zhí)行dispatchAttachedToWindow和dispatchDetachedToWindow。
這樣導致View#mAttachInfo大部分時間為null,從而影響到業(yè)務(wù)代碼中往主線程中發(fā)送的Message的執(zhí)行邏輯。
文章到這里就講述的差不多了,解決這個問題給我?guī)淼母惺芡ι羁痰?,之前學習Android系統(tǒng)的相關(guān)源碼只不過是大家都在學、面試都在問。
能在應(yīng)用到實際研發(fā)過程中涉及到的知識點還是比較少,好多情況下都是能解決問題就行,也就是知其然而不知其所以然。
這次解決的問題能讓我深切感受到fuck the source code is beatifully。
文章到這里就全部講述完啦,若有其他需要交流的可以留言哦~!
2023年祝你在新一年心情日新月異,快樂如糖似蜜,朋友重情重義,愛人不離不棄,工作頻傳佳績,萬事稱心如意!