1. 程式人生 > >Android 自定義View的post(Runnable)方法非100%執行的原因和處理方法解析

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方法裡面都幹了什麼:

    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;
    }
可以看到,post()當中實際使用的是attachInfo的Handler,正好與我們出現問題的場景吻合(其實100%復現問題後找原因就很簡單了)。第三行,attachInfo 是否為空進行判斷,我們的問題場景明顯不符合,因此走到第7行。可以看到仍然是使用的ViewRootImpl這個根View,獲取它的RunQueue來post這個這個Runnable。 看到第6行這個註釋,我們就知道不太妙了:『假設它待會會成功執行』,然後系統默默地返回了true。。。 可以看到這裡與我們的問題已經完全對應了。

解決方案

從解決問題的角度,分析到這裡已經能夠形成解決方案了。那麼根據View的attach狀態,我們只需要在attached過後的detached期間,換用另一種更靠譜的方法彌補這個方法的不足即可。對於非同步但不要求delay的Runnable,直接執行即可:

        if(mAttached) {
            post(r);
        } else {
            r.run();
        }
其中r是我自己new出來的Runnable變數。如果仍然期待使用post()達到的效果拒絕立即同步呼叫,也可以換用Handler,程式碼也都相當簡單:
        if(mAttached) {
            post(r);
        } else {
            Handler handler = new Handler();
            handler.post(r);
        }
mAttached是自行維護的一個變數,等價於View的:
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的生命週期,避免實際執行時機與順序與預期的不一致。