前言
这是一个初级Android工程师面试问题,一般标准答案:子线程不能操作UI控件。
那我为什么还要问这个弱智的问题呢?
因为我心目中的标准答案:子线程不能操作"参与绘制"的UI控件。
一、什么是操作UI
如何理解我的标准答案,首先回答一下,什么叫做操作UI ?
以TextView为例:
TextView.setText("OK");//操作1
TextView.setBackgroundColor(0xffffff);//操作2
TextView.invalidate();//操作3
其实操作1和操作2最终也是调用操作3,操作3才是真正刷新界面的代码。
“操作UI”理解成“调用View.invalidate()”。
问题也就变成了:子线程能否调用View.invalidate()。
二、View.invalidate()
接下来分析一下View.invalidate()代码
代码2.1
public void invalidate() {
invalidate(true);
}
public 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 (skipInvalidate()) {//判断是否需要跳过绘制
return;
}
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)) {
...
// 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);//调用ViewParent的invalidateChild
}
...
}
}
private boolean skipInvalidate() {
return (mViewFlags & VISIBILITY_MASK) != VISIBLE && mCurrentAnimation == null &&
(!(mParent instanceof ViewGroup) ||
!((ViewGroup) mParent).isViewTransitioning(this));
}
// Propagate the damage rectangle to the parent view.
这句注释其实很有意思,翻译:将损坏矩形复制到父视图。其实这个damage有助于提高绘制的效率,有兴趣的朋友可以自己研究,以后我也会讲这块绘制的内容,敬请期待。
言归正传,整个调用逻辑比较清晰可以看到
invalidate()->invalidate(true)->p.invalidateChild(this, damage);
按照一个Activity中View的嵌套关系,我们可以知道整个调用过程变成了
invalidate()->invalidate(true)->DecorView.invalidateChild(this, damage)->ViewRootImpl.invalidateChild(this, damage);
三、ViewRootImpl.invalidateChild
继续研究ViewRootImpl.invalidateChild的代码逻辑
@Override
public void invalidateChild(View child, Rect dirty) {
invalidateChildInParent(null, dirty);
}
@Override
public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
checkThread();//最关键的线程检查,如果是子线程就抛出异常了
if (DEBUG_DRAW) Log.v(mTag, "Invalidate child: " + dirty);
if (dirty == null) {
invalidate();
return null;
} else if (dirty.isEmpty() && !mIsAnimating) {
return null;
}
if (mCurScrollY != 0 || mTranslator != null) {
mTempRect.set(dirty);
dirty = mTempRect;
if (mCurScrollY != 0) {
dirty.offset(0, -mCurScrollY);
}
if (mTranslator != null) {
mTranslator.translateRectInAppWindowToScreen(dirty);
}
if (mAttachInfo.mScalingRequired) {
dirty.inset(-1, -1);
}
}
invalidateRectOnScreen(dirty);
return null;
}
void checkThread() {
if (mThread != Thread.currentThread()) {//mThread为主线程,也就是UI线程
//子线程就抛出异常了
throw new CalledFromWrongThreadException(
"Only the original thread that created a view hierarchy can touch its views.");
}
}
整个调用逻辑invalidateChild-> invalidateChildInParent-> checkThread
在checkThread中,如果发现当前的线程Thread.currentThread不等于mThread(主线程/UI线程),也就是子线程。
抛出异常:Only the original thread that created a view hierarchy can touch its views.
四、什么是参与绘制
看完上面的内容,肯定有人说答案不就是子线程不能操作UI控件嘛,为什么还要加上"参与绘制"的条件。我们好好思考一下代码2.1中两处代码
4.1 skipInvalidate
if (skipInvalidate()) {//判断是否需要跳过绘制
return;
}
//判断是否需要可见或者处于动画中
private boolean skipInvalidate() {
return (mViewFlags & VISIBILITY_MASK) != VISIBLE && mCurrentAnimation == null &&
(!(mParent instanceof ViewGroup) ||
!((ViewGroup) mParent).isViewTransitioning(this));
}
当前的View不可见且没有动画,那skipInvalidate就是true,直接return了,就可以避免调用到ViewRootImpl.invalidateChild-> checkThread,也就不会抛出异常了。
4.2 ViewParent
if (p != null && ai != null && l < r && t < b) {
final Rect damage = ai.mTmpInvalRect;
damage.set(l, t, r, b);
p.invalidateChild(this, damage);//调用ViewParent的invalidateChild
}
当前View没有ViewParent或者最顶层的ViewParent不是ViewRootImpl,也可以避免调用到ViewRootImpl.invalidateChild-> checkThread,也就不会抛出异常了。
4.2.1 什么情况下View没有ViewParent:
我们new了一个View还没有addView到一个ViewParent
4.2.2 什么情况下最顶层的ViewParent不是ViewRootImpl
我们在Activity触发onResume之前操作View,因为这个时候Decorview还没有添加到ViewRootImpl。
以上情况就是属于“不参与绘制”的情况
总结
现在应该理解我的标准答案:子线程不能操作"参与绘制"的UI控件。
思考
postInvalidate能否在子线程调用,它与invalidate的区别在哪里?