Activity黑屏分析

摘要

一、背景

近期有内部用户反馈页面操作若干下后黑屏,根据用户提供的手机型号和商品后稳定复现。

1.1 现象

1.2 操作现象+流程说明

  1. 进入商详页,点击立即购买;
  2. SKU浮层出现,点击切换SKU;
  3. 第一次切SKU,商详数据变化,第二次切SKU,商详数据未变化;(期望结果:商详数据应跟随SKU变化
  4. 点击SKU浮层立即购买;
  5. 进入提交订单页,点击返回,商详黑屏,SKU可正常操作。(SKU为Flutter页面,事件分发区别于Native

二、探索

2.1 Log日志

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Input Dispatcher State at time of last ANR:
ANR:
Time: 2021-04-13 21:17:19
Window: AppWindowToken{f779543 token=Token{4150af2 ActivityRecord{eb31dfd u0 com.xx/.xxActivity t2901}}}
DispatchLatency: 9679.6ms
WaitDuration: 5005.7ms
Reason: Waiting because no window has focus but there is a focused application that may eventually add a window when it finishes starting up.
DispatchEnabled: true
DispatchFrozen: false
InputFilterEnabled: false
FocusedDisplayId: 0
FocusedApplications:
displayId=0, name='AppWindowToken{f779543 token=Token{4150af2 ActivityRecord{eb31dfd u0 com.xx/.xxActivity t2901}}}', dispatchingTimeout=5000.000ms
FocusedWindows: <none>

Reason 内容来自 InputDispatcher::findFocusedWindowTargetsLocked,从日志可以看出 FocusedApplications 有值,FocusedWindows 为null,Reason 也说明当前处于无窗口有应用的状态。

2.2 复现过程中 Logcat 打印

在复现过程中,发现图片网络请求在频繁打印,且打印的图片 url 一直为重复的几张图,通过图片定位到模块代码,找到有一段可能导致触发频繁请求图片的代码:

1
2
3
4
5
6
7
8
9
10
LogisticsTraceView.this.getViewTreeObserver()// 注释1
.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
LogisticsTraceView.this.getViewTreeObserver().removeOnPreDrawListener(this);// 注释2
int width = LogisticsTraceView.this.getMeasuredWidth();
setContent(width, newTraceList);
return false;
}
});

这段代码在收到 onPreDraw 回调后就第一时间做了 remove操作,讲道理并不应该被频繁调用,不由猜测removeOnPreDrawListener 调用并没有成功将 listener 移除。

2.3 OnPreDrawListener 和 onPreDraw

OnPreDrawListener 官方介绍如下

Interface definition for a callback to be invoked when the view tree is about to be drawn.

onPreDraw 官方介绍如下:

Callback method to be invoked when the view tree is about to be drawn. At this point, all views in the tree have been measured and given a frame. Clients can use this to adjust their scroll bounds or even to request a new layout before drawing occurs.

大意就是:

  • OnPreDrawListener 是即将绘制视图树时执行的回调接口;
  • onPreDraw 是即将绘制视图树时执行的回调函数。此时所有的视图都完成测量并确定边界。客户端可以使用该方法来调整滚动边界,甚至在绘制之前重新请求布局。

此处使用该回调的主要原因是为了获取 View 的宽高后再执行数据绑定。

2.4 removeOnPreDrawListener 为何不生效?

在 RecyclerView.Adapter#onBindViewHolder 中执行 addOnPreDrawListener 操作,发现在 addOnPreDrawListener (见注释1)时 View#getViewTreeObserver 获取到的 observer 不等于 onPreDraw (见注释2) 回调里 View#getViewTreeObserver 获取到的 observer ,所以 removeOnPreDrawListener 调用肯定没有成功

2.4.1 getViewTreeObserver
1
2
3
4
5
6
7
8
9
10
// View.java
public ViewTreeObserver getViewTreeObserver() {
if (mAttachInfo != null) {
return mAttachInfo.mTreeObserver;
}
if (mFloatingTreeObserver == null) {
mFloatingTreeObserver = new ViewTreeObserver(mContext);
}
return mFloatingTreeObserver;
}

从源码不难看出,当 View 被 attach 到 window 上时 getViewTreeObserver 返回的是 attachInfo 中的 mTreeObserver,如果 View 没有被 attach 到 window 上时(即被从父容器中 remove 或从未被添加进任何父容器),返回的是另外一个ViewTreeObserver。

2.4.2 isAttachedToWindow

在 addOnPreDrawListener 调用时 和 onPreDraw 调用时,打印 View#isAttachedToWindow 发现都是 false,说明不管是 addOnPreDrawListener 调用时 还是 onPreDraw 回调时,LogisticsTraceView 均没有被添加到视图树中。

1
2
3
4
// View.java
public boolean isAttachedToWindow() {
return mAttachInfo != null;
}

但根据 2.4.1 getViewTreeObserver 所示,只有在 mFloatingTreeObserver == null 时才会重新 new 一个对象。搜索全局代码发现会将 mFloatingTreeObserver 置为 null 的地方在 dispatchAttachedToWindow 中,继续跟踪。

2.4.3 dispatchAttachedToWindow
1
2
3
4
5
6
7
8
9
10
11
12
// View.java
void dispatchAttachedToWindow(AttachInfo info, int visibility) {
mAttachInfo = info;
...
if (mFloatingTreeObserver != null) {
info.mTreeObserver.merge(mFloatingTreeObserver);
mFloatingTreeObserver = null;
}
...
onAttachedToWindow();
...
}

dispatchAttachedToWindow 不能被复写,但 onAttachedToWindow 可以,复写 onAttachedToWindow 代码后打印方法调用顺序如下:

1
2
3
4
2021-04-19 10:34:47.957 23796-23796/com.kaola E/LogisticsTraceView.addOnPreDrawListener
2021-04-19 10:34:47.958 23796-23796/com.kaola E/LogisticsTraceView.onAttachedToWindow
2021-04-19 10:34:47.963 23796-23796/com.kaola E/LogisticsTraceView.onDetachedFromWindow
2021-04-19 10:34:47.964 23796-23796/com.kaola E/LogisticsTraceView.onPreDraw

这下很清楚了,因为 addOnPreDrawListener 后 onAttachedToWindow 被调用,导致 mFloatingTreeObserver 置为 null,在 onPreDraw 里再次调用 View#getViewTreeObserver 拿到的 observer 又是再次 new 操作所得,所以二者 observer 肯定不相等,导致 removeOnPreDrawListener 失败。

2.4 onPreDraw 为何一直回调?

寻找 OnPreDrawListener 回调处,在 ViewTreeObserver 里触发:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// ViewTreeObserver.java
/**
* @return True if the current draw should be canceled and resceduled, false otherwise.
*/
public final boolean dispatchOnPreDraw() {
boolean cancelDraw = false;
final CopyOnWriteArray<OnPreDrawListener> listeners = mOnPreDrawListeners;
if (listeners != null && listeners.size() > 0) {
CopyOnWriteArray.Access<OnPreDrawListener> access = listeners.start();
try {
int count = access.size();
for (int i = 0; i < count; i++) {
cancelDraw |= !(access.get(i).onPreDraw());
}
} finally {
listeners.end();
}
}
return cancelDraw;
}

如果 onPreDraw 返回 true,那绘制流程会被取消并重新安排。搜索调用 dispatchOnPreDraw 的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// ViewRootImpl.java
private void performTraversals() {
boolean cancelDraw = mAttachInfo.mTreeObserver.dispatchOnPreDraw() || !isViewVisible;
if (!cancelDraw && !newSurface) {
...
performDraw();
} else {
if (isViewVisible) {
// Try again
scheduleTraversals();
} else if (mPendingTransitions != null && mPendingTransitions.size() > 0) {
...
}
}
}

如果 cancelDraw 一直返回 true,那 performDraw 一直无法执行,只能继续等待下一次 Vsync 信号到来,执行 scheduleTraversals,但下一次 onPreDraw 依然返回 false,导致 cancelDraw 依然返回 true,无限循环,View 无法绘制,导致该更新的数据未更新,这里只是 performDraw 这个方法跑不到,除此之外没有任何流程被阻塞,主线程消息队列不受影响,所以也就看不到 ANR 对话框。

三、修复

3.1 保证拿到的 ViewTreeObserver 一致

关于如何让 removeOnPreDrawListener 执行成功,需要保证的是每次拿到的 ViewTreeObserver 都一致,不管该 View 是否 attach 到视图树中。瞄准 getRootView 方法,可以很好的满足诉求。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// View.java
public View getRootView() {
if (mAttachInfo != null) {
final View v = mAttachInfo.mRootView;
if (v != null) {
return v;
}
}
View parent = this;
while (parent.mParent != null && parent.mParent instanceof View) {
parent = (View) parent.mParent;
}
return parent;
}

不管 mAttachInfo 还是 parent.mParent 都是在 ViewRootImpl 中设置上,保证返回的 View 一致。

可能有人会发出疑问,万一 addOnPreDrawListener 调用时 mAttachInfo 为空,但 onPreDraw 时 mAttachInfo 不为空,或者反过来,毕竟 getRootView 方法走了两段不一样的逻辑,为什么返回的值一定相同呢?咱们往下看。

3.1.1 mAttachInfo

mAttachInfo 的赋值处如下,具体 ViewRootImpl 方法的调用流程可参考Android视图之View的绘制流程与源码分析

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
// ViewRootImpl.java
public ViewRootImpl(Context context, Display display) {
...
mAttachInfo = new View.AttachInfo(mWindowSession, mWindow, display, this, mHandler, this);
...
}
private void performTraversals() {
...
if (mFirst) {
...
host.dispatchAttachedToWindow(mAttachInfo, 0);
...
}
...
}
// View.java
void dispatchAttachedToWindow(AttachInfo info, int visibility) {
mAttachInfo = info;
...
}
void dispatchDetachedFromWindow() {
...
mAttachInfo = null;
...
}
3.1.2 mParent

mParent 赋值处如下,具体 ViewRootImpl 方法的调用流程可参考Android视图之View的绘制流程与源码分析

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
// ViewRootImpl.java
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
...
// mAttachInfo 的 mRootView 为 DecorView
mAttachInfo.mRootView = view;
...
requestLayout();
...
// 该 view 为 DecorView,即 ViewRootImpl 是 DecorView 的 parent
view.assignParent(this);
...
}
void dispatchDetachedFromWindow() {
...
mView.assignParent(null);
...
}
// View.java
void assignParent(ViewParent parent) {
if (mParent == null) {
mParent = parent;
} else if (parent == null) {
mParent = null;
} else {
throw new RuntimeException("view " + this + " being added, but"
+ " it already has a parent");
}
}

对比 3.1.1 和 3.1.2 部分源码可以很明显的看到:

  • mAttachInfo.mRootView 是 DecorView;
  • while (parent.mParent != null && parent.mParent instanceof View) 的循环遍历,最顶层也是 DecorView,因为只有 DecorView instanceof View 为 true,ViewRootImpl 并不是 View;

所以第一种解法,把代码修改为:

1
2
3
4
5
6
7
8
9
10
LogisticsTraceView.this.getRootView().getViewTreeObserver()
.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
LogisticsTraceView.this.getRootView().getViewTreeObserver().removeOnPreDrawListener(this);
int width = LogisticsTraceView.this.getMeasuredWidth();
setContent(width, newTraceList);
return true
}
});

3.2 替换为 View#post 调用

1
2
3
4
LogisticsTraceView.this.post(() -> {
int width = LogisticsTraceView.this.getMeasuredWidth();
setContent(width, newTraceList);
});

四、总结

  1. 获取 ViewTreeObserver 时应该通过顶层View;
  2. 使用 OnPreDrawListener 时 onPreDraw 返回 false 应该要慎重(至少不能一直返回 false )