Android 自定義View的post(Runnable)方法非100%執行的原因和處理方法解析
最近在寫一個需求,需要在view.post(Runnable)方法當中進行一些操作。但是實際使用中(特定場景)發現並不靠譜。
現象
如果呼叫了view的post(Runnable)方法,該Runnable在View處於detached狀態期間並不會執行;只有當此View或另一個View的view.post()方法被呼叫,且這個view處於attached狀態時(也就是這個Runnable能順利執行時),前一個post的Runnable才會順帶一塊被執行。
原理
一個功能既然一部分能夠成功執行,一部分不能夠成功執行,那一定是有原因的,那我們來看看View的post方法裡面都幹了什麼:
可以看到,post()當中實際使用的是attachInfo的Handler,正好與我們出現問題的場景吻合(其實100%復現問題後找原因就很簡單了)。第三行,attachInfo 是否為空進行判斷,我們的問題場景明顯不符合,因此走到第7行。可以看到仍然是使用的ViewRootImpl這個根View,獲取它的RunQueue來post這個這個Runnable。 看到第6行這個註釋,我們就知道不太妙了:『假設它待會會成功執行』,然後系統默默地返回了true。。。 可以看到這裡與我們的問題已經完全對應了。public boolean post(Runnable action) { final AttachInfo attachInfo = mAttachInfo; if (attachInfo != null) { return attachInfo.mHandler.post(action); } // Assume that post will succeed later ViewRootImpl.getRunQueue().post(action); return true; }
解決方案
從解決問題的角度,分析到這裡已經能夠形成解決方案了。那麼根據View的attach狀態,我們只需要在attached過後的detached期間,換用另一種更靠譜的方法彌補這個方法的不足即可。對於非同步但不要求delay的Runnable,直接執行即可:
if(mAttached) {
post(r);
} else {
r.run();
}
其中r是我自己new出來的Runnable變數。如果仍然期待使用post()達到的效果拒絕立即同步呼叫,也可以換用Handler,程式碼也都相當簡單:mAttached是自行維護的一個變數,等價於View的:if(mAttached) { post(r); } else { Handler handler = new Handler(); handler.post(r); }
isAttachedToWindow()
,只是該api較高,通常為了兼容於是自行維護該狀態。維護的程式碼如下: @Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
mAttached = true;
}
@Override
protected void onDetachedFromWindow() {
mAttached = false;
super.onDetachedFromWindow();
}
是不是很簡單。當一個問題能100%復現後,其解決方案總是很簡單。
淺析深入原理
只是解決問題,到上面的部分就可以結束了。但作為一個原理,我們還是希望繼續深入瞭解一下Android對post()這些機制的處理,那麼我們繼續深挖剛才看到的RunQueue的程式碼,至少能夠把我們的好奇心說服為止。來看看ViewRootImpl.GetRunQueue()的方法:
static RunQueue getRunQueue() {
RunQueue rq = sRunQueues.get();
if (rq != null) {
return rq;
}
rq = new RunQueue();
sRunQueues.set(rq);
return rq;
}
可以發現,只是一個相對簡單的get方法,存在了sRunQueues裡。那麼這個RunQueue是個啥Queue,我們來看一下這個RunQueue型別的說明。 /**
* The run queue is used to enqueue pending work from Views when no Handler is
* attached. The work is executed during the next call to performTraversals on
* the thread.
* @hide
*/
static final class RunQueue {
private final ArrayList<HandlerAction> mActions = new ArrayList<HandlerAction>();
void post(Runnable action) {
postDelayed(action, 0);
}
void postDelayed(Runnable action, long delayMillis) {
HandlerAction handlerAction = new HandlerAction();
handlerAction.action = action;
handlerAction.delay = delayMillis;
synchronized (mActions) {
mActions.add(handlerAction);
}
}
void executeActions(Handler handler) {
synchronized (mActions) {
final ArrayList<HandlerAction> actions = mActions;
final int count = actions.size();
for (int i = 0; i < count; i++) {
final HandlerAction handlerAction = actions.get(i);
handler.postDelayed(handlerAction.action, handlerAction.delay);
}
actions.clear();
}
}
private static class HandlerAction {
Runnable action;
long delay;
...
}
}
這裡只包括了主要的變數和方法。RunQueue是ViewRootImpl.class的一個內部靜態類,可以看到這個佇列是用一個叫做mActions的ArrayList實現的,元素就是一個Runnable(系統叫做action)和一個delay時間組成的物件。對於我們從使用角度理解原理,註釋已經把把該類的功能進行了一個概括:『這個執行佇列用於在沒有Handler attached時,把來自View的即將執行的工作加入此佇列。這個工作會此此執行緒下次呼叫遍歷時執行。』
這個程式碼較多,再精簡來看一下我們在post時會呼叫的runQueue.post(Runnable)方法:
void post(Runnable action) {
postDelayed(action, 0);
}
首先呼叫到postDelayed(Runnable, long),再看看實現: void postDelayed(Runnable action, long delayMillis) {
HandlerAction handlerAction = new HandlerAction();
handlerAction.action = action;
handlerAction.delay = delayMillis;
synchronized (mActions) {
mActions.add(handlerAction);
}
}
可以看到new了一個HandlerAction包裝我們傳入的action,並新增到mActions中。什麼?整個方法竟然就只是add到了一個List中。沒錯,這和我們之前觀察的現象是完全一致的。呼叫了post(),但並沒有執行。這些Runnable物件,會在excuteActions當中會執行:
void executeActions(Handler handler) {
synchronized (mActions) {
final ArrayList<HandlerAction> actions = mActions;
final int count = actions.size();
for (int i = 0; i < count; i++) {
final HandlerAction handlerAction = actions.get(i);
handler.postDelayed(handlerAction.action, handlerAction.delay);
}
actions.clear();
}
}
而在什麼時機呼叫的呢?在performTraversals當中,會呼叫該方法,執行加入佇列的操作如果有detached的view往佇列里加入過action。action就是我們傳入的Runnable物件。使用的Handler仍然是attachInfo的Handler,可以知道原理和view.post(Runnable)當中的前面那段的邏輯一致,也就是說相當於做了一個暫停,並在合適的時機再執行。執行時的呼叫實現都是一致的。 private void performTraversals() {
...
// Execute enqueued actions on every traversal in case a detached view enqueued an action
getRunQueue().executeActions(mAttachInfo.mHandler);
...
}
總結
至此,我們已經瞭解清楚,View在執行post(Runnable)方法時,會使用attachInfo中的mHandler;而在沒有attachedInfo時,會使用一個RunQueue暫時裝載著Runnable物件而不會立即執行;在進行遍歷時,會用新的attachInfo的Handler執行這個Runnable物件。這與我們觀察到的現象,以及使用的解決方法是一致的。
如果有需要一定執行同時又使用view.post(Runnable)實現時,留心一下post(Runnable)呼叫時View的生命週期,避免實際執行時機與順序與預期的不一致。