由View的onAttachedToWindow引發(fā)的圖片輪播問(wèn)題探究
前言
本篇文章是在View的postDelayed方法深度思考這篇文章的所有的基礎(chǔ)理論上進(jìn)行研究的益涧,可以說(shuō)是對(duì)于View的postDelayed方法深度思考這篇文章知識(shí)點(diǎn)的實(shí)踐搔确。
某天同事某進(jìn)在做一個(gè)列表頁(yè)添加輪播Banner
的需求的時(shí)候摇天,發(fā)下偶爾會(huì)出現(xiàn)輪播間隔時(shí)間錯(cuò)亂的問(wèn)題。
我看了他的輪播的實(shí)現(xiàn)方案:利用Handle.postDelayed
間隔輪播時(shí)長(zhǎng)每次執(zhí)行完輪播之后再次循環(huán)發(fā)送;
代碼貌似沒(méi)有太大問(wèn)題,但通過(guò)現(xiàn)象看來(lái)應(yīng)該是removeCallbacks
失效了~!
Handle#removeCallbacks
在stackoverflow
上找了相關(guān)資料Why to use removeCallbacks() with postDelayed()?,之后嘗試將postDelayed
不靠譜那么改為post
纵顾,發(fā)現(xiàn)貌似輪播間隔時(shí)間錯(cuò)亂的問(wèn)題解決了~!
雖然不清楚什么原因?qū)е聠?wèn)題不再出現(xiàn)栋盹,但后續(xù)因?yàn)槠渌ぷ鞔驍辔茨芾^續(xù)排查下去施逾。
若干天之后,再次發(fā)現(xiàn)輪播間隔時(shí)間錯(cuò)亂的問(wèn)題有一次出現(xiàn)了例获。
這次我們使用自定
Handler
進(jìn)行removeCallBacks
和postDelayed
汉额,完美的解決了問(wèn)題。
下面記錄一下整問(wèn)題解決過(guò)程中的思考~榨汤!
待解決問(wèn)題
-
View.removeCallbacks
是否真的可靠蠕搜; -
View.post
和View.postDelayed
相比為什么bug復(fù)現(xiàn)頻率更低;
View#dispatchAttachedToWindow
Handle
的removeCallBacks
移除方法是不可靠的么收壕?如果當(dāng)前的任務(wù)不是在執(zhí)行中妓灌,那么該任務(wù)一定會(huì)被移除。
換句話說(shuō)蜜宪,Handle#removeCallBacks
移除的就是在隊(duì)列中等待被執(zhí)行的Message
虫埂。
那么問(wèn)題到底出在哪里,而且為什么
postDelayed
替換為post
問(wèn)題的復(fù)現(xiàn)概率降低了圃验?
這次有些時(shí)間掉伏,跟了一下源碼發(fā)現(xiàn)使用View#postDelayed
發(fā)送的消息不一定會(huì)立即被放在消息隊(duì)列。
回顧之前View的postDelayed方法深度思考這篇文章中關(guān)于View.postDelayed小結(jié)
中的描述:
postDelayed
方法調(diào)用的時(shí)候,如果當(dāng)前的View
沒(méi)有依附在Window
上的時(shí)候斧散,先將Runnable
緩存在RunQueue
隊(duì)列中供常。等到View.dispatchAttachedToWindow
調(diào)用之后,再被ViewRootHandler
進(jìn)行一次postDelayed
颅湘。這個(gè)過(guò)程中相同的Runnable
只會(huì)被postDelay
一次话侧。
我們打印stopTimer
和startTimer
方法執(zhí)行的時(shí)ViewPager#getHandler
的Handler
實(shí)例,發(fā)現(xiàn)在列表快速滑動(dòng)時(shí)大部分為null
闯参。
好吧,之前忽略了這個(gè)Banner
在滑動(dòng)過(guò)程中的被View#dispatchDetachedFromWindow
悲立。這個(gè)方法的調(diào)用會(huì)導(dǎo)致View
內(nèi)部的Handle
為null
鹿寨。
如果View
的Handle
為null
,那么Message
的執(zhí)行可能會(huì)收到影響薪夕。
在View的postDelayed方法深度思考這篇文章中關(guān)于mAttachInfo
對(duì)于View.postDelayed
的影響嫂丙,也都進(jìn)行了分析梧却。這里我們撿主要的源碼閱讀一下。
//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方法深度思考這篇文章中進(jìn)行過(guò)講解,會(huì)在View
執(zhí)行dispatchAttachedToWindow
方法的時(shí)候執(zhí)行RunQueue
中存放的Message
泛豪。
RunQueue.executeActions
是在ViewRootImpl.performTraversal
當(dāng)中進(jìn)行調(diào)用;
RunQueue.executeActions
是在執(zhí)行完host.dispatchAttachedToWindow(mAttachInfo, 0);
之后調(diào)用莱坎;
RunQueue.executeActions
是每次執(zhí)行ViewRootImpl.performTraversal
都會(huì)進(jìn)行調(diào)用玫氢;
RunQueue.executeActions
的參數(shù)是mAttachInfo
中的Handler
也就是ViewRootHandler
;
從這里看也是沒(méi)有任何問(wèn)題的,我們使用View#post
的消息都會(huì)在View
被Attached
的時(shí)候進(jìn)行執(zhí)行讲仰;
一般程序在開(kāi)發(fā)的過(guò)程中慕趴,如果涉及容器的使用那么必然需要考慮的生產(chǎn)和消費(fèi)兩個(gè)情況。
上面的源碼我們是看了到了消息被執(zhí)行的邏輯(最終所有的消息都會(huì)被放在MainLooper
中被消費(fèi))鄙陡,如果涉及消息被移除呢冕房?
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;
}
}
}
}
移除消息的時(shí)候如果當(dāng)前View
的mAttahInfo
為空,那么我們只會(huì)移除RunQuque
中換緩存的消息趁矾。耙册。。
哦哦
原來(lái)是這樣啊~毫捣!
確實(shí)只能這樣~详拙!
總結(jié)一下,如果View#mAttachInfo
不為空那么你好培漏,我好溪厘,大家好。否則View#post
的消息會(huì)在緩存隊(duì)列中等待被添加牌柄,但移除的消息卻只能移除RunQueue
中緩存的消息畸悬。如果此時(shí)RunQueue
中的消息已經(jīng)被同步到MainLooper
中那么,抱歉沒(méi)有View#mAttachInfo
臣妾移除不了呀。
按照之前的業(yè)務(wù)代碼蹋宦,如果當(dāng)前
View
被dispatchDetachedFromWindow
之后執(zhí)行消息的移除操作披粟,那么已經(jīng)在MainLooper
隊(duì)列中的消息是無(wú)法被移除且如果繼續(xù)添加輪播消息,那么就會(huì)造成輪播代碼塊的頻繁執(zhí)行冷冗。
文字描述可能一時(shí)間不太容易理解守屉,下面是一次超預(yù)期之外的輪播(為什么會(huì)有多個(gè)輪播消息)流程簡(jiǎn)單的分析圖:
再說(shuō)post和postDelayed
如果只看相關(guān)源碼我感覺(jué)是發(fā)現(xiàn)不了問(wèn)題了,因?yàn)?code>post最后執(zhí)行的也是postDelayed
方法蒿辙。所以兩者相比只不過(guò)時(shí)間差而已拇泛,這個(gè)時(shí)間差能造成什么影響呢?
回頭看了看自己之前寫(xiě)的文章又一年對(duì)Android消息機(jī)制(Handler&Looper)的思考思灌,其中有一個(gè)名詞叫做同步屏障俺叭。
同步屏障:忽略所有的同步消息,返回異步消息泰偿。再換句話說(shuō)熄守,同步屏障為
Handler
消息機(jī)制增加了一種簡(jiǎn)單的優(yōu)先級(jí)機(jī)制,異步消息的優(yōu)先級(jí)要高于同步消息耗跛。
而同步屏障用的最多的就是頁(yè)面的刷新(ViewRootImpl#mTraversalRunnable
)相關(guān)文章可以閱讀Android系統(tǒng)的編舞者Choreographer裕照,而ViewRootImpl的獨(dú)白,我不是一個(gè)View(布局篇)這篇文章講述了View#dispatchAttachedToWindow
的方法就是由ViewRootImpl#performTraversals
觸發(fā)的调塌。
為什么要說(shuō)同步屏障呢晋南?上面的超預(yù)期輪播的流程圖中可以看出View#dispatchAttachedToWindow
的方法調(diào)用對(duì)于整個(gè)流程非常重要。移除
和添加
兩個(gè)消息兩個(gè)如果由于postDelayed
導(dǎo)致中間有其他消息的插入烟阐,而同步屏障是最有可能被插入的消息且這條消息會(huì)使View#mAttachInfo
產(chǎn)生變化搬俊。
這就使原來(lái)有些小問(wèn)題的代碼雪上加霜,bug更容易復(fù)現(xiàn)蜒茄。
話說(shuō)RecycleView
為什么要提到這個(gè)問(wèn)題唉擂,因?yàn)楹枚鄷r(shí)候我們使用View.post
執(zhí)行任務(wù)是沒(méi)有問(wèn)題(PS:我感覺(jué)這個(gè)觀點(diǎn)也是這個(gè)問(wèn)題產(chǎn)生的最初的源頭)。
我們知道RecycleView
的內(nèi)部子View
僅僅是比屏幕大小多出一條預(yù)加載View
檀葛,超過(guò)這個(gè)范圍或者進(jìn)入這個(gè)范圍都會(huì)導(dǎo)致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);
}
}
/***部分代碼省略***/
}
/***部分代碼省略***/
}
如果我們頻繁來(lái)回滑動(dòng)列表,那么這個(gè)Banner
會(huì)不斷的被執(zhí)行dispatchAttachedToWindow
和dispatchDetachedToWindow
屿聋。
這樣導(dǎo)致View#mAttachInfo
大部分時(shí)間為null
空扎,從而影響到業(yè)務(wù)代碼中往主線程中發(fā)送的Message
的執(zhí)行邏輯。
文章到這里就講述的差不多了润讥,解決這個(gè)問(wèn)題給我?guī)?lái)的感受挺深刻的转锈,之前學(xué)習(xí)Android系統(tǒng)的相關(guān)源碼只不過(guò)是大家都在學(xué)、面試都在問(wèn)楚殿。
能在應(yīng)用到實(shí)際研發(fā)過(guò)程中涉及到的知識(shí)點(diǎn)還是比較少撮慨,好多情況下都是能解決問(wèn)題就行,也就是知其然而不知其所以然。
這次解決的問(wèn)題能讓我深切感受到fuck the source code is beatifully
砌溺。
文章到這里就全部講述完啦影涉,若有其他需要交流的可以留言哦~!
2023年祝你在新一年心情日新月異规伐,快樂(lè)如糖似蜜蟹倾,朋友重情重義,愛(ài)人不離不棄猖闪,工作頻傳佳績(jī)鲜棠,萬(wàn)事稱(chēng)心如意!