使用0.75.5版本ReactFragment开启新架构后的问题

背景

由于某些场景下需要使用到Fragment,比如说首页上的某个Tab要用React Native来实现,所以就按照官方文档接入了ReactFragment,但是由于在新架构下,ReactFragment存在一些Bug,导致运行起来后会Crash,下面具体来看下遇到的问题。

问题一

Crash堆栈

css 复制代码
java.lang.NullPointerException: Attempt to invoke interface method 'com.facebook.react.interfaces.fabric.ReactSurface com.facebook.react.ReactHost.createSurface(android.content.Context, java.lang.String, android.os.Bundle)' on a null object reference
    at com.facebook.react.ReactDelegate.loadApp(ReactDelegate.java:283)
    at com.facebook.react.ReactDelegate.loadApp(ReactDelegate.java:275)
    at com.facebook.react.ReactFragment.onCreateView(ReactFragment.java:95)
    at androidx.fragment.app.Fragment.performCreateView(Fragment.java:2963)
    at androidx.fragment.app.FragmentStateManager.createView(FragmentStateManager.java:518)
    at androidx.fragment.app.FragmentStateManager.moveToExpectedState(FragmentStateManager.java:282)
    at androidx.fragment.app.FragmentManager.executeOpsTogether(FragmentManager.java:2189)
    at androidx.fragment.app.FragmentManager.removeRedundantOperationsAndExecute(FragmentManager.java:2100)
    at androidx.fragment.app.FragmentManager.execPendingActions(FragmentManager.java:2002)
    at androidx.fragment.app.FragmentManager$5.run(FragmentManager.java:524)
    at android.os.Handler.handleCallback(Handler.java:942)
    at android.os.Handler.dispatchMessage(Handler.java:99)
    at android.os.Looper.loopOnce(Looper.java:240)
    at android.os.Looper.loop(Looper.java:351)
    at android.app.ActivityThread.main(ActivityThread.java:8423)
    at java.lang.reflect.Method.invoke(Native Method)
    at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:584)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1013)

问题定位

先来看loadApp方法的实现:

kotlin 复制代码
public void loadApp(String appKey) {
  // With Bridgeless enabled, create and start the surface
  if (ReactFeatureFlags.enableBridgelessArchitecture) {
    if (mReactSurface == null) {
      // Create a ReactSurface
      mReactSurface = mReactHost.createSurface(mActivity, appKey, mLaunchOptions);
      // Set main Activity's content view
      mActivity.setContentView(mReactSurface.getView());
    }
    mReactSurface.start();
  } else {
    if (mReactRootView != null) {
      throw new IllegalStateException("Cannot loadApp while app is already running.");
    }
    mReactRootView = createRootView();
    mReactRootView.startReactApplication(
        getReactNativeHost().getReactInstanceManager(), appKey, mLaunchOptions);
  }
}

结合Crash堆栈,可以推断出mReactHost为null,所以再来看下mReactHost是如何赋值的。看了ReactDelegate的代码,发现mReactHost只有在其中一个构造方法里才会赋值。接下来就来看下ReactFragment是如何创建ReactDelegate的。

kotlin 复制代码
// region Lifecycle
@Override
public void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  String mainComponentName = null;
  Bundle launchOptions = null;
  Boolean fabricEnabled = null;
  if (getArguments() != null) {
    mainComponentName = getArguments().getString(ARG_COMPONENT_NAME);
    launchOptions = getArguments().getBundle(ARG_LAUNCH_OPTIONS);
    fabricEnabled = getArguments().getBoolean(ARG_FABRIC_ENABLED);
  }
  if (mainComponentName == null) {
    throw new IllegalStateException("Cannot loadApp if component name is null");
  }
  mReactDelegate =
      new ReactDelegate(
          getActivity(), getReactNativeHost(), mainComponentName, launchOptions, fabricEnabled);
}

ReactFragment里只有一处地方创建了ReactDelegate,但是它调用的构造方法是针对旧架构的,并不会给mReactHost赋值。看到这里基本上知道问题原因了,React Native在支持新架构后,遗漏了ReactFragment里创建ReactDelegate时对于新旧架构的区分处理。

问题解决

在新架构下,需要调用对应的ReactDelegate构造方法赋值给mReactDelegate,由于mReactDelegate是protected,很自然就想到继承ReactFragment,然后重写onCreate方法,新增创建新架构对应ReactDelegate的逻辑。代码如下:

kotlin 复制代码
class CustomReactFragment : ReactFragment() {

    private val reactHost: ReactHost?
        get() = (activity?.application as ReactApplication?)?.reactHost

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        if (ReactFeatureFlags.enableBridgelessArchitecture) {
            var mainComponentName: String? = null
            var launchOptions: Bundle? = null

            arguments?.let { arguments ->
                mainComponentName = arguments.getString(ARG_COMPONENT_NAME)
                launchOptions = arguments.getBundle(ARG_LAUNCH_OPTIONS)
            }

            mReactDelegate =
                ReactDelegate(activity, reactHost, mainComponentName, launchOptions)
        }
    }

    class Builder : ReactFragment.Builder() {
        override fun build(): CustomReactFragment = newInstance(mComponentName, mLaunchOptions, mFabricEnabled)
    }

    companion object {
        private fun newInstance(
            componentName: String?,
            launchOptions: Bundle?,
            fabricEnabled: Boolean?,
        ): CustomReactFragment {
            val args =
                Bundle().apply {
                    putString(ARG_COMPONENT_NAME, componentName)
                    putBundle(ARG_LAUNCH_OPTIONS, launchOptions)
                    putBoolean(ARG_FABRIC_ENABLED, fabricEnabled ?: false)
                }
            return CustomReactFragment().apply { setArguments(args) }
        }
    }
}

问题二

Crash堆栈

less 复制代码
java.lang.IllegalStateException: The specified child already has a parent. You must call removeView() on the child's parent first.
    at android.view.ViewGroup.addViewInner(ViewGroup.java:5342)
    at android.view.ViewGroup.addView(ViewGroup.java:5163)
    at android.view.ViewGroup.addView(ViewGroup.java:5103)
    at androidx.fragment.app.FragmentStateManager.addViewToContainer(FragmentStateManager.java:833)
    at androidx.fragment.app.FragmentStateManager.createView(FragmentStateManager.java:523)
    at androidx.fragment.app.FragmentStateManager.moveToExpectedState(FragmentStateManager.java:282)
    at androidx.fragment.app.FragmentManager.executeOpsTogether(FragmentManager.java:2189)
    at androidx.fragment.app.FragmentManager.removeRedundantOperationsAndExecute(FragmentManager.java:2100)
    at androidx.fragment.app.FragmentManager.execPendingActions(FragmentManager.java:2002)
    at androidx.fragment.app.FragmentManager$5.run(FragmentManager.java:524)
    at android.os.Handler.handleCallback(Handler.java:942)
    at android.os.Handler.dispatchMessage(Handler.java:99)
    at android.os.Looper.loopOnce(Looper.java:240)
    at android.os.Looper.loop(Looper.java:351)
    at android.app.ActivityThread.main(ActivityThread.java:8423)
    at java.lang.reflect.Method.invoke(Native Method)
    at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:584)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1013)

问题定位

当第一个问题解决后,随之而来的就是这个Crash。

从这个Crash堆栈我们可以知道这个问题是ReactFragment的View往上级ViewGroup添加时,发现这个View已经有上级ViewGroup了,无法再次添加。

那我们首先看下ReactFragment的View是怎么获取的,具体看ReactFragment里onCreateView方法的代码:

java 复制代码
@Override
public View onCreateView(
    @NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
  mReactDelegate.loadApp();
  return mReactDelegate.getReactRootView();
}

接下来看ReactDelegate里getReactRootView方法的实现:

java 复制代码
public ReactRootView getReactRootView() {
  if (ReactFeatureFlags.enableBridgelessArchitecture) {
    return (ReactRootView) mReactSurface.getView();
  } else {
    return mReactRootView;
  }
}

因为我们走的是Bridgeless模式,所以看下ReactSurfaceImpl里getView方法的实现:

java 复制代码
@Override
public @Nullable ViewGroup getView() {
  return mSurfaceView.get();
}

所以ReactFragment里onCreateView方法返回的就是这个View。那我们在这里打个断点,看下是哪里提前获取了这个View并添加到了某个ViewGroup中。经过调试,发现调用的堆栈如下:

less 复制代码
Breakpoint reached
    at com.facebook.react.runtime.ReactSurfaceImpl.getView(ReactSurfaceImpl.java:139)
    at com.facebook.react.ReactDelegate.loadApp(ReactDelegate.java:285)
    at com.facebook.react.ReactDelegate.loadApp(ReactDelegate.java:275)
    at com.facebook.react.ReactFragment.onCreateView(ReactFragment.java:95)
    at androidx.fragment.app.Fragment.performCreateView(Fragment.java:2963)
    at androidx.fragment.app.FragmentStateManager.createView(FragmentStateManager.java:518)
    at androidx.fragment.app.FragmentStateManager.moveToExpectedState(FragmentStateManager.java:282)
    at androidx.fragment.app.FragmentManager.executeOpsTogether(FragmentManager.java:2189)

从堆栈来看是ReactDelegate的laodApp方法获取了这个View,那我们再来看下获取View后是不是添加到了某个ViewGroup中。ReactDelegate的laodApp方法实现如下:

java 复制代码
public void loadApp(String appKey) {
  // With Bridgeless enabled, create and start the surface
  if (ReactFeatureFlags.enableBridgelessArchitecture) {
    if (mReactSurface == null) {
      // Create a ReactSurface
      mReactSurface = mReactHost.createSurface(mActivity, appKey, mLaunchOptions);
      // Set main Activity's content view
      mActivity.setContentView(mReactSurface.getView());
    }
    mReactSurface.start();
  } else {
    if (mReactRootView != null) {
      throw new IllegalStateException("Cannot loadApp while app is already running.");
    }
    mReactRootView = createRootView();
    mReactRootView.startReactApplication(
        getReactNativeHost().getReactInstanceManager(), appKey, mLaunchOptions);
  }
}

从这个代码可以很明显地看到这个View被Activity的setContentView方法添加到了视图上,所以也就导致了前述的Crash。

问题解决

很明显,这里Activity的setContentView方法是不应该被调用的。

为了不调用Activity的setContentView方法,很容易会想到和前面那个问题类似的解决方法,继承ReactDelegate,然后重写loadApp方法,但是这个方法实现里用到了很多private的变量,所以重写的时候无法直接调用,如果硬要这么实现就只能通过反射的方式来调用。如果考虑继承重写之外的方式,那还可以通过字节码修改来实现不调用Activity的setContentView方法。

但这两种方式实现成本都比较高,有没有比较简单的解决方案呢?前面的思路都是围绕不调用Activity的setContentView方法,那换个思路,如果让这个方法被调用,但实际不添加View呢?按照这个思路,我们只需要重写Activity的setContentView方法就行了。考虑到ReactSurfaceImpl里getView方法返回的View一定是ReactSurfaceView类型的,我们就可以以此为判断条件来决定是否要执行添加View的逻辑,具体代码如下:

kotlin 复制代码
override fun setContentView(view: View?) {
    if (view is ReactSurfaceView) {
        return
    }
    super.setContentView(view)
}

一般项目中都会有基类的Activity,将上述代码加入到基类Activity即可。

相关代码

Github上Demo项目的kotlin-version分支

相关推荐
一头小鹿20 小时前
【React Native+Appwrite】获取数据时的分页机制
前端·react native
XiaoSong20 小时前
基于 React Native/Expo 项目的持续集成(CI)最佳实践配置指南
前端·react native·react.js
VisuperviReborn1 天前
React Native 与 iOS 原生通信:从理论到实践
前端·react native·前端框架
冰冷的bin2 天前
【React Native】粘性布局StickyScrollView
react native
chenbin___3 天前
react native中 createAsyncThunk 的详细说明,及用法示例(转自通义千问)
javascript·react native·react.js
前端拿破轮5 天前
ReactNative从入门到性能优化(一)
前端·react native·客户端
ideaout技术团队7 天前
android集成react native组件踩坑笔记(Activity局部展示RN的组件)
android·javascript·笔记·react native·react.js
洞窝技术8 天前
前端开发APP之跨平台开发(ReactNative0.74.5)
android·react native·ios
光影少年8 天前
React Native 第三章
javascript·react native·react.js