mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
Delete jsi::Functions before jsi::Runtime gets deleted
Summary: ## The Problem 1. `CatalystInstanceImpl` indirectly holds on to the `jsi::Runtime`. When you destroy `CatalystInstanceImpl`, you destroy the `jsi::Runtime`. As a part of reloading React Native, we destroy and re-create `CatalystInstanceImpl`, which destroys and re-creates the `jsi::Runtime`. 2. When JS passes in a callback to a TurboModule method, we take that callback (a `jsi::Function`) and wrap it in a Java `Callback` (implemented by `JCxxCallbackImpl`). This Java `Callback`, when executed, schedules the `jsi::Function` to be invoked on a Java thread at a later point in time. **Note:** The Java NativeModule can hold on to the Java `Callback` (and, by transitivity, the `jsi::Function`) for potentially forever. 3. It is a requirement of `jsi::Runtime` that all objects associated with the Runtime (ex: `jsi::Function`) must be destroyed before the Runtime itself is destroyed. See: https://fburl.com/m3mqk6wt ### jsi.h ``` /// .................................................... In addition, to /// make shutdown safe, destruction of objects associated with the Runtime /// must be destroyed before the Runtime is destroyed, or from the /// destructor of a managed HostObject or HostFunction. Informally, this /// means that the main source of unsafe behavior is to hold a jsi object /// in a non-Runtime-managed object, and not clean it up before the Runtime /// is shut down. If your lifecycle is such that avoiding this is hard, /// you will probably need to do use your own locks. class Runtime { public: virtual ~Runtime(); ``` Therefore, when you delete `CatalystInstanceImpl`, you could end up with a situation where the `jsi::Runtime` is destroyed before all `jsi::Function`s are destroyed. In dev, this leads the program to crash when you reload the app after having used a TurboModule method that uses callbacks. ## The Solution If the only reference to a `HostObject` or a `HostFunction` is in the JS Heap, then the `HostObject` and `HostFunction` destructors can destroy JSI objects. The TurboModule cache is the only thing, aside from the JS Heap, that holds a reference to all C++ TurboModules. But that cache (and the entire native side of `TurboModuleManager`) is destroyed when we call `mHybridData.resetNative()` in `TurboModuleManager.onCatalystInstanceDestroy()` in D16552730. (I verified this by commenting out `mHybridData.resetNative()` and placing a breakpoint in the destructor of `JavaTurboModule`). So, when we're cleaning up `TurboModuleManager`, the only reference to a Java TurboModule is the JS Heap. Therefore, it's safe and correct for us to destroy all `jsi::Function`s created by the Java TurboModule in `~JavaTurboModule`. So, in this diff, I keep a set of all `CallbackWrappers`, and explicitly call `destroy()` on them in the `JavaTurboModule` destructor. Note that since `~JavaTurboModule` accesses `callbackWrappers_`, it must be executed on the JS Thread, since `createJavaCallbackFromJSIFunction` also accesses `callbackWrappers_` on the JS Thread. For additional safety, I also eagerly destroyed the `jsi::Function` after it's been invoked once. I'm not yet sure if we only want JS callbacks to only ever be invoked once. So, I've created a Task to document this work: T48128233. Reviewed By: mdvacca Differential Revision: D16623340 fbshipit-source-id: 3a4c3efc70b9b3c8d329f19fdf4b4423c489695b
This commit is contained in:
committed by
Facebook Github Bot
parent
7c0b73501e
commit
bc7c85f153
@@ -23,6 +23,7 @@ import com.facebook.react.bridge.queue.ReactQueueConfigurationImpl;
|
||||
import com.facebook.react.bridge.queue.ReactQueueConfigurationSpec;
|
||||
import com.facebook.react.common.ReactConstants;
|
||||
import com.facebook.react.common.annotations.VisibleForTesting;
|
||||
import com.facebook.react.config.ReactFeatureFlags;
|
||||
import com.facebook.react.module.annotations.ReactModule;
|
||||
import com.facebook.react.turbomodule.core.JSCallInvokerHolderImpl;
|
||||
import com.facebook.react.turbomodule.core.interfaces.TurboModule;
|
||||
@@ -359,24 +360,56 @@ public class CatalystInstanceImpl implements CatalystInstance {
|
||||
listener.onBridgeDestroyed();
|
||||
}
|
||||
}
|
||||
AsyncTask.execute(
|
||||
new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
// Kill non-UI threads from neutral third party
|
||||
// potentially expensive, so don't run on UI thread
|
||||
|
||||
// contextHolder is used as a lock to guard against other users of the JS VM
|
||||
// having
|
||||
// the VM destroyed underneath them, so notify them before we resetNative
|
||||
mJavaScriptContextHolder.clear();
|
||||
final JSIModule turboModuleManager =
|
||||
ReactFeatureFlags.useTurboModules
|
||||
? mJSIModuleRegistry.getModule(JSIModuleType.TurboModuleManager)
|
||||
: null;
|
||||
|
||||
mHybridData.resetNative();
|
||||
getReactQueueConfiguration().destroy();
|
||||
Log.d(ReactConstants.TAG, "CatalystInstanceImpl.destroy() end");
|
||||
ReactMarker.logMarker(ReactMarkerConstants.DESTROY_CATALYST_INSTANCE_END);
|
||||
}
|
||||
});
|
||||
getReactQueueConfiguration()
|
||||
.getJSQueueThread()
|
||||
.runOnQueue(
|
||||
new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
// We need to destroy the TurboModuleManager on the JS Thread
|
||||
if (turboModuleManager != null) {
|
||||
turboModuleManager.onCatalystInstanceDestroy();
|
||||
}
|
||||
|
||||
getReactQueueConfiguration()
|
||||
.getUIQueueThread()
|
||||
.runOnQueue(
|
||||
new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
// AsyncTask.execute must be executed from the UI Thread
|
||||
AsyncTask.execute(
|
||||
new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
// Kill non-UI threads from neutral third party
|
||||
// potentially expensive, so don't run on UI thread
|
||||
|
||||
// contextHolder is used as a lock to guard against
|
||||
// other users of the JS VM having the VM destroyed
|
||||
// underneath them, so notify them before we reset
|
||||
// Native
|
||||
mJavaScriptContextHolder.clear();
|
||||
|
||||
mHybridData.resetNative();
|
||||
getReactQueueConfiguration().destroy();
|
||||
Log.d(
|
||||
ReactConstants.TAG,
|
||||
"CatalystInstanceImpl.destroy() end");
|
||||
ReactMarker.logMarker(
|
||||
ReactMarkerConstants.DESTROY_CATALYST_INSTANCE_END);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -32,7 +32,15 @@ public class JSIModuleRegistry {
|
||||
}
|
||||
|
||||
public void notifyJSInstanceDestroy() {
|
||||
for (JSIModuleHolder moduleHolder : mModules.values()) {
|
||||
for (Map.Entry<JSIModuleType, JSIModuleHolder> entry : mModules.entrySet()) {
|
||||
JSIModuleType moduleType = entry.getKey();
|
||||
|
||||
// Don't call TurboModuleManager.onCatalystInstanceDestroy
|
||||
if (moduleType == JSIModuleType.TurboModuleManager) {
|
||||
continue;
|
||||
}
|
||||
|
||||
JSIModuleHolder moduleHolder = entry.getValue();
|
||||
moduleHolder.notifyJSInstanceDestroy();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user