您的位置:首页 > 移动开发 > Android开发

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

2015-12-11 10:56 507 查看
最近在写一个需求,需要在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%复现后,其解决方案总是很简单。

[b]浅析深入原理[/b]

只是解决问题,到上面的部分就可以结束了。但作为一个原理,我们还是希望继续深入了解一下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 handlerActi
93f0
on = 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的生命周期,避免实际执行时机与顺序与预期的不一致。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息