likes
comments
collection
share

记录 Android 上 偶现 React Native - ViewManager for tag XXX could not be found 问题

作者站长头像
站长
· 阅读数 13

背景

项目在某一个版本上的 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;
}

该方法被调用的地方有:

  1. manageChildren时,view显示/隐藏时,例如数据返回前显示子元素a,返回后隐藏子元素a;
  2. setChildren时,简化版本的manageChildren,增加子view时使用;
  3. dropView时,删除rootview或者子view;
  4. updateProperties时,更新view时;
  5. updateViewExtraData时,更新view样式;
  6. 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页面的生命周期了,见下面流程图:

记录 Android 上 偶现 React Native - ViewManager for tag XXX could not be found 问题 梳理一下关键点:

  1. 首次进入RN页面,对应的RootView,tag数值是11
  2. 点击分屏时,先触发Native端onDestroy,会把tag是11的RootView清除,清除时创建了对象RemoveRootViewOperation;
  3. 紧接着,RN页面的Activity重新创建,但是RootView的实例还是同一个,tag数值因为自加计算数值变成21;
  4. 然后UIViewOperationQueue中的flushPendingBatches(帧刷新回调此方法)方法中会遍历执行mOperations,该集合中包含ManageChildenOperation(管理增加/删除元素)和上次onDestroy时创建的RemoveRootViewOperation,所以RemoveRootViewOperation会把rootview删除,导致后续操作异常。

总结

带着问题分析RN源码,效率还是挺高的,把这次定位问题的过程记录下来,对学习RN也是有很大帮助的。大家有遇到过哪些RN疑难杂症,可以一起讨论~