使用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分支

相关推荐
冯志浩9 小时前
React Native 中 `useMemo` 的使用与优化
react native·掘金·金石计划
冯志浩2 天前
React Native 中的 useRef 介绍
react native·掘金·金石计划
木西2 天前
React Native DApp 开发全栈实战·从 0 到 1 系列(钱包全链路)
react native·web3
namehu3 天前
🚀 2025 年最新教程:在 React Native 0.70+ 项目中接入 react-native-wechat-lib
react native·微信·taro
章丸丸5 天前
Tube - tRPC setup
react native·全栈
麦客奥德彪5 天前
React native 项目函数式编程的背后-另类的架构InversifyJS 依赖注入(DI)
react native·架构·客户端
冯志浩6 天前
React Native 中 useEffect 的使用
react native·掘金·金石计划
青红光硫化黑7 天前
React-native之组件
javascript·react native·react.js
冰冷的bin7 天前
【React Native】自定义跑马灯组件Marquee
react native