bug背景
华为手机上windowAnimationStyle属性没生效,导致项目里某个activity进出场动画都成了系统默认的动画。在onNewIntent
和onCreate
里使用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
,进而调用ActivityStackSupervisor
的activityIdleInternalLocked
方法,进而调用ActivityStack
的stopActivityLocked
方法,最后调用ActivityThread
的scheduleStopActivity
方法:1
2
3
4
5
6
7
8
9public 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 | private Field messagesField; |
可见消息队列第一个全是syncBarrier,而且很容易看到某个控件的dispatchDraw被执行了N多次,从ViewRootImpl
的源码中也可以知道invalidate或者是requestLayout,setLayoutParams等诸多UI操作都会往消息队列中post syncBarrier。1
2
3
4
5
6
7
8
9
10
11
12
13void 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
会调用ActivityTransitionState
的onStop
,这个类是专门处理activity跳转动画逻辑的,接下来的调用链如下:1
2ActivityTransitionState::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
7Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() {
public boolean queueIdle() {
// TODO resume 动画
return false;
}
});