一、问题描述
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;
}