bug场景:
2017年11月初的某个下午,大家都很开心的在测试自己写的代码,因为明天就要提测了,突然组内有一位同事说:不会吧,现在出现这样的bug,他说RecyclerView里面包裹的HeaderView点击没有反应,点击事件没反应!!!不会吧,刚开始写android第一行代码就已经会写的点击事件不响应了;虽然我们很快的规避了这个bug,但是规避毕竟只是规避,并没有找出问题的根本,但是作为程序员怎么能知其然,而不知其所以然呢?闲暇之日,我开启了寻找bug之路,为了复现bug,我模拟了代码场景:我使用的RecyclerView是recyclerview-v7:25.2.0。
代码如下:
1 | Activity如下: |
以上代码就是我们常规使用RecyclerView的方式,可是当网络数据返回,页面刷新后却发现:头布局的点击事件无法响应,但是只要稍微滑动一下RecyclerView,之前所有的onClick事件都会响应。
针对bug现象,提出疑问:
onClick事件不能被响应,是否被拦截?不,肯定不是,因为在某个时刻,之前的onClick事件一次性释放出来。所以,事件没有并被消费,而是被保存起来了,那么:
- 1.onClick事件的触发及响应流程是什么呢?
- 2.在什么场景下,onClick才会被保存起来?
- 3.onClick事件是因为什么原因而被保存起来?
只要解决上面三个疑问,答案自然揭晓,带着这些疑问,开始寻找:
为了清楚onClick事件的触发及响应流程,我们需要对android sdk 源码进行debug调试,以往的经验都是对自己写的代码进行debug调试,还从没对sdk源码debug调试,带着好奇心使用手机去debug源码,第一个拦路虎出现了:源码错行,因为手机的Android版本和编译的sdk版本不同,解决这个问题有俩种方案:
将手机刷成和编译sdk相同的版本,然后进行调试。
使用同版本的模拟器调试。
为了快捷,我使用了模拟器方式,android studio自带的模拟器有点卡,于是下载了一个genymotion。自此,sdk源码的调试便可方便自如,调试环境如下:
Android SDK 25 Android模拟7.1
在View源码中的dispatchTouchEvent可以发现onTouch是被最先回调的,然后会在onTouchEvent的MotionEvent.ACTION_UP事件中post一条消息,代码如下:
1 | public boolean post(Runnable action) { |
从上面代码可以很直观看出:如果attachInfo != null,会用handler向主线程的looper发一条message,经测试得知正常的一次onClick事件的触发及响应流程如下图:

根据上图流程,调试RecyclerView头布局的onClick事件时发现:attachInfo == null,所以造成点击事件没有被及时回调,而是调用getRunQueue().post(action),将action缓存HandlerActionQueue里面,而在dispatchAttachedToWindow时会调用mRunQueue.executeActions;也就是说:之所以出现点击事件无法响/应,是因为attachInfo == null,而造成点击事件缓存是因为getRunQueue().post(action),至于为什么会在某个时刻一次性释放onClick,某个时刻指的是:dispatchAttachedToWindow时,一次性释放是因为调用mRunQueue.executeActions。
截止目前为止,上面提到的三个疑问,解决了俩个,还剩最后一个没有解决;而最后一个也是最重要的一个:onClick事件是因为什么原因而被保存起来?是因为attachInfo = null被保存起来,那么View的attachInfo为什么会为null?翻开View的源码:attachInfo是在dispatchAttachedToWindow时赋值的,只有在dispatchDetachedFromWindow方法内才会将attachInfo = null,而View的dispatchDetachedFromWindow会在ViewGourp调用,可是在ViewGroup中多处都会调用View的dispatchDetachedFromWindow,为了更准确的寻找调用地方,只能在每一个可能发生的位置break point,经过调试得知是在ViewGroup的finishAnimatingView中调用View的dispatchDetachedFromWindow,只不过在ViewGroup的finishAnimatingView中调用VIew的dispatchDetachedFromWindow是有条件的:mDisappearingChildren != null。
####接下来我们寻找mDisappearingChildren是在什么时候被赋值的?
上面当网络数据返回,调用Adapter的notityDataSetChanged时,会调用mObservable.notifyChanged(),接着调用mObservers.get(i).onChanged(),接着调用requestLayout:requestLayout是我们非常熟悉的方法,它的执行流程如下图:

当调用RecyclerView的onMeasure时,会调用RecyclerView的dispatchLayoutStep2,接着调用mLayout.onLayoutChildren,而我使用的LinearLayoutManager,即调用LinearLayoutManager的onLayoutChildren,接着调用RecyclerView的detachAndScrapAttachedViews,接着调用RecyclerView的scrapOrRecycleView,接着调用RecyclerView的removeViewAt,接着调用mChildHelper.removeViewAt,然后调用RecyclerView.this.removeViewAt,而RecyclerView extends ViewGourp 即调用ViewGroup的removeViewInternal,此时view.getAnimation() != null,也就是动画还未执行完,会执行ViewGroup的addDisappearingView,在addDisappearingView方法内会为mDisappearingChildren赋值,到此为止,整个流程大概跑通了,通俗将就是:
当网络请求回来之后,刷新RecyclerView时,因为头布局item已经存在,并且正在执行动画,所以导致mDisappearingChildren != null,当动画执行完成之后,却因为mDisappearingChildren != null ,而执行view.dispatchDetachedFromWindow()从而将mAttachInfo置为null。
以上就是为什么点击RecyclerView的头布局item无法响应点击事件的全部流程分析。
既然原因都已经如此透彻了,要想解决这个bug,自然轻而易举:比如在网络返回需要刷新RecyclerView时,可以清除RecyclerView内Item的Animation;至此,bug已经被正面解决了。
可是我还是有一个疑问:
我始终觉得这属于RecyclerView的bug,毕竟大多数Android Developer并不清楚以上整个流程,也不能轻而易举的在众多业务代码中找到:Item点击事件无法响应,是因为RecyclerView里面Item的Animation还未执行完时,我们主动调用Adapter的notityDataSetChanged去刷新RecyclerView导致的,带着这个疑问我继续寻找:终于,皇天不负有人心,最终发现:
Google Android官方在recyclerview-v7:25.4.0上修复了这个bug(其实25.4.0发布也没多久),而我使用的还是recyclerview-v7:25.2.0,Google修复的办法其实我们的方法一模一样,它是在:mChildHelper的removeViewAt时调用了child.clearAnimation(),也就是说:当RecyclerView重新刷新的时候,如果包裹的View有Animation,它会清除此Animation。正如注释:
Clear any android.view.animation.Animation that may prevent the item from detaching when being removed. If a child is re-added before the lazy detach occurs, it will receive invalid attach/detach sequencing.
这样的话:因为View的Animation被清除了,当再次调用draw方法绘制当前View的时候,便不会调用parent.finishAnimatingView(this, a),也就不存在使得mAttachInfo置为null的情况了。
经此一战之后,得出一个重要结论:
Android sdk太过庞大,虽然被很多人使用,但也难免会因为使用不当出现bug,遇到问题,要敢于质疑,小心论证;还有就是一定要及时更新Android最新发布的sdk版本,时刻紧跟google官方节奏。
后续:
关于上面view.getAnimation() != nul分析:
onCreat执行setContentView之后然后初始化xml,在ViewGroup的构造方法中调用initFromAttributes,如果发现ViewGroup中有layoutAnimation属性,会在ViewGroup中创建LayoutAnimationController对象;
onResume执行时依次会遍历执行child view的 layout,measure,draw(Activity中有ActivityThread,ActivityThread中的handleResumeActivity调用WindowManagerGlobal的addView,WindowManagerGlobal中的addView调用ViewRootImpl的setView方法,而setView中调用了requestLayout,requestLayout中调用了scheduleTraversals方法,scheduleTraversals中使用Choreographer发送一个message,最后会回调doTraversal,执行performTraversals。然后进行measure,layout,draw)
View的draw方法中会调用dispatchDraw,而View中的dispatchDraw是一个空实现,具体的实现在ViewGroup当中,当dispatchDraw执行时,如果mLayoutAnimationController != null,会遍历子View执行bindLayoutAnimation,在bindLayoutAnimation中调用View的setAnimation方法。
通俗讲就是:如果你在xml为某个View或者ViewGroup设置layoutAnimation属性,当它绘制的时候便会调用View的setAnimation方法,或者遍历里面所有的View调用它的setAnimation方法。
