一、背景
在做項(xiàng)目時(shí),我們有一個(gè)相機(jī)界面,這個(gè)界面包括相機(jī)和一些浮層,其中有一個(gè)浮層是一個(gè)自定義的 View,負(fù)責(zé)在手機(jī)橫豎屏變化時(shí)展示一個(gè)提示,本來很簡單的一個(gè)界面,但是這個(gè)界面在使用一段時(shí)間后會(huì)偶現(xiàn)一種“假死狀態(tài)”,假死出現(xiàn)時(shí),相機(jī)預(yù)覽可以正常繪制,但是界面所有的點(diǎn)擊事件、回調(diào)事件全部消失,而且界面在過了 ANR 的時(shí)間后也不會(huì)出現(xiàn)崩潰,十分詭異,下面我就說下我們是怎么解決問題和分析問題的。
二、解決問題
我們首先排除了主線程被阻塞的可能性,因?yàn)殡m然頁面的 onBackClicked、onClick、EventBus 等回調(diào)完全無效,但是其 onTouch 事件是可以正常回調(diào)的,問題好像變的無解了。
對于這種看似無解的問題,我們只能通過重復(fù)其復(fù)現(xiàn)步驟,觀察規(guī)律,來尋求突破了。我們發(fā)現(xiàn),當(dāng)上文提到的我們的自定義 View 刷新頻率很快的時(shí)候,頁面很容易進(jìn)入假死狀態(tài),于是我們開始 review 這塊的代碼,然后看到了這個(gè)代碼塊:
@Subscribe
override fun onScreenOrientationChanged(event: ScreenOrientationEvent) {
if (mOrientation != event.getOrientationType()) {
mOrientation = event.getOrientationType()
invalidate()
}
}
這個(gè)界面通過 EventBus 監(jiān)聽屏幕方向變化,然后重新繪制自身,展示特殊的提示,但是這個(gè)事件的產(chǎn)生者是在一個(gè)異步線程分發(fā)的事件,所以這個(gè) invalidate() 函數(shù)也是在異步線程回調(diào)的,但是詭異的是這里并沒有拋出我們熟悉的“Only the original thread that created a view hierarchy can touch its views.” 異常信息,而是平靜正常的運(yùn)轉(zhuǎn)下去了,什么時(shí)候非 UI 線程也可以刷新界面了?當(dāng)然先解決問題,看看是不是這里的影響,我們修改代碼如下:
@Subscribe(threadMode = ThreadMode.MAIN)
override fun onScreenOrientationChanged(event: ScreenOrientationEvent) {
if (mOrientation != event.getOrientationType()) {
mOrientation = event.getOrientationType()
invalidate()
}
}
果然頁面再也沒有出現(xiàn)假死了,當(dāng)然我們也很好理解為什么頁面會(huì)進(jìn)入假死,因?yàn)?View 并沒有做同步處理,所以里面的一些標(biāo)志位在多線程競爭的時(shí)候就很容易出現(xiàn)錯(cuò)亂,導(dǎo)致 View 的狀態(tài)出現(xiàn)異常,從而可能會(huì)出現(xiàn)各種各樣的頁面假死,但是為什么我們可以在非 UI 線程刷新界面而且不拋出異常呢?這完全和我們的認(rèn)知相違背啊,下面我們就來剖析一下問題。
三、剖析問題
要了解為什么會(huì)出現(xiàn)問題的最好的辦法當(dāng)然就是 Read The Fucking Source Code 咯,首先我們看 invalidate 函數(shù):
/**
* Mark the area defined by dirty as needing to be drawn. If the view is
* visible, {@link #onDraw(android.graphics.Canvas)} will be called at some
* point in the future.
* <p>
* This must be called from a UI thread. To call from a non-UI thread, call
* {@link #postInvalidate()}.
* <p>
* <b>WARNING:</b> In API 19 and below, this method may be destructive to
* {@code dirty}.
*
* @param dirty the rectangle representing the bounds of the dirty region
*/
public void invalidate(Rect dirty) {
final int scrollX = mScrollX;
final int scrollY = mScrollY;
invalidateInternal(dirty.left - scrollX, dirty.top - scrollY,
dirty.right - scrollX, dirty.bottom - scrollY, true, false);
}
我們可以看到函數(shù)注釋中明確寫明了我們必須在 UI 線程調(diào)用這個(gè)函數(shù),我們再往下跟:
void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,
boolean fullInvalidate) {
//...
// 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);
}
//...
}
這個(gè)函數(shù)會(huì)構(gòu)建一個(gè)臟區(qū)域,然后通過父 View 去刷新自己,我們繼續(xù)跟 invalidateChild() 這個(gè)函數(shù),它的實(shí)現(xiàn)在 ViewGroup 中:
public final void invalidateChild(View child, final Rect dirty) {
//...
do {
View view = null;
if (parent instanceof View) {
view = (View) parent;
}
if (drawAnimation) {
if (view != null) {
view.mPrivateFlags |= PFLAG_DRAW_ANIMATION;
} else if (parent instanceof ViewRootImpl) {
((ViewRootImpl) parent).mIsAnimating = true;
}
}
// If the parent is dirty opaque or not dirty, mark it dirty with the opaque
// flag coming from the child that initiated the invalidate
if (view != null) {
if ((view.mViewFlags & FADING_EDGE_MASK) != 0 &&
view.getSolidColor() == 0) {
opaqueFlag = PFLAG_DIRTY;
}
if ((view.mPrivateFlags & PFLAG_DIRTY_MASK) != PFLAG_DIRTY) {
view.mPrivateFlags = (view.mPrivateFlags & ~PFLAG_DIRTY_MASK) | opaqueFlag;
}
}
parent = parent.invalidateChildInParent(location, dirty);
if (view != null) {
// Account for transform on current parent
Matrix m = view.getMatrix();
if (!m.isIdentity()) {
RectF boundingRect = attachInfo.mTmpTransformRect;
boundingRect.set(dirty);
m.mapRect(boundingRect);
dirty.set((int) Math.floor(boundingRect.left),
(int) Math.floor(boundingRect.top),
(int) Math.ceil(boundingRect.right),
(int) Math.ceil(boundingRect.bottom));
}
}
} while (parent != null);
//...
}
這里我們發(fā)現(xiàn),函數(shù)在 while 循環(huán)里逐漸向上尋找最終的父節(jié)點(diǎn),然后每一個(gè)父節(jié)點(diǎn)都會(huì)調(diào)用 invalidateChildInParent() 進(jìn)行刷新,這里也比較好理解,因?yàn)楦?View 的寬高可能會(huì)依賴與子 View 的寬高來計(jì)算,所以這里需要倒著逐層去算。然后我們知道界面的最終的父節(jié)點(diǎn)是 ViewRootImpl,當(dāng)然它不是一個(gè) View,只是一個(gè)中間層,我們直接走到 ViewRootImpl.invalidateChildInParent() 中去,我們發(fā)現(xiàn):
@Override
public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
checkThread();
//...
}
void checkThread() {
if (mThread != Thread.currentThread()) {
throw new CalledFromWrongThreadException(
"Only the original thread that created a view hierarchy can touch its views.");
}
}
在這里我們發(fā)現(xiàn)了我們熟悉的異常,也就是說我們不能在非 UI 線程更新界面的線程檢查是在這里做的(默認(rèn) ViewRootImpl 在主線程創(chuàng)建),但是走到這里我們依然沒有發(fā)現(xiàn)為什么我們可以在非 UI 線程刷新界面,因?yàn)榫€程檢查就在函數(shù)開始,不可能逃脫掉,那我們在向上走一走,我們看上方倒數(shù)第二個(gè)代碼塊,如果在 invalidateChild() 中的循環(huán)里,invalidateChildInParent() 返回了空,那循環(huán)就會(huì)被中斷,這樣才不會(huì)走到 ViewRootImpl 中去,從而逃脫掉線程檢查,也就是說,在逐層查找父節(jié)點(diǎn)時(shí),有一個(gè) ViewGroup 的 invalidateChildInParent() 返回了空,那我們看看 ViewGroup.invalidateChildInParent() 這個(gè)函數(shù)什么時(shí)候會(huì)返回空:
/**
* Don't call or override this method. It is used for the implementation of
* the view hierarchy.
*
* This implementation returns null if this ViewGroup does not have a parent,
* if this ViewGroup is already fully invalidated or if the dirty rectangle
* does not intersect with this ViewGroup's bounds.
*
* @deprecated Use {@link #onDescendantInvalidated(View, View)} instead to observe updates to
* draw state in descendants.
*/
@Deprecated
@Override
public ViewParent invalidateChildInParent(final int[] location, final Rect dirty) {
//...
}
這里我們直接看函數(shù)注釋:
如果此ViewGroup沒有父類,如果此ViewGroup已經(jīng)完全無效,或者如果臟矩形沒有與此ViewGroup的邊界相交,則此實(shí)現(xiàn)返回空。
看到這里我們其實(shí)也大概就可以理解作者的設(shè)計(jì)思路了,在 View 刷新時(shí),為了保證效率,我們只需要刷新那些受這個(gè) View 變化影響的 View 就可以了,所以當(dāng)我們判斷到臟區(qū)域不會(huì)再對外層 View 產(chǎn)生影像時(shí)我們就沒必要再去傳遞和刷新了,也就是說,我們自定義 View 刷新時(shí)因?yàn)闆]有一些特殊的變化(如寬、高),僅僅是自身繪制發(fā)生了變化,所以 invalidate 并不會(huì)傳遞到 ViewRootImpl 中去,也就沒有線程檢查這一說,所以在非 UI 線程中調(diào)用也不會(huì)拋出異常,雖然這是非法的。
四、總結(jié)
如果沒有遇到這個(gè)問題可能我永遠(yuǎn)都不會(huì)知道 invalidate 的這個(gè)秘密,也希望這個(gè)問題的解決方案能幫大家解決自己項(xiàng)目中出現(xiàn)的一些詭異問題。