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

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

2023新年快樂(lè)

前言

本篇文章是在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ā)送;

banner_carousel.png

代碼貌似沒(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)行removeCallBackspostDelayed汉额,完美的解決了問(wèn)題。

下面記錄一下整問(wèn)題解決過(guò)程中的思考~榨汤!

待解決問(wèn)題

  1. View.removeCallbacks 是否真的可靠蠕搜;
  2. View.postView.postDelayed相比為什么bug復(fù)現(xiàn)頻率更低;

View#dispatchAttachedToWindow

HandleremoveCallBacks移除方法是不可靠的么收壕?如果當(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一次话侧。

我們打印stopTimerstartTimer方法執(zhí)行的時(shí)ViewPager#getHandlerHandler實(shí)例,發(fā)現(xiàn)在列表快速滑動(dòng)時(shí)大部分為null闯参。

好吧,之前忽略了這個(gè)Banner在滑動(dòng)過(guò)程中的被View#dispatchDetachedFromWindow悲立。這個(gè)方法的調(diào)用會(huì)導(dǎo)致View內(nèi)部的Handlenull鹿寨。

如果ViewHandlenull,那么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;
}

postpostDelayedView的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ì)在ViewAttached的時(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)前ViewmAttahInfo為空,那么我們只會(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)前ViewdispatchDetachedFromWindow之后執(zhí)行消息的移除操作披粟,那么已經(jīng)在MainLooper隊(duì)列中的消息是無(wú)法被移除且如果繼續(xù)添加輪播消息,那么就會(huì)造成輪播代碼塊的頻繁執(zhí)行冷冗。

文字描述可能一時(shí)間不太容易理解守屉,下面是一次超預(yù)期之外的輪播(為什么會(huì)有多個(gè)輪播消息)流程簡(jiǎn)單的分析圖:

view-post-runqueue.png

再說(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);
            }
        }
        /***部分代碼省略***/
    }
    /***部分代碼省略***/
}
view_add_remove.png

如果我們頻繁來(lái)回滑動(dòng)列表,那么這個(gè)Banner會(huì)不斷的被執(zhí)行dispatchAttachedToWindowdispatchDetachedToWindow屿聋。
這樣導(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)心如意!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市培慌,隨后出現(xiàn)的幾起案子岔留,更是在濱河造成了極大的恐慌,老刑警劉巖检柬,帶你破解...
    沈念sama閱讀 216,591評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異竖配,居然都是意外死亡何址,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,448評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門(mén)进胯,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)用爪,“玉大人,你說(shuō)我怎么就攤上這事胁镐≠搜” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 162,823評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵盯漂,是天一觀的道長(zhǎng)颇玷。 經(jīng)常有香客問(wèn)我,道長(zhǎng)就缆,這世上最難降的妖魔是什么帖渠? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,204評(píng)論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮竭宰,結(jié)果婚禮上空郊,老公的妹妹穿的比我還像新娘。我一直安慰自己切揭,他們只是感情好狞甚,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,228評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著廓旬,像睡著了一般哼审。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,190評(píng)論 1 299
  • 那天棺蛛,我揣著相機(jī)與錄音怔蚌,去河邊找鬼。 笑死旁赊,一個(gè)胖子當(dāng)著我的面吹牛桦踊,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播终畅,決...
    沈念sama閱讀 40,078評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼籍胯,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了离福?” 一聲冷哼從身側(cè)響起杖狼,我...
    開(kāi)封第一講書(shū)人閱讀 38,923評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎妖爷,沒(méi)想到半個(gè)月后蝶涩,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,334評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡絮识,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,550評(píng)論 2 333
  • 正文 我和宋清朗相戀三年绿聘,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片次舌。...
    茶點(diǎn)故事閱讀 39,727評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡熄攘,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出彼念,到底是詐尸還是另有隱情挪圾,我是刑警寧澤,帶...
    沈念sama閱讀 35,428評(píng)論 5 343
  • 正文 年R本政府宣布逐沙,位于F島的核電站哲思,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏酱吝。R本人自食惡果不足惜也殖,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,022評(píng)論 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望务热。 院中可真熱鬧忆嗜,春花似錦、人聲如沸崎岂。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,672評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)冲甘。三九已至绩卤,卻和暖如春途样,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背濒憋。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,826評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工何暇, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人凛驮。 一個(gè)月前我還...
    沈念sama閱讀 47,734評(píng)論 2 368
  • 正文 我出身青樓裆站,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親黔夭。 傳聞我的和親對(duì)象是個(gè)殘疾皇子宏胯,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,619評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容