记录 Android 上 偶现 React Native - ViewManager for tag XXX could not be found 问题
背景
项目在某一个版本上的 RN 页面突然出现 “ViewManager for tag XXX could not be found” 的问题,而且每天影响用户量在稳定增加,相关同事立即把此次版本上有改动 RN 代码的同学拉到了问题定位小组,而我很‘幸运’就在此次版本上修复了 RN 的另外一个问题,然后就开始了自证清白的问题定位之旅~
问题定位
首先需要找到问题抛出的代码位置:RN 框架中的 NativeViewHierarchyManager 类,如下:
public final synchronized ViewManager resolveViewManager(int tag) {
ViewManager viewManager = mTagsToViewManagers.get(tag);
if (viewManager == null) {
boolean alreadyDropped = Arrays.asList(mDroppedViewArray).contains(tag);
throw new IllegalViewOperationException(
"ViewManager for tag "
+ tag
+ " could not be found.\n View already dropped? "
+ alreadyDropped
+ ".\nLast index "
+ mDroppedViewIndex
+ " in last 100 views"
+ mDroppedViewArray.toString());
}
return viewManager;
}
该方法被调用的地方有:
- manageChildren时,view显示/隐藏时,例如数据返回前显示子元素a,返回后隐藏子元素a;
- setChildren时,简化版本的manageChildren,增加子view时使用;
- dropView时,删除rootview或者子view;
- updateProperties时,更新view时;
- updateViewExtraData时,更新view样式;
- dispatchCommand时,分发调用view的命令。
场景还是挺多的,且从经过混淆的日志信息中也无法判断是哪里抛出的,此处的线索就中断了。。。
再观察一下抛出异常的代码可知,异常说明 mTagsToViewManagers 中没有对应 tag 的 ViewManager,为什么会没有呢?只有两种可能:没有添加对应 tag 的 ViewManager 或者是移除了对应 tag 的 ViewManager。 继续查看对 mTagsToViewManagers 增/删的相关源码:
mTagsToViewManagers 中添加元素的地方,有两处:
第一处创建 RN 页面的 RootView 时:
protected final synchronized void addRootViewGroup(int tag, View view) {
if (view.getId() != View.NO_ID) {
FLog.e(
TAG,
"Trying to add a root view with an explicit id ("
+ view.getId()
+ ") already "
+ "set. React Native uses the id field to track react tags and will overwrite this field. "
+ "If that is fine, explicitly overwrite the id field to View.NO_ID before calling "
+ "addRootView.");
}
mTagsToViews.put(tag, view);
mTagsToViewManagers.put(tag, mRootViewManager); // 第一处
mRootTags.put(tag, true);
view.setId(tag);
}
第二处创建 RN 页面的子元素时:
public synchronized void createView(
ThemedReactContext themedContext,
int tag,
String className,
@Nullable ReactStylesDiffMap initialProps) {
UiThreadUtil.assertOnUiThread();
SystraceMessage.beginSection(
Systrace.TRACE_TAG_REACT_VIEW, "NativeViewHierarchyManager_createView")
.arg("tag", tag)
.arg("className", className)
.flush();
try {
ViewManager viewManager = mViewManagers.get(className);
View view = viewManager.createView(themedContext, null, null, mJSResponderHandler);
mTagsToViews.put(tag, view);
mTagsToViewManagers.put(tag, viewManager); // 第二处
// Use android View id field to store React tag. This is possible since we don't inflate
// React views from layout xmls. Thus it is easier to just reuse that field instead of
// creating another (potentially much more expensive) mapping from view to React tag
view.setId(tag);
if (initialProps != null) {
viewManager.updateProperties(view, initialProps);
}
} finally {
Systrace.endSection(Systrace.TRACE_TAG_REACT_VIEW);
}
}
mTagsToViewManagers 中删除元素的地方,只有一处:
protected synchronized void dropView(View view) {
UiThreadUtil.assertOnUiThread();
......
ViewManager viewManager = mTagsToViewManagers.get(view.getId());
if (view instanceof ViewGroup && viewManager instanceof ViewGroupManager) {
ViewGroup viewGroup = (ViewGroup) view;
ViewGroupManager viewGroupManager = (ViewGroupManager) viewManager;
for (int i = viewGroupManager.getChildCount(viewGroup) - 1; i >= 0; i--) {
View child = viewGroupManager.getChildAt(viewGroup, i);
if (child == null) {
FLog.e(TAG, "Unable to drop null child view");
} else if (mTagsToViews.get(child.getId()) != null) {
dropView(child);
}
}
viewGroupManager.removeAllViews(viewGroup);
}
mTagsToPendingIndicesToDelete.remove(view.getId());
mTagsToViews.remove(view.getId());
mTagsToViewManagers.remove(view.getId());
}
所以:创建 RN 页面的根布局和子元素,都会把对应的 ViewManager 添加进来,tag 作为对应的key,移除根布局和删除子元素时也会从 mTagsToViewManagers 移除掉。
猜想可不可以根据 tag 对应的数值,找到是哪个 view 出问题了呢?然后查看源码发现 RootView 对应的 tag 数值是在 Native端生成的,如下:
public <T extends View> int addRootView(
final T rootView, WritableMap initialProps, @Nullable String initialUITemplate) {
Systrace.beginSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, "UIManagerModule.addRootView");
final int tag = ReactRootViewTagGenerator.getNextRootViewTag(); // rootview 对应的tag
final ReactApplicationContext reactApplicationContext = getReactApplicationContext();
final ThemedReactContext themedRootContext =
new ThemedReactContext(reactApplicationContext, rootView.getContext());
mUIImplementation.registerRootView(rootView, tag, themedRootContext);
Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE);
return tag;
}
public class ReactRootViewTagGenerator {
// Keep in sync with ReactIOSTagHandles JS module - see that file for an explanation on why the
// increment here is 10.
private static final int ROOT_VIEW_TAG_INCREMENT = 10;
private static int sNextRootViewTag = 1;
public static synchronized int getNextRootViewTag() {
final int tag = sNextRootViewTag;
sNextRootViewTag += ROOT_VIEW_TAG_INCREMENT;
return tag;
}
}
由源码可知:RootView对应的tag,一定是1结尾,每次创建会按照10去递增,如:11,21......
再次查看“ViewManager for tag XXX could not be found”的日志发现,里面的tag,都是以1结尾,且没有11都是除11之外的其他数值,这说明此问题肯定不是首次加载RN页面出现的,应该是重复加载导致的;
再次分析 Sentry上的日志信息和路径信息,发现此异常抛出还有一个共同特点,连续打开两次RN页面,且中间出现了一下“state: foreground”的标识,由此可以推断出,异常触发路径应该是,首次进入RN页面,触发退到后台,二次打开RN页面
然后,按照推荐各种尝试复现,终于在RN页面,点击Android手机上的分屏操作,稳定复现了此问题
原因
一般问题找到了稳定复现路径,解决它就是轻而易举的事情了。
为什么点击分屏,会触发RN页面崩溃,抛出“ViewManager for tag XXX could not be found”的异常呢?
查看代码发现,此次版本上,同事在改动其他需求时,把在RN页面onDestroy时需要销毁的一个对象,注释掉了,也就导致,onDestroy时,该对象没有被销毁。而点击分屏后,会触发RN页面重新创建,该对象还是之前的实例,导致RN框架里面的实例也是同一套,然后然后就崩溃了,详细原因见下面底层分析。
底层分析
这里就得分析一下点击分屏操作时,Native和RN页面的生命周期了,见下面流程图:
梳理一下关键点:
- 首次进入RN页面,对应的RootView,tag数值是11
- 点击分屏时,先触发Native端onDestroy,会把tag是11的RootView清除,清除时创建了对象RemoveRootViewOperation;
- 紧接着,RN页面的Activity重新创建,但是RootView的实例还是同一个,tag数值因为自加计算数值变成21;
- 然后UIViewOperationQueue中的flushPendingBatches(帧刷新回调此方法)方法中会遍历执行mOperations,该集合中包含ManageChildenOperation(管理增加/删除元素)和上次onDestroy时创建的RemoveRootViewOperation,所以RemoveRootViewOperation会把rootview删除,导致后续操作异常。
总结
带着问题分析RN源码,效率还是挺高的,把这次定位问题的过程记录下来,对学习RN也是有很大帮助的。大家有遇到过哪些RN疑难杂症,可以一起讨论~
转载自:https://juejin.cn/post/7132677636211343390