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

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

2023新年快樂

前言

本篇文章是在View的postDelayed方法深度思考這篇文章的所有的基礎(chǔ)理論上進行研究的,可以說是對于View的postDelayed方法深度思考這篇文章知識點的實踐。

某天同事某進在做一個列表頁添加輪播Banner的需求的時候,發(fā)下偶爾會出現(xiàn)輪播間隔時間錯亂的問題。

我看了他的輪播的實現(xiàn)方案:利用Handle.postDelayed間隔輪播時長每次執(zhí)行完輪播之后再次循環(huán)發(fā)送;

banner_carousel.png

代碼貌似沒有太大問題,但通過現(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進行removeCallBackspostDelayed,完美的解決了問題。

下面記錄一下整問題解決過程中的思考~!

待解決問題

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

View#dispatchAttachedToWindow

HandleremoveCallBacks移除方法是不可靠的么?如果當前的任務(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一次。

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

好吧,之前忽略了這個Banner在滑動過程中的被View#dispatchDetachedFromWindow。這個方法的調(diào)用會導致View內(nèi)部的Handlenull

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

postpostDelayedView的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的消息都會在ViewAttached的時候進行執(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;
            }
        }
    }
}

移除消息的時候如果當前ViewmAttahInfo為空,那么我們只會移除RunQuque中換緩存的消息。。。

哦哦
原來是這樣啊~!
確實只能這樣~!

總結(jié)一下,如果View#mAttachInfo不為空那么你好,我好,大家好。否則View#post的消息會在緩存隊列中等待被添加,但移除的消息卻只能移除RunQueue中緩存的消息。如果此時RunQueue中的消息已經(jīng)被同步到MainLooper中那么,抱歉沒有View#mAttachInfo臣妾移除不了呀。

按照之前的業(yè)務(wù)代碼,如果當前ViewdispatchDetachedFromWindow之后執(zhí)行消息的移除操作,那么已經(jīng)在MainLooper隊列中的消息是無法被移除且如果繼續(xù)添加輪播消息,那么就會造成輪播代碼塊的頻繁執(zhí)行。

文字描述可能一時間不太容易理解,下面是一次超預期之外的輪播(為什么會有多個輪播消息)流程簡單的分析圖:

view-post-runqueue.png

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

如果我們頻繁來回滑動列表,那么這個Banner會不斷的被執(zhí)行dispatchAttachedToWindowdispatchDetachedToWindow。
這樣導致View#mAttachInfo大部分時間為null,從而影響到業(yè)務(wù)代碼中往主線程中發(fā)送的Message的執(zhí)行邏輯。

文章到這里就講述的差不多了,解決這個問題給我?guī)淼母惺芡ι羁痰?,之前學習Android系統(tǒng)的相關(guān)源碼只不過是大家都在學、面試都在問。
能在應(yīng)用到實際研發(fā)過程中涉及到的知識點還是比較少,好多情況下都是能解決問題就行,也就是知其然而不知其所以然。
這次解決的問題能讓我深切感受到fuck the source code is beatifully。

文章到這里就全部講述完啦,若有其他需要交流的可以留言哦~!

2023年祝你在新一年心情日新月異,快樂如糖似蜜,朋友重情重義,愛人不離不棄,工作頻傳佳績,萬事稱心如意!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容