美文网首页
从一次诡异的Bug出发,窥探View更新的原理

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

作者: mrwangyong | 来源:发表于2018-07-03 10:12 被阅读46次
    前言

    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大佬们, 这个方法明显超行了好不好, 一个方法代码200多行,要命了,

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

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

    3. 从上面流程可以看出,要想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 树无法得到刷新

    4. 那该标记位什么时候变化,搜索整个源码 发现 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状态错误

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

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

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

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

    9. 感谢

    requestLayout in layout问题

    相关文章

      网友评论

          本文标题:从一次诡异的Bug出发,窥探View更新的原理

          本文链接:https://www.haomeiwen.com/subject/gelduftx.html