从一次诡异的Bug出发,窥探View更新的原理

前言

1 最近业务,有一个复现步骤和路径非常长的bug,经历过一些问题之后,出现名称和其他元素不显示的问题.这个问题复现步骤长,而且多次排查(陆陆续续一个多月,公司所有大佬都来看过没有找到真正原因),并没有什么布局问题,布局都是正常的布局

  1. Debug问题出现点,发现里面的显示名称TextView,有名称时展示,没名称是Gone

    if (TextUtils.isEmpty(name)) {
                mName.setVisibility(GONE);
            } else {
                mName.setVisibility(VISIBLE);
            }
    

    这样理论上来说,不会有什么问题,每次name不为空时,TextView由GONE变为Visible状态,这个时候,会触发TextView发出requestLayout,因为布局发生改变了(Gone不占用空间,而Visible占用空间),而观众上座后,不显示名称,Debug发现TextView 已经Visible了,但是宽高都是0,我们之前 requestLayout必须层层传递,发到最顶级的父类ViewRootImpl中才会有效,说明这个请求没有发出去,导致没有走onLayout,所以自然没有宽高

  2. 顺着上面的思路,看看,一个View发出requestLayout只有,到底走了那些流程

    public void requestLayout() {
            if (mMeasureCache != null) mMeasureCache.clear();
            // 判断当前View是否已经attach了 当前肯定是已经attach了
            if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == null) {
                // Only trigger request-during-layout logic if this is the view requesting it,
                // not the views in its parent hierarchy
                ViewRootImpl viewRoot = getViewRootImpl();
                if (viewRoot != null && viewRoot.isInLayout()) {
                    if (!viewRoot.requestLayoutDuringLayout(this)) {
                        return;
                    }
                }
                mAttachInfo.mViewRequestingLayout = this;
            }
            // 把 mPrivateFlags 改为 PFLAG_FORCE_LAYOUT 说明正在更新布局 
            mPrivateFlags |= PFLAG_FORCE_LAYOUT;
             // 把 mPrivateFlags 改为 PFLAG_INVALIDATED 说明正在重绘  并不会覆盖上面的值 因为采用大bitMap法 32位每个位记录不一样的信息
            mPrivateFlags |= PFLAG_INVALIDATED;
            // isLayoutRequested 父控件是否在更新布局中,如果正在更新布局,则无法响应此次请求
            if (mParent != null && !mParent.isLayoutRequested()) {
                mParent.requestLayout();
            }
            if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == this) {
                mAttachInfo.mViewRequestingLayout = null;
            }
        }
    

    这里的父控件会一层层的往上传递,直到最顶级的父类ViewRootImpl

    @Override
        public void requestLayout() {
            if (!mHandlingLayoutInLayoutRequest) {
             // 检查是不是主线程
                checkThread();
                //设置标记
                mLayoutRequested = true;
                //真正刷新View树的方法
                scheduleTraversals();
            }
        }
    

    然后

     void scheduleTraversals() {
            if (!mTraversalScheduled) {
                mTraversalScheduled = true;
                mTraversalBarrier = mHandler.getLooper().postSyncBarrier();
                // 通过 mChoreographer 发送一个Handler消息,更新布局,每16.5ms更新一次
                mChoreographer.postCallback(
                        Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
                if (!mUnbufferedInputDispatch) {
                    scheduleConsumeBatchedInput();
                }
                notifyRendererOfFramePending();
            }
        }
    

    执行了 mTraversalRunnable 这个Runable里面的方法为doTraversal

    void doTraversal() {
                ....
                try {
                    performTraversals();
                } finally {
                    Trace.traceEnd(Trace.TRACE_TAG_VIEW);
                }
                ...
            }
        }
    

    最终走的是performTraversals

    private void performTraversals() {
        ..... 
        performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
        ......
        performLayout(lp, desiredWindowWidth, desiredWindowHeight);
        .....
        performDraw();
        .....
    }
    
    

    喂喂, Google大佬们, 这个方法明显超行了好不好, 一个方法代码2000多行,要命了,

    最主要的是调用这三个方法,后面的方法,大家都知道了

    performMeasure -> Measure->onMeasure()-> measureChildren->chlid onMeasure()
    performLayout -> layout -> onLayout
    performDraw -> draw->onDraw

  1. 从上面流程可以看出,要想TextView的OnLayout 执行,必须requestLayout发到底层的ViewRootImpl中,问题的原因是因为requestlayout的请求没有发出去,到底是哪里出了问题, 后续通过一步步的Debug该View的父类,发现有一个WindowControllerView的父类,requestlayout发到他这里,接收了,但是没有往上传递,继续Debug源码,发现

    if (mParent != null && !mParent.isLayoutRequested()) {
                mParent.requestLayout();
            }
    

    mParent.isLayoutRequested()这个返回为true,导致没有执行,查看该方法的实现

    public boolean isLayoutRequested() {
            return (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;
        }
    

    还是这个 mPrivateFlags 的原因,最终定位到这个mPrivateFlags上,就是因为这个mPrivateFlags的状态异常,导致整个 View 树无法得到刷新

  2. 那该标记位什么时候变化,搜索整个源码 发现 Layout Measure Draw focus等方法中会改变,而 requestLayout 中会变为PFLAG_FORCE_LAYOUT 而这个值什么时候可以改变呢?Layout

    public void layout(int l, int t, int r, int b) {
            if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
                onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
                mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
            }
    
            int oldL = mLeft;
            int oldT = mTop;
            int oldB = mBottom;
            int oldR = mRight;
    
            boolean changed = isLayoutModeOptical(mParent) ?
                    setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
    
            if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
                onLayout(changed, l, t, r, b);
                mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
    
                ListenerInfo li = mListenerInfo;
                if (li != null && li.mOnLayoutChangeListeners != null) {
                    ArrayList<OnLayoutChangeListener> listenersCopy =
                            (ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
                    int numListeners = listenersCopy.size();
                    for (int i = 0; i < numListeners; ++i) {
                        listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
                    }
                }
            }
         // 这里 改为了非PFLAG_FORCE_LAYOUT值
            mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
            mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
        }
    

    所以说,requestLayout和 layout 方法,一一对应,如果只有一个执行,另外一个不执行,都会导致mPrivateFlags状态错误

  3. 根源找到了,接下来就是在该TextView所有的父控件的 requestlayout 和 onlayout 都打上日志,运行发现,重新走复现步骤发现,一个父控件 requestLayout 了 但是没有继续 走 onLayout,所有,真正的问题点就在这里,就是因为这一次的 requestLayout,导致mPrivateFlags错误,

  4. 打印程序堆栈信息,发现是一个 Media层的回调,联想到之前 Media层回调经常忘记切线程,故意打了一个线程 Id,果然,线程 ID 为 thread-2580

  5. 一切理清楚了,在子线程一个 TextView.setText 了,引起了父 View 在子线程 request 了,而 requestLayout 在子线程中根本无法生效,到不了 ViewRootImpl,Layout 方法不走,mPrivateFlags状态一直重置不回来,导致后续的所有 requestlayout 无法生效

  6. 什么,你问为什么在子线程 requestLayout 不会异常,因为 检测线程的代码,全都在ViewRootImpl 源码中, requestLayout 发不出去,自然不会调用检测线程的代码,也自然没有问题

  7. 感谢

requestLayout in layout问题

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,063评论 6 510
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,805评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,403评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,110评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,130评论 6 395
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,877评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,533评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,429评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,947评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,078评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,204评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,894评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,546评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,086评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,195评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,519评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,198评论 2 357