背景
由于某些场景下需要使用到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即可。