Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >View的onAttachedToWindow引发的图片轮播问题探究

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

作者头像
静默加载
发布于 2023-01-14 01:54:04
发布于 2023-01-14 01:54:04
59000
代码可运行
举报
运行总次数:0
代码可运行

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

2023新年快乐

前言

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

某天同事某进在做一个列表页添加轮播Banner的需求的时候,发下偶尔会出现轮播间隔时间错乱的问题。

我看了他的轮播的实现方案:利用Handle.postDelayed间隔轮播时长每次执行完轮播之后再次循环发送;

banner_carousel.png

代码貌似没有太大问题,但通过现象看来应该是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的影响,也都进行了分析。这里我们捡主要的源码阅读一下。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
//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执行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中被消费),如果涉及消息被移除呢?

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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-post-runqueue.png

再说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被添加和移除。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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会不断的被执行dispatchAttachedToWindowdispatchDetachedToWindow。 这样导致View#mAttachInfo大部分时间为null,从而影响到业务代码中往主线程中发送的Message的执行逻辑。

文章到这里就讲述的差不多了,解决这个问题给我带来的感受挺深刻的,之前学习Android系统的相关源码只不过是大家都在学、面试都在问。 能在应用到实际研发过程中涉及到的知识点还是比较少,好多情况下都是能解决问题就行,也就是知其然而不知其所以然。 这次解决的问题能让我深切感受到fuck the source code is beatifully

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

2023年祝你在新一年心情日新月异,快乐如糖似蜜,朋友重情重义,爱人不离不弃,工作频传佳绩,万事称心如意!

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2023-01-13,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
面试官:View.post() 为什么能够获取到 View 的宽高?
说一些题外话,Android 面试进阶指南 其实是我在小专栏维护的一个付费专栏,且已经有部分付费用户。本文是第九篇文章了,为了维护付费用户的权益,没有办法把所有文章都同步到公众号。如果你对这个专栏感兴趣,不妨点击文末 阅读原文 了解专栏详情。
路遥TM
2021/08/31
1.4K0
Android | View.post 到底是在什么时候执行的?
相信绝大部分人都使用过 view.post这个方法,且使用场景基本上都是用来获取 view 的一些属性数据,并且我们也都知道,该方法会使用 handler 发送一个消息,并且该消息被回调执行的时候 view 是已经绘制完成的,今天我们来聊一聊它内部的一些细节。
345
2023/05/02
6150
Carson带你学Android:为什么view.post()能保证获取到view的宽高?
为什么view.post()能保证获取到view的宽高?本文将手把手带你深入源码了解view.post() 原理。
Carson.Ho
2021/12/06
4310
Carson带你学Android:为什么view.post()能保证获取到view的宽高?
从 Android 开发到读懂源码 第03期:View.post 源码解析
这个方法在日常开发中是经常用到的,例如在子线程中我们需要更新 UI,可以通过 post 一个 runnable ,在 run 方法中去绘制 UI ,或者我们需要在 Activity 的 onCreate 中获取一个 View 的宽高时,也会通过 post 一个 runnable 并在 run 方法中获取这个 View 的 width 和 height 信息。本文基于 Android 9.0 的源码进行分析。
数据库交流
2022/04/25
3130
从 Android 开发到读懂源码 第03期:View.post 源码解析
View的postDelayed方法深度思考
突然某天好友老瑞问我 “View的postdelay方法,延迟时间如果设置10分钟或者更长的时间有什么问题吗?“ 。当时听到这个问题时候我只能联想到 Handle.postDelay ,与此同时让我回想起了之前的一些疑问?
静默加载
2020/05/29
2.2K0
面试官带你学Android——面试中Handler 这些必备知识点你都知道吗?
在Android面试中,关于 Handler 的问题是必备的,但是这些关于 Handler 的知识点你都知道吗?
Android技术干货分享
2020/10/12
7190
面试官带你学Android——面试中Handler 这些必备知识点你都知道吗?
View三问—斗鱼真题
Android作为一种前端语言,view肯定是占据着很重要的位置,熟用view也是我们必须具备的能力,今天就来看看view相关的那些问题:
码上积木
2020/10/29
6040
java中的onresume_android onCreate onResume中获取 View 宽高为0分析
xmlns:tools=”http://schemas.android.com/tools”
全栈程序员站长
2022/09/05
4170
Carson带你学Android:那些关于view.post() 的四大常见疑难杂症
View.post()的原理:以Handler为基础,View.post() 将传入任务添加到 View绘制任务所在的消息队列尾部,从而保证View.post() 任务的执行时机是在View 绘制任务完成之后的。 其中,几个关键点:
Carson.Ho
2021/12/06
5830
Carson带你学Android:那些关于view.post() 的四大常见疑难杂症
View.Post()保证UI带你装逼带你飞
因为当onCreate()方法被调用的时候会通过LayoutInflater将xml文件填充到ContentView。 填充过程中只包括创建视图,不包括设置视图大小。而设置视图的大小和具体的位置则是通过布局层层遍历获得的。 如下图:
先知先觉
2019/01/21
5820
[Android] Toast问题深度剖析(二)
题记 Toast 作为 Android 系统中最常用的类之一,由于其方便的api设计和简洁的交互体验,被我们所广泛采用。但是,伴随着我们开发的深入,Toast 的问题也逐渐暴露出来。 本系列文章将分成两篇: 第一篇,我们将分析 Toast 所带来的问题 第二篇,将提供解决 Toast 问题的解决方案 (注:本文源码基于Android 7.0) 1.回顾 上一篇 [[Android] Toast问题深度剖析(一)] 笔者解释了: Toast 系统如何构建窗口(通过系统服务NotificationManage
QQ音乐技术团队
2018/02/01
5K0
[Android] Toast问题深度剖析(二)
看完这篇,别再说你不了解 Handler 消息机制了
原文链接 https://juejin.cn/post/7291935623476183067
GeeJoe
2023/10/24
7740
看完这篇,别再说你不了解 Handler 消息机制了
Android面试题之自定义View注意事项
主要用于实现不规则的效果,即这种效果不方便通过布局的组合方式来实现。相当于就是得自己“画”了。采用这种方式需要自己支持wrap_content,padding也需要自己处理
AntDream
2024/07/22
1700
Android面试题之自定义View注意事项
从 Android 开发到读懂源码 第08期:Android应用层视图渲染机制
Android应用层是不涉及 SurfaceFlinger,FrameBuffer 之类的底层框架,常用刷新视图都是在 View 的 draw 相关方法中进行标准绘制 api 操作,然后通过 View.invalidate 或者 View.requestLayout 通知系统进行视图显示的刷新。在此不讨论 draw 相关的 api , draw 的所有绘制方法都是直接jni调用对应 skia 的绘制,具体的自己查看 skia 引擎相关的资料。
数据库交流
2022/04/25
6610
从 Android 开发到读懂源码 第08期:Android应用层视图渲染机制
【Andorid源码解析】View.post() 到底干了啥
emmm,大伙都知道,子线程是不能进行 UI 操作的,或者很多场景下,一些操作需要延迟执行,这些都可以通过 Handler 来解决。但说实话,实在是太懒了,总感觉写 Handler 太麻烦了,一不小心又很容易写出内存泄漏的代码来,所以为了偷懒,我就经常用 View.post() or View.postDelay() 来代替 Handler 使用。
请叫我大苏
2018/05/17
1.4K0
Android冷启动优化,一顿操作猛如虎
大部分开发者在遇到页面冷启动耗时初始化时,会首先考虑通过Handler.postDelay()方法延迟执行。但延迟多久合适?100ms?500ms?还是1s?
Android技术干货分享
2019/07/15
1.5K0
Android冷启动优化,一顿操作猛如虎
Android View post 方法
说起post方法,我们很容易联想到Handler的post方法,都是接收一个Runnable对象。那么这两个方法有啥不同呢?
玖柒的小窝
2021/09/20
5020
Android View post 方法
Android | 理解 ViewRootImpl
ViewRootImpl 是 View 的最高层级,是所有 View 的根。ViewRootImpl 实现了 View 和 WindowManager 之间所需要的协议。ViewRootImpl 的创建过程是从 WindowManagerImpl 中开始的。View 的测量,布局,绘制以及上屏,都是从 ViewRootImpl 中开始的。
345
2022/03/25
1.2K0
Android | 理解 ViewRootImpl
invalidate方法知多少[-View-] 源码级
零、前言 本文聚焦: [1].View#invalidate做了什么,为什么会触发View的重绘? [2].View是如何被添加到ViewGroup中的? [3].ViewGroup和ViewRoot
张风捷特烈
2022/09/20
2770
invalidate方法知多少[-View-] 源码级
带你了解源码中的 ThreadLocal提问源码分析小彩蛋应用场景
这次想来讲讲 ThreadLocal 这个很神奇的东西,最开始接触到这个是看了主席的《开发艺术探索》,后来是在研究 ViewRootImpl 中又碰到一次,而且还发现 Android 中一个小彩蛋,就越发觉得这个东西很有趣,那么便借助主席的这次作业来好好梳理下吧。
请叫我大苏
2018/08/02
4200
推荐阅读
相关推荐
面试官:View.post() 为什么能够获取到 View 的宽高?
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验