Android 自定义View的post(Runnable)方法非100%执行的原因和处理方法解析

来源:互联网 发布:linux系统怎么ping 编辑:程序博客网 时间:2024/06/05 00:37

最近在写一个需求,需要在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的生命周期,避免实际执行时机与顺序与预期的不一致。

2 0
原创粉丝点击