前言
1 最近业务,有一个复现步骤和路径非常长的bug,经历过一些问题之后,出现名称和其他元素不显示的问题.这个问题复现步骤长,而且多次排查(陆陆续续一个多月,公司所有大佬都来看过没有找到真正原因),并没有什么布局问题,布局都是正常的布局
-
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,所以自然没有宽高 -
顺着上面的思路,看看,一个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
-
从上面流程可以看出,要想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 树无法得到刷新 -
那该标记位什么时候变化,搜索整个源码 发现
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
listenersCopy = (ArrayList )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
状态错误 根源找到了,接下来就是在该TextView所有的父控件的 requestlayout 和 onlayout 都打上日志,运行发现,重新走复现步骤发现,一个父控件 requestLayout 了 但是没有继续 走 onLayout,所有,真正的问题点就在这里,就是因为这一次的 requestLayout,导致
mPrivateFlags
错误,打印程序堆栈信息,发现是一个 Media层的回调,联想到之前 Media层回调经常忘记切线程,故意打了一个线程 Id,果然,线程 ID 为 thread-2580
一切理清楚了,在子线程一个 TextView.setText 了,引起了父 View 在子线程 request 了,而 requestLayout 在子线程中根本无法生效,到不了 ViewRootImpl,Layout 方法不走,
mPrivateFlags
状态一直重置不回来,导致后续的所有 requestlayout 无法生效什么,你问为什么在子线程 requestLayout 不会异常,因为 检测线程的代码,全都在ViewRootImpl 源码中, requestLayout 发不出去,自然不会调用检测线程的代码,也自然没有问题
感谢
requestLayout in layout问题