前言
這是一個初級Android工程師面試問題,一般標(biāo)準(zhǔn)答案:子線程不能操作UI控件纫版。
那我為什么還要問這個弱智的問題呢?
因為我心目中的標(biāo)準(zhǔn)答案:子線程不能操作"參與繪制"的UI控件匹耕。
一、什么是操作UI
如何理解我的標(biāo)準(zhǔn)答案恋捆,首先回答一下,什么叫做操作UI ?
以TextView為例:
TextView.setText("OK");//操作1
TextView.setBackgroundColor(0xffffff);//操作2
TextView.invalidate();//操作3
其實操作1和操作2最終也是調(diào)用操作3,操作3才是真正刷新界面的代碼偷仿。
“操作UI”理解成“調(diào)用View.invalidate()”。
問題也就變成了:子線程能否調(diào)用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);//調(diào)用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.
這句注釋其實很有意思,翻譯:將損壞矩形復(fù)制到父視圖羡玛。其實這個damage有助于提高繪制的效率别智,有興趣的朋友可以自己研究,以后我也會講這塊繪制的內(nèi)容稼稿,敬請期待薄榛。
言歸正傳,整個調(diào)用邏輯比較清晰可以看到
invalidate()->invalidate(true)->p.invalidateChild(this, damage);
按照一個Activity中View的嵌套關(guān)系让歼,我們可以知道整個調(diào)用過程變成了
invalidate()->invalidate(true)->DecorView.invalidateChild(this, damage)->ViewRootImpl.invalidateChild(this, damage);
三敞恋、ViewRootImpl.invalidateChild
繼續(xù)研究ViewRootImpl.invalidateChild的代碼邏輯
@Override
public void invalidateChild(View child, Rect dirty) {
invalidateChildInParent(null, dirty);
}
@Override
public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
checkThread();//最關(guān)鍵的線程檢查,如果是子線程就拋出異常了
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.");
}
}
整個調(diào)用邏輯invalidateChild-> invalidateChildInParent-> checkThread
在checkThread中硬猫,如果發(fā)現(xiàn)當(dāng)前的線程Thread.currentThread不等于mThread(主線程/UI線程),也就是子線程。
拋出異常:Only the original thread that created a view hierarchy can touch its views.
四啸蜜、什么是參與繪制
看完上面的內(nèi)容坑雅,肯定有人說答案不就是子線程不能操作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));
}
當(dāng)前的View不可見且沒有動畫霞丧,那skipInvalidate就是true,直接return了冕香,就可以避免調(diào)用到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);//調(diào)用ViewParent的invalidateChild
}
當(dāng)前View沒有ViewParent或者最頂層的ViewParent不是ViewRootImpl悉尾,也可以避免調(diào)用到ViewRootImpl.invalidateChild-> checkThread突那,也就不會拋出異常了。
4.2.1 什么情況下View沒有ViewParent:
我們new了一個View還沒有addView到一個ViewParent
4.2.2 什么情況下最頂層的ViewParent不是ViewRootImpl
我們在Activity觸發(fā)onResume之前操作View构眯,因為這個時候Decorview還沒有添加到ViewRootImpl愕难。
以上情況就是屬于“不參與繪制”的情況
總結(jié)
現(xiàn)在應(yīng)該理解我的標(biāo)準(zhǔn)答案:子線程不能操作"參與繪制"的UI控件。
思考
postInvalidate能否在子線程調(diào)用惫霸,它與invalidate的區(qū)別在哪里猫缭?