overridePendingTransition居然失效了?

bug背景

华为手机上windowAnimationStyle属性没生效,导致项目里某个activity进出场动画都成了系统默认的动画。在onNewIntentonCreate里使用overridePendingTransition设置入场动画就能够让绝大多数case都有正常的入场动画,但是有个case例外:

从activity A进入该activity B入场动画正常,然后退出activity B后迅速重进B -> 入场动画变成了华为系统默认的动画

bug分析

通过打印出activity B的生命周期可以看出,出问题的时候onStop居然在onPause之后差不多1.7秒才执行,直觉上感觉可能是再次进入B 时退出时候的onStop还没调用导致的问题,简单验证一下确实如此(原因下文结合源码分析)。 那么onStop为什么延迟调用了呢?

我们退出activity B回到activity A的时候:首先是activity B先onPause,然后activity A onResume,再activity B onStop,那么会不会是activity A onResume执行时间过长呢?打点后发现才50ms,这个理由不成立。

通过查看activity销毁源码可以看出,ActivityThread执行handleResumeActivity的时候通过:

1
Looper.myQueue().addIdleHandler(new Idler());

给主线程Looper关联的消息队列加了个IdleHandler,IdleHandler执行的时候会调用AMS的activityIdle,进而调用ActivityStackSupervisoractivityIdleInternalLocked方法,进而调用ActivityStackstopActivityLocked方法,最后调用ActivityThreadscheduleStopActivity方法:

1
2
3
4
5
6
7
8
9
public final void scheduleStopActivity(IBinder token, boolean showWindow,
int configChanges) {
int seq = getLifecycleSeq();
if (DEBUG_ORDER) Slog.d(TAG, "stopActivity " + ActivityThread.this
+ " operation received seq: " + seq);
sendMessage(
showWindow ? H.STOP_ACTIVITY_SHOW : H.STOP_ACTIVITY_HIDE,
token, 0, configChanges, seq);
}

由于IdleHandler的执行受制于当前MessageQueue里SyncBarrier消息的执行(即MessageQueue为空或者是第一个message被延期执行时IdleHandler才会执行),所以很可能当前主线程的消息队列头一直被SyncBarrier阻塞,具体原因可见MessageQueue的next部分源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
    // If first time idle, then get the number of idlers to run.
// Idle handles only run if the queue is empty or if the first message
// in the queue (possibly a barrier) is due to be handled in the future.
if (pendingIdleHandlerCount < 0
&& (mMessages == null || now < mMessages.when)) {
pendingIdleHandlerCount = mIdleHandlers.size();
}
if (pendingIdleHandlerCount <= 0) {
// No idle handlers to run. Loop and wait some more.
mBlocked = true;
continue;
}

if (mPendingIdleHandlers == null) {
mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)];
}
mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);
}

// Run the idle handlers.
// We only ever reach this code block during the first iteration.
for (int i = 0; i < pendingIdleHandlerCount; i++) {
final IdleHandler idler = mPendingIdleHandlers[i];
mPendingIdleHandlers[i] = null; // release the reference to the handler

boolean keep = false;
try {
keep = idler.queueIdle();
} catch (Throwable t) {
Log.wtf(TAG, "IdleHandler threw exception", t);
}

if (!keep) {
synchronized (this) {
mIdleHandlers.remove(idler);
}
}
}

可以通过反射尝试在主线程每一帧打印出messageQueue里的消息看看到底是什么鬼阻塞了IdleHandler。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
private Field messagesField;
private Field nextField;
{
try {
messagesField = MessageQueue.class.getDeclaredField("mMessages");
messagesField.setAccessible(true);
nextField = Message.class.getDeclaredField("next");
nextField.setAccessible(true);
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
}

@Override
protected void onResume() {
Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() {
@Override
public void doFrame(long frameTimeNanos) {
Choreographer.getInstance().postFrameCallback(this);
Log.d(TAG, "doFrame");
printMessages();
}
});
}
}

private void printMessages() {
MessageQueue queue = Looper.myQueue();
try {
Message msg = (Message) messagesField.get(queue);
StringBuilder sb = new StringBuilder();
while (msg != null) {
sb.append(msg.toString());
sb.append("\n");
msg = (Message) nextField.get(msg);
}
Log.i(TAG, sb.toString());
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}

可见消息队列第一个全是syncBarrier,而且很容易看到某个控件的dispatchDraw被执行了N多次,从ViewRootImpl的源码中也可以知道invalidate或者是requestLayout,setLayoutParams等诸多UI操作都会往消息队列中post syncBarrier。

1
2
3
4
5
6
7
8
9
10
11
12
13
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
if (!mUnbufferedInputDispatch) {
scheduleConsumeBatchedInput();
}
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}

所以问题的根源就在某个切换主题的动画导致了 dispatchDraw被执行了多次,导致消息队列头部一直是SyncBarrier,idleHandler得不到执行,onStop迟迟不能调用

再回到一开始的一个问题:为什么activity B的onStop没执行会影响到重进B的动画?

let’s read the fucking source code again!

activity的onStop会调用ActivityTransitionStateonStop,这个类是专门处理activity跳转动画逻辑的,接下来的调用链如下:

1
2
ActivityTransitionState::restoreExitedViews
ExitTransitionCoordinator::resetViews

从ExitTransitionCoordinator类的注释可以看到:

1
2
3
4
5
/**
* This ActivityTransitionCoordinator is created in ActivityOptions#makeSceneTransitionAnimation
* to govern the exit of the Scene and the shared elements when calling an Activity as well as
* the reentry of the Scene when coming back from the called Activity.
*/

该类是掌控activity退出和重进动画的。resetViews会设置transitionViews和SharedElements为可见,并取消背景动画,如果没有此步,所有的切换动画均看不到。

—— 更新 ——

同事遇到过这种场景:从activity B退回到activity A的时候,在A的onResume里继续A的动画,看上去是没有问题的。但是因为A的onResume在B的onStop之前执行,所以动画又延迟了B的onStop执行。这种情况可以考虑把动画在B的onStop之后执行。

1
2
3
4
5
6
7
Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() {
@Override
public boolean queueIdle() {
// TODO resume 动画
return false;
}
});