继续记录我的疑难bug解决过程,这次要说的bug相比前几篇来说,更难定位,因为影响较大,直接导致不解决这个bug,根本就没有办法出版本,两三个同事定位了半天也没有结果,最后我自告奋勇的暂时放下手中的工作任务,去解决,因为我确实很喜欢解决疑难的bug,哈哈,这里面小小自吹一下哈~~
主要的现象是一些操作之后,添加fragment,正常应该会显示fragment中的页面,但是这个bug奇怪的地方就是fragment的生命周期都走了,onCreateView 也正常,页面却始终不出来。
业务流程:互动上课中,点击下课,回到主界面,再进去课表页面
bug是回到主界面再次进去课表页面,课表页面不见了。
一、现象描述:
1、我先贴出正常的页面操作流程,见下图。
2、bug的现象:
对比画面的最后一帧,正常现象是返回到课表页面,出问题的是进去课表没有了,显现的是摄像机画面。
二、问题定位
首先经过日志打印,fragment生命周期是走的,再次通过Android DeviceMonitor工具查看view的层级树,看看是怎么回事。
右边红框子里面的是fragment的根view,左边的参数表明这个view的宽和高都是0,说明虽然已经添加,但是没有测量,是不是很奇怪?
遂又从日志中分析,发现了一个 令我警惕的日志。
android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views
很明显,在子线程当中操作了View的状态,应该就是和这个有关系了,这个操作代码是隐藏的子线程,不容易发现,是其他同事加的,好了,找到直接原因了,版本可以顺利发布了,得到了同事们的夸赞,
后面 改为在主线程中操作,果然问题就没有了,
<但是子线程操作是如何导致问题产生的呢?是什么样的机制原理呢?这个才是本篇博客的重点
三、本质原因分析
我们正常子线程操作view,程序会崩溃,这位同事加的代码用异常捕获掉了,所以没有那么容易发现。
这个代码中,子线程是做了view.setVisibility操作,继续源码分析模式~。
1、 setVisilibity
@RemotableViewMethod
public void setVisibility(@Visibility int visibility) {
setFlags(visibility, VISIBILITY_MASK);
}
2、setFlags
void setFlags(int flags, int mask) {
final boolean accessibilityEnabled =
AccessibilityManager.getInstance(mContext).isEnabled();
final boolean oldIncludeForAccessibility = accessibilityEnabled && includeForAccessibility();
int old = mViewFlags;
mViewFlags = (mViewFlags & ~mask) | (flags & mask);
//省去部分代码
final int newVisibility = flags & VISIBILITY_MASK;
if (newVisibility == VISIBLE) {
if ((changed & VISIBILITY_MASK) != 0) {
// 设置flag
mPrivateFlags |= PFLAG_DRAWN;
// 走这里方法,重新绘制
invalidate(true);
}
}
}
3、invalidate 方法
void invalidate(boolean invalidateCache) {
invalidateInternal(0, 0, mRight - mLeft, mBottom - mTop, invalidateCache, true);
}
void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,
boolean fullInvalidate) {
// 省略代码
if ((mPrivateFlags & (PFLAG_DRAWN | PFLAG_HAS_BOUNDS)) == (PFLAG_DRAWN | PFLAG_HAS_BOUNDS)
|| (invalidateCache && (mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == PFLAG_DRAWING_CACHE_VALID)
|| (mPrivateFlags & PFLAG_INVALIDATED) != PFLAG_INVALIDATED
|| (fullInvalidate && isOpaque() != mLastIsOpaque)) {
if (fullInvalidate) {
mLastIsOpaque = isOpaque();
mPrivateFlags &= ~PFLAG_DRAWN;
}
mPrivateFlags |= PFLAG_DIRTY;
if (invalidateCache) {
mPrivateFlags |= PFLAG_INVALIDATED;
// 设置 PFLAG_DRAWING_CACHE_VALID 的flag,此处是重点;
mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID;
}
// Propagate the damage rectangle to the parent view.
final AttachInfo ai = mAttachInfo;
final ViewParent p = mParent;
if (p != null && ai != null && l < r && t < b) {
final Rect damage = ai.mTmpInvalRect;
damage.set(l, t, r, b);
p.invalidateChild(this, damage);
}
}
4、ViewGroup 的 invalidateChild方法
/**
* Don't call or override this method. It is used for the implementation of
* the view hierarchy.
*/
@Override
public final void invalidateChild(View child, final Rect dirty) {
ViewParent parent = this;
// 省略部分代码,只抓取最主要的代码
//此处从当前view,开启循坏遍历,一直到达ViewRootImpl,调用每一个层级的invalidateChildInParent
do {
View view = null;
if (parent instanceof View) {
view = (View) parent;
}
parent = parent.invalidateChildInParent(location, dirty);
}
} while (parent != null);
}
}
5、ViewGroup 的 invalidateChildInParent 方法
这个方法是根据当前的flag等条件,返回父view
这个方法很关键,也是后面导致添加fragment不显示的方法原因所在。
public ViewParent invalidateChildInParent(final int[] location, final Rect dirty) {
// 条件判断。是否是有效的cache或者可以绘制的。
if ((mPrivateFlags & PFLAG_DRAWN) == PFLAG_DRAWN ||
(mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == PFLAG_DRAWING_CACHE_VALID) {
if ((mGroupFlags & (FLAG_OPTIMIZE_INVALIDATE | FLAG_ANIMATION_DONE)) !=
FLAG_OPTIMIZE_INVALIDATE) {
//设置flag的cache无效,需要走绘制流程
mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID;
location[CHILD_LEFT_INDEX] = left;
location[CHILD_TOP_INDEX] = top;
if (mLayerType != LAYER_TYPE_NONE) {
mPrivateFlags |= PFLAG_INVALIDATED;
}
return mParent;
} else {
mPrivateFlags &= ~PFLAG_DRAWN & ~PFLAG_DRAWING_CACHE_VALID;
if (mLayerType != LAYER_TYPE_NONE) {
mPrivateFlags |= PFLAG_INVALIDATED;
}
return mParent;
}
}
//没有找到父view
return null;
}
好了,到这里我们理一下,第四个方法,开启了循坏,一直找ViewParent,并且将这条路径上的每个view的flag都设置成CacheInvalid等状态。想象一下,就像一个view树,从底部结点一直找到根view。
我们看下,ViewRootImpl 的invalidateChildInParent
@Override
public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
checkThread();
///XXXX
invalidateRectOnScreen(dirty);
return null;
}
首先进行了 线程检测,非主线程操作,抛出异常。
只不过我们代码中被我们捕获了异常,程序没有崩溃,下次我继续添加view,没有显现,我刚才已经讲了,如果正常流程走下来,会触发ViewRootImpl的performTraversals,然后View布局绘制测量三大方法,每一层的view的flag都会设置成正常状态。
但是怪就怪在程序没有崩溃,上面视图检测工具中我红色框框标出来的ViewGroup的flag被设置成了PFLAG_DRAWING_CACHE_VALID 以及 PFLAG_DRAWN,所以下次调用该ViewGroup的addView的时候,又走到了上面第五步invalidateChildInParent方法,该方法首先判断 flag,不满足,返回null,所以addView 也一直想找到ViewRootImpl的这条路径被掐断了,所以我们看到视图工具里面虽然add上去了,但是根本就没有onMeasure,宽和高都为0,所以也根本就没有页面显示出来了。
好了,分析源码不易,但要抓住重点,这样更能锻炼自己的分析疑难问题的能力以及源码阅读能力