React Native解决跳转新RN容器之后返回上级页面Modal不显示问题

一、问题描述

React Native工程,如果上级页面A是一个RN页面并且此页面中有一个Modal弹层,下级页面B是通过路由的方式跳转,由一个原生RN容器去承载一个新的RN页面。这个时候如果跳转下级页面B,再次返回上级页面A你会发现点击不再显示Modal弹窗。

二、原因分析

我们可以先看一下Modal弹层的原生实现ReactModalHostView.java,关键代码分析

  /**
   * showOrUpdate will display the Dialog. It is called by the manager once all properties are set
   * because we need to know all of them before creating the Dialog. It is also smart during updates
   * if the changed properties can be applied directly to the Dialog or require the recreation of a
   * new Dialog.
   */
  protected void showOrUpdate() {
    // If the existing Dialog is currently up, we may need to redraw it or we may be able to update
    // the property without having to recreate the dialog
    if (mDialog != null) {
      if (mPropertyRequiresNewDialog) {
        dismiss();
      } else {
        updateProperties();
        return;
      }
    }

    // Reset the flag since we are going to create a new dialog
    mPropertyRequiresNewDialog = false;
    int theme = R.style.Theme_FullScreenDialog;
    if (mAnimationType.equals("fade")) {
      theme = R.style.Theme_FullScreenDialogAnimatedFade;
    } else if (mAnimationType.equals("slide")) {
      theme = R.style.Theme_FullScreenDialogAnimatedSlide;
    }
    Activity currentActivity = getCurrentActivity();
    Context context = currentActivity == null ? getContext() : currentActivity;
    mDialog = new Dialog(context, theme);
    mDialog
        .getWindow()
        .setFlags(
            WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
            WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE);

    mDialog.setContentView(getContentView());
    updateProperties();

    mDialog.setOnShowListener(mOnShowListener);
    mDialog.setOnKeyListener(
        new DialogInterface.OnKeyListener() {
          @Override
          public boolean onKey(DialogInterface dialog, int keyCode, KeyEvent event) {
            if (event.getAction() == KeyEvent.ACTION_UP) {
              // We need to stop the BACK button from closing the dialog by default so we capture
              // that
              // event and instead inform JS so that it can make the decision as to whether or not
              // to
              // allow the back button to close the dialog.  If it chooses to, it can just set
              // visible
              // to false on the Modal and the Modal will go away
              if (keyCode == KeyEvent.KEYCODE_BACK) {
                Assertions.assertNotNull(
                    mOnRequestCloseListener,
                    "setOnRequestCloseListener must be called by the manager");
                mOnRequestCloseListener.onRequestClose(dialog);
                return true;
              } else {
                // We redirect the rest of the key events to the current activity, since the
                // activity
                // expects to receive those events and react to them, ie. in the case of the dev
                // menu
                Activity currentActivity = ((ReactContext) getContext()).getCurrentActivity();
                if (currentActivity != null) {
                  return currentActivity.onKeyUp(keyCode, event);
                }
              }
            }
            return false;
          }
        });

    mDialog.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);
    if (mHardwareAccelerated) {
      mDialog.getWindow().addFlags(WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED);
    }
    if (currentActivity != null && !currentActivity.isFinishing()) {
      mDialog.show();
      if (context instanceof Activity) {
        mDialog
            .getWindow()
            .getDecorView()
            .setSystemUiVisibility(
                ((Activity) context).getWindow().getDecorView().getSystemUiVisibility());
      }
      mDialog.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE);
    }
  }

注意到有以下一段逻辑,获取当前ReactContext所属Activity

  private @Nullable Activity getCurrentActivity() {
    return ((ReactContext) getContext()).getCurrentActivity();
  }
  /**
   * Get the activity to which this context is currently attached, or {@code null} if not attached.
   * DO NOT HOLD LONG-LIVED REFERENCES TO THE OBJECT RETURNED BY THIS METHOD, AS THIS WILL CAUSE
   * MEMORY LEAKS.
   */
  public @Nullable Activity getCurrentActivity() {
    if (mCurrentActivity == null) {
      return null;
    }
    return mCurrentActivity.get();
  }

那么我们可以猜测,没有显示出来大概率原因和这个Activity有关系,获取到的Activity并不是当前显示的Activity!走断点!果然和我想的一致,拿到的是新页面B容器的Activity,那么为啥拿到的Activity不是当前的显示的Activity! 追溯代码查看mCurrentActivity被赋值的地方

  /** Should be called by the hosting Fragment in {@link Fragment#onResume} */
  public void onHostResume(@Nullable Activity activity) {
    mLifecycleState = LifecycleState.RESUMED;
    mCurrentActivity = new WeakReference(activity);
    ReactMarker.logMarker(ReactMarkerConstants.ON_HOST_RESUME_START);
    for (LifecycleEventListener listener : mLifecycleEventListeners) {
      try {
        listener.onHostResume();
      } catch (RuntimeException e) {
        handleException(e);
      }
    }
    ReactMarker.logMarker(ReactMarkerConstants.ON_HOST_RESUME_END);
  }

  public void onNewIntent(@Nullable Activity activity, Intent intent) {
    UiThreadUtil.assertOnUiThread();
    mCurrentActivity = new WeakReference(activity);
    for (ActivityEventListener listener : mActivityEventListeners) {
      try {
        listener.onNewIntent(intent);
      } catch (RuntimeException e) {
        handleException(e);
      }
    }
  }

onNewIntent这边的赋值我们可以不用关心,因为我们是新打开页面不会走到这里,所以我们关注onHostResume被调用的地方,最后可以看到会在ReactInstanceManager.java的moveToResumedLifecycleState方法被调用

  private synchronized void moveToResumedLifecycleState(boolean force) {
    ReactContext currentContext = getCurrentReactContext();
    if (currentContext != null) {
      // we currently don't have an onCreate callback so we call onResume for both transitions
      if (force
          || mLifecycleState == LifecycleState.BEFORE_RESUME
          || mLifecycleState == LifecycleState.BEFORE_CREATE) {
        currentContext.onHostResume(mCurrentActivity);
      }
    }
    mLifecycleState = LifecycleState.RESUMED;
  }

而这个方法最终会被RN容器的onResume方法调用,分析到这里大致可以猜到原因了,就是我在页面B回页面A的时候,调用A容器的onResume方法时并没有去赋值,因为下面的条件没有过,此时的mLifecycleState还是 RESUMED状态

if (force
    || mLifecycleState == LifecycleState.BEFORE_RESUME
    || mLifecycleState == LifecycleState.BEFORE_CREATE) {
  currentContext.onHostResume(mCurrentActivity);
}

我们继续追溯ReactInstanceManager.java中的onHostDestroy方法,看看页面B销毁的时候做了哪些事情

  /**
   * Call this from {@link Activity#onDestroy()}. This notifies any listening modules so they can do
   * any necessary cleanup.
   *
   * @deprecated use {@link #onHostDestroy(Activity)} instead
   */
  @ThreadConfined(UI)
  public void onHostDestroy() {
    UiThreadUtil.assertOnUiThread();

    if (mUseDeveloperSupport) {
      mDevSupportManager.setDevSupportEnabled(false);
    }

    moveToBeforeCreateLifecycleState();
    mCurrentActivity = null;
  }

  private synchronized void moveToBeforeCreateLifecycleState() {
    ReactContext currentContext = getCurrentReactContext();
    if (currentContext != null) {
      if (mLifecycleState == LifecycleState.RESUMED) {
        currentContext.onHostPause();
        mLifecycleState = LifecycleState.BEFORE_RESUME;
      }
      if (mLifecycleState == LifecycleState.BEFORE_RESUME) {
        currentContext.onHostDestroy();
      }
    }
    mLifecycleState = LifecycleState.BEFORE_CREATE;
  }

从代码中可以看到,在销毁的时候会去重置生命周期状态mLifecycleState值为BEFORE_CREATE,那么结合前面分析的代码,理应在执行moveToResumedLifecycleState方法时,那边的判断条件中mLifecycleState 的值为BEFORE_CREATE,进入判断条件,重新赋值mCurrentActivity为页面A的Activity才对!我们在RN的容器的基类onResume方法和onDestroy方法打入断点,观察执行顺序,发现原因:在页面B回到页面A时,首先会去执行页面A的onResume方法,再尔才会去执行页面B的onDestroy方法,所以导致当执行moveToResumedLifecycleState方法时,此时的生命周期状态mLifecycleState还没有被重置为 BEFORE_CREATE,所以就不会进入重新赋值mCurrentActivity的逻辑,在页面A点击事件显示Modal时,ReactModalHostView.java获取到的Activity不是当前Activity,而是上次赋值的页面B所在的Activity,所以就不会显示Modal

三、解决问题

分析上述代码,可以得知Modal显示不出来是RN容器生命周期执行顺序的问题导致,那么我们唯一要做的处理就是保证页面B的onDestroy方法先于页面A的onResume方法就可以解决此问题!我的处理是在页面B容器执行finish方法之前或者点击RN容器根视图返回时去执行RN代理方法的onDestroy,这样就保证了页面B的onDestroy方法先于页面A的onResume方法!

//由于onResume方法会先于onDestroy执行,所以在执行finish()之前就去执行onDestroy
getReactDelegate().onDestroy();
this.finish();
public boolean onBackPressed() {
    Activity rootActivity = ActivityRegisterUtil.getCurrentActivity();
    if (getReactNativeHost().hasInstance()) {
        if (rootActivity != null && !rootActivity.isFinishing() && rootActivity instanceof XReactActivity) {
            String rootPageName = ((XReactActivity) rootActivity).getPageName();
            //判断是否是RN容器根视图,如果是则执行原生返回,不是则执行RN返回事件
            ArrayList pageNameList = ((XReactActivity) rootActivity).getPageNameList();
            if (pageNameList.size() == 1 && rootPageName.equals(pageNameList.get(0))) {
                //由于onResume方法会先于onDestroy执行,所以在执行finish()之前就去执行onDestroy
                onDestroy();
                return false;
            } else {
                getReactNativeHost().getReactInstanceManager().onBackPressed();
                return true;
            }
        }
        getReactNativeHost().getReactInstanceManager().onBackPressed();
        return true;
    }
    return false;
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,254评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,875评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,682评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,896评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,015评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,152评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,208评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,962评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,388评论 1 304
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,700评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,867评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,551评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,186评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,901评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,142评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,689评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,757评论 2 351