前语

本篇文章是在View的postDelayed办法深度考虑这篇文章的所有的根底理论上进行研究的,可以说是对于View的postDelayed办法深度考虑这篇文章知识点的实践。

某天同事某进在做一个列表页增加轮播Banner的需求的时分,发下偶尔会呈现轮播距离时刻紊乱的问题。

我看了他的轮播的完成方案:利用Handle.postDelayed距离轮播时长每次履行完轮播之后再次循环发送;

由View的onAttachedToWindow引发的图片轮播问题探究

代码貌似没有太大问题,但经过现象看来应该是removeCallbacks失效了~!

Handle#removeCallbacks

stackoverflow上找了相关材料Why to use removeCallbacks() with postDelayed()?,之后测验将postDelayed不靠谱那么改为post,发现貌似轮播距离时刻紊乱的问题处理了~!

虽然不清楚什么原因导致问题不再呈现,但后续由于其他工作打断未能持续排查下去。

若干天之后,再次发现轮播距离时刻紊乱的问题有一次呈现了。

这次咱们使用自定Handler进行removeCallBackspostDelayed,完美的处理了问题。

下面记载一下整问题处理过程中的考虑~!

待处理问题

  1. View.removeCallbacks 是否真的牢靠;
  2. View.postView.postDelayed相比为什么bug复现频率更低;

View#dispatchAttachedToWindow

HandleremoveCallBacks移除办法是不牢靠的么?假如当时的使命不是在履行中,那么该使命一定会被移除。 换句话说,Handle#removeCallBacks移除的便是在行列中等待被履行的Message

那么问题究竟出在哪里,并且为什么postDelayed替换为post问题的复现概率降低了?

这次有些时刻,跟了一下源码发现使用View#postDelayed发送的音讯不一定会立即被放在音讯行列。

回忆之前View的postDelayed办法深度考虑这篇文章中关于View.postDelayed小结中的描绘:

postDelayed办法调用的时分,假如当时的View没有依附在Window上的时分,先将Runnable缓存在RunQueue行列中。比及View.dispatchAttachedToWindow调用之后,再被ViewRootHandler进行一次postDelayed。这个过程中相同的Runnable只会被postDelay一次。

咱们打印stopTimerstartTimer办法履行的时ViewPager#getHandlerHandler实例,发现在列表快速滑动时大部分为null

好吧,之前疏忽了这个Banner在滑动过程中的被View#dispatchDetachedFromWindow。这个办法的调用会导致View内部的Handlenull

假如ViewHandlenull,那么Message的履行或许会收到影响。

在View的postDelayed办法深度考虑这篇文章中关于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;
}

postpostDelayed在View的postDelayed办法深度考虑这篇文章中进行过讲解,会在View履行dispatchAttachedToWindow办法的时分履行RunQueue中寄存的Message

RunQueue.executeActions是在ViewRootImpl.performTraversal当中进行调用;

RunQueue.executeActions是在履行完host.dispatchAttachedToWindow(mAttachInfo, 0);之后调用;

RunQueue.executeActions是每次履行ViewRootImpl.performTraversal都会进行调用;

RunQueue.executeActions的参数是mAttachInfo中的Handler也便是ViewRootHandler;

从这儿看也是没有任何问题的,咱们使用View#post的音讯都会在ViewAttached的时分进行履行;

一般程序在开发的过程中,假如触及容器的使用那么必然需求考虑的生产和消费两个状况。 上面的源码咱们是看了到了音讯被履行的逻辑(终究所有的音讯都会被放在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中换缓存的音讯。。。

哦哦 原来是这样啊~! 的确只能这样~!

总结一下,假如View#mAttachInfo不为空那么你好,我好,我们好。否则View#post的音讯会在缓存行列中等待被增加,但移除的音讯却只能移除RunQueue中缓存的音讯。假如此时RunQueue中的音讯已经被同步到MainLooper中那么,抱歉没有View#mAttachInfo臣妾移除不了呀。

依照之前的事务代码,假如当时ViewdispatchDetachedFromWindow之后履行音讯的移除操作,那么已经在MainLooper行列中的音讯是无法被移除且假如持续增加轮播音讯,那么就会形成轮播代码块的频频履行。

文字描绘或许一时刻不太简单理解,下面是一次超预期之外的轮播(为什么会有多个轮播音讯)流程简单的剖析图:

由View的onAttachedToWindow引发的图片轮播问题探究

再说post和postDelayed

假如只看相关源码我感觉是发现不了问题了,由于post最后履行的也是postDelayed办法。所以两者相比只不过时刻差而已,这个时刻差能形成什么影响呢? 回头看了看自己之前写的文章又一年对Android音讯机制(Handler&Looper)的考虑,其中有一个名词叫做同步屏障

同步屏障:疏忽所有的同步音讯,返回异步音讯。再换句话说,同步屏障为Handler音讯机制增加了一种简单的优先级机制,异步音讯的优先级要高于同步音讯。

同步屏障用的最多的便是页面的刷新(ViewRootImpl#mTraversalRunnable)相关文章可以阅览Android体系的编舞者Choreographer,而ViewRootImpl的独白,我不是一个View(布局篇)这篇文章叙述了View#dispatchAttachedToWindow的办法便是由ViewRootImpl#performTraversals触发的。

为什么要说同步屏障呢?上面的超预期轮播的流程图中可以看出View#dispatchAttachedToWindow的办法调用对于整个流程非常重要。移除增加两个音讯两个假如由于postDelayed导致中心有其他音讯的刺进,而同步屏障是最有或许被刺进的音讯且这条音讯会使View#mAttachInfo产生变化。 这就使原来有些小问题的代码落井下石,bug更简单复现。

话说RecycleView

为什么要提到这个问题,由于好多时分咱们使用View.post履行使命是没有问题(PS:我感觉这个观念也是这个问题产生的开始的源头)。

咱们知道RecycleView的内部子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的onAttachedToWindow引发的图片轮播问题探究

假如咱们频频来回滑动列表,那么这个Banner会不断的被履行dispatchAttachedToWindowdispatchDetachedToWindow。 这样导致View#mAttachInfo大部分时刻为null,从而影响到事务代码中往主线程中发送的Message的履行逻辑。

文章到这儿就叙述的差不多了,处理这个问题给我带来的感触挺深刻的,之前学习Android体系的相关源码只不过是我们都在学、面试都在问。 能在应用到实际研制过程中触及到的知识点还是比较少,好多状况下都是能处理问题就行,也便是知其然而不知其所以然。 这次处理的问题能让我深切感触到fuck the source code is beatifully

文章到这儿就全部叙述完啦,若有其他需求沟通的可以留言哦~!

2023年祝你在新一年心境一日千里,高兴如糖似蜜,朋友重情重义,爱人不离不弃,工作频传佳绩,万事左右逢源!