前语
本篇文章是在View的postDelayed办法深度考虑这篇文章的所有的根底理论上进行研究的,可以说是对于View的postDelayed办法深度考虑这篇文章知识点的实践。
某天同事某进在做一个列表页增加轮播Banner
的需求的时分,发下偶尔会呈现轮播距离时刻紊乱的问题。
我看了他的轮播的完成方案:利用Handle.postDelayed
距离轮播时长每次履行完轮播之后再次循环发送;
代码貌似没有太大问题,但经过现象看来应该是removeCallbacks
失效了~!
Handle#removeCallbacks
在stackoverflow
上找了相关材料Why to use removeCallbacks() with postDelayed()?,之后测验将postDelayed
不靠谱那么改为post
,发现貌似轮播距离时刻紊乱的问题处理了~!
虽然不清楚什么原因导致问题不再呈现,但后续由于其他工作打断未能持续排查下去。
若干天之后,再次发现轮播距离时刻紊乱的问题有一次呈现了。
这次咱们使用自定
Handler
进行removeCallBacks
和postDelayed
,完美的处理了问题。
下面记载一下整问题处理过程中的考虑~!
待处理问题
-
View.removeCallbacks
是否真的牢靠; -
View.post
和View.postDelayed
相比为什么bug复现频率更低;
View#dispatchAttachedToWindow
Handle
的removeCallBacks
移除办法是不牢靠的么?假如当时的使命不是在履行中,那么该使命一定会被移除。
换句话说,Handle#removeCallBacks
移除的便是在行列中等待被履行的Message
。
那么问题究竟出在哪里,并且为什么
postDelayed
替换为post
问题的复现概率降低了?
这次有些时刻,跟了一下源码发现使用View#postDelayed
发送的音讯不一定会立即被放在音讯行列。
回忆之前View的postDelayed办法深度考虑这篇文章中关于View.postDelayed小结
中的描绘:
postDelayed
办法调用的时分,假如当时的View
没有依附在Window
上的时分,先将Runnable
缓存在RunQueue
行列中。比及View.dispatchAttachedToWindow
调用之后,再被ViewRootHandler
进行一次postDelayed
。这个过程中相同的Runnable
只会被postDelay
一次。
咱们打印stopTimer
和startTimer
办法履行的时ViewPager#getHandler
的Handler
实例,发现在列表快速滑动时大部分为null
。
好吧,之前疏忽了这个Banner
在滑动过程中的被View#dispatchDetachedFromWindow
。这个办法的调用会导致View
内部的Handle
为null
。
假如View
的Handle
为null
,那么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;
}
post
和postDelayed
在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
的音讯都会在View
被Attached
的时分进行履行;
一般程序在开发的过程中,假如触及容器的使用那么必然需求考虑的生产和消费两个状况。
上面的源码咱们是看了到了音讯被履行的逻辑(终究所有的音讯都会被放在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
中换缓存的音讯。。。
哦哦 原来是这样啊~! 的确只能这样~!
总结一下,假如View#mAttachInfo
不为空那么你好,我好,我们好。否则View#post
的音讯会在缓存行列中等待被增加,但移除的音讯却只能移除RunQueue
中缓存的音讯。假如此时RunQueue
中的音讯已经被同步到MainLooper
中那么,抱歉没有View#mAttachInfo
臣妾移除不了呀。
依照之前的事务代码,假如当时
View
被dispatchDetachedFromWindow
之后履行音讯的移除操作,那么已经在MainLooper
行列中的音讯是无法被移除且假如持续增加轮播音讯,那么就会形成轮播代码块的频频履行。
文字描绘或许一时刻不太简单理解,下面是一次超预期之外的轮播(为什么会有多个轮播音讯)流程简单的剖析图:
再说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);
}
}
/***部分代码省掉***/
}
/***部分代码省掉***/
}
假如咱们频频来回滑动列表,那么这个Banner
会不断的被履行dispatchAttachedToWindow
和dispatchDetachedToWindow
。
这样导致View#mAttachInfo
大部分时刻为null
,从而影响到事务代码中往主线程中发送的Message
的履行逻辑。
文章到这儿就叙述的差不多了,处理这个问题给我带来的感触挺深刻的,之前学习Android体系的相关源码只不过是我们都在学、面试都在问。
能在应用到实际研制过程中触及到的知识点还是比较少,好多状况下都是能处理问题就行,也便是知其然而不知其所以然。
这次处理的问题能让我深切感触到fuck the source code is beatifully
。
文章到这儿就全部叙述完啦,若有其他需求沟通的可以留言哦~!
2023年祝你在新一年心境一日千里,高兴如糖似蜜,朋友重情重义,爱人不离不弃,工作频传佳绩,万事左右逢源!