From 797ca6c219b2a44f88f10c61d91e8cc21e2f306e Mon Sep 17 00:00:00 2001 From: Aaron Chiu Date: Fri, 23 Sep 2016 15:48:54 -0700 Subject: [PATCH] Add ability to lazy load Native Java Modules Summary: Utilizes the build time annotation processor ReactModuleSpecProcessor that creates ReactModuleInfos for modules annotated with ReactModule and listed in the ReactModuleList annotation of LazyReactPackages. This way we don't have to instantiate the native modules to get the name, canOverrideExistingModule, and supportsWebWorkers values of the native modules. In the NativeModuleRegistry, we either store these ReactModuleInfos inside of a ModuleHolder or if we can't get the ReactModuleInfo for a specific module we instantiate that module to get the values (as we previously did) to store in a LegacyModuleInfo. Reviewed By: astreet Differential Revision: D3796561 fbshipit-source-id: f8fb9b4993f59b51ce595eb2f2c3425129b28ce5 --- .../java/com/facebook/react/testing/BUCK | 1 + .../testing/ReactIntegrationTestCase.java | 5 +- .../react/testing/ReactTestHelper.java | 32 ++-- .../facebook/react/EagerModuleProvider.java | 24 +++ .../react/XReactInstanceManagerImpl.java | 97 ++++++++++- .../react/bridge/CatalystInstance.java | 2 +- .../react/bridge/LifecycleEventListener.java | 5 +- .../facebook/react/bridge/ReactContext.java | 31 +++- .../java/com/facebook/react/cxxbridge/BUCK | 3 +- .../react/cxxbridge/JavaModuleWrapper.java | 22 +-- .../react/cxxbridge/ModuleHolder.java | 159 ++++++++++++++++++ .../react/cxxbridge/NativeModuleRegistry.java | 112 ++++++------ .../com/facebook/react/module/model/Info.java | 17 ++ .../react/module/model/ReactModuleInfo.java | 33 +++- .../module/model/ReactModuleInfoProvider.java | 13 ++ .../processing/ReactModuleSpecProcessor.java | 60 ++++--- 16 files changed, 498 insertions(+), 118 deletions(-) create mode 100644 ReactAndroid/src/main/java/com/facebook/react/EagerModuleProvider.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/cxxbridge/ModuleHolder.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/module/model/Info.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/module/model/ReactModuleInfoProvider.java diff --git a/ReactAndroid/src/androidTest/java/com/facebook/react/testing/BUCK b/ReactAndroid/src/androidTest/java/com/facebook/react/testing/BUCK index 22336f925..e7d5eec31 100644 --- a/ReactAndroid/src/androidTest/java/com/facebook/react/testing/BUCK +++ b/ReactAndroid/src/androidTest/java/com/facebook/react/testing/BUCK @@ -16,6 +16,7 @@ android_library( react_native_target('java/com/facebook/react/common:common'), react_native_target('java/com/facebook/react/cxxbridge:bridge'), react_native_target('java/com/facebook/react/devsupport:devsupport'), + react_native_target('java/com/facebook/react/module/model:model'), react_native_target('java/com/facebook/react/modules/core:core'), react_native_target('java/com/facebook/react/modules/debug:debug'), react_native_target('java/com/facebook/react/shell:shell'), diff --git a/ReactAndroid/src/androidTest/java/com/facebook/react/testing/ReactIntegrationTestCase.java b/ReactAndroid/src/androidTest/java/com/facebook/react/testing/ReactIntegrationTestCase.java index 286da50a9..9be060b99 100644 --- a/ReactAndroid/src/androidTest/java/com/facebook/react/testing/ReactIntegrationTestCase.java +++ b/ReactAndroid/src/androidTest/java/com/facebook/react/testing/ReactIntegrationTestCase.java @@ -22,17 +22,16 @@ import android.view.ViewGroup; import com.facebook.infer.annotation.Assertions; import com.facebook.react.bridge.BaseJavaModule; -import com.facebook.react.bridge.ReactContext; -import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.CatalystInstance; import com.facebook.react.bridge.LifecycleEventListener; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactContext; import com.facebook.react.bridge.SoftAssertions; import com.facebook.react.bridge.UiThreadUtil; import com.facebook.react.common.ApplicationHolder; import com.facebook.react.common.futures.SimpleSettableFuture; import com.facebook.react.devsupport.DevSupportManager; import com.facebook.react.modules.core.Timing; - import com.facebook.soloader.SoLoader; import static org.mockito.Mockito.mock; diff --git a/ReactAndroid/src/androidTest/java/com/facebook/react/testing/ReactTestHelper.java b/ReactAndroid/src/androidTest/java/com/facebook/react/testing/ReactTestHelper.java index c701c2b83..46f636d25 100644 --- a/ReactAndroid/src/androidTest/java/com/facebook/react/testing/ReactTestHelper.java +++ b/ReactAndroid/src/androidTest/java/com/facebook/react/testing/ReactTestHelper.java @@ -10,6 +10,9 @@ package com.facebook.react.testing; import javax.annotation.Nullable; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import java.util.concurrent.Callable; import android.app.Instrumentation; @@ -18,30 +21,34 @@ import android.support.test.InstrumentationRegistry; import android.view.View; import android.view.ViewGroup; +import com.facebook.react.EagerModuleProvider; import com.facebook.react.ReactInstanceManager; import com.facebook.react.bridge.CatalystInstance; -import com.facebook.react.cxxbridge.CatalystInstanceImpl; -import com.facebook.react.cxxbridge.JSBundleLoader; -import com.facebook.react.cxxbridge.NativeModuleRegistry; -import com.facebook.react.cxxbridge.JSCJavaScriptExecutor; -import com.facebook.react.cxxbridge.JavaScriptExecutor; import com.facebook.react.bridge.JavaScriptModuleRegistry; +import com.facebook.react.bridge.ModuleSpec; import com.facebook.react.bridge.NativeModule; import com.facebook.react.bridge.NativeModuleCallExceptionHandler; import com.facebook.react.bridge.WritableNativeMap; import com.facebook.react.bridge.queue.ReactQueueConfigurationSpec; +import com.facebook.react.cxxbridge.CatalystInstanceImpl; +import com.facebook.react.cxxbridge.JSBundleLoader; +import com.facebook.react.cxxbridge.JSCJavaScriptExecutor; +import com.facebook.react.cxxbridge.JavaScriptExecutor; +import com.facebook.react.cxxbridge.NativeModuleRegistry; +import com.facebook.react.module.model.ReactModuleInfo; import com.android.internal.util.Predicate; public class ReactTestHelper { private static class DefaultReactTestFactory implements ReactTestFactory { private static class ReactInstanceEasyBuilderImpl implements ReactInstanceEasyBuilder { - private @Nullable Context mContext; - private final NativeModuleRegistry.Builder mNativeModuleRegistryBuilder = - new NativeModuleRegistry.Builder(); + + private final List mModuleSpecList = new ArrayList<>(); private final JavaScriptModuleRegistry.Builder mJSModuleRegistryBuilder = new JavaScriptModuleRegistry.Builder(); + private @Nullable Context mContext; + @Override public ReactInstanceEasyBuilder setContext(Context context) { mContext = context; @@ -49,8 +56,9 @@ public class ReactTestHelper { } @Override - public ReactInstanceEasyBuilder addNativeModule(NativeModule module) { - mNativeModuleRegistryBuilder.add(module); + public ReactInstanceEasyBuilder addNativeModule(NativeModule nativeModule) { + mModuleSpecList.add( + new ModuleSpec(nativeModule.getClass(), new EagerModuleProvider(nativeModule))); return this; } @@ -71,7 +79,9 @@ public class ReactTestHelper { return new CatalystInstanceImpl.Builder() .setReactQueueConfigurationSpec(ReactQueueConfigurationSpec.createDefault()) .setJSExecutor(executor) - .setRegistry(mNativeModuleRegistryBuilder.build()) + .setRegistry(new NativeModuleRegistry( + mModuleSpecList, + Collections.emptyMap())) .setJSModuleRegistry(mJSModuleRegistryBuilder.build()) .setJSBundleLoader(JSBundleLoader.createAssetLoader( mContext, diff --git a/ReactAndroid/src/main/java/com/facebook/react/EagerModuleProvider.java b/ReactAndroid/src/main/java/com/facebook/react/EagerModuleProvider.java new file mode 100644 index 000000000..3270ca340 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/EagerModuleProvider.java @@ -0,0 +1,24 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +package com.facebook.react; + +import javax.inject.Provider; + +import com.facebook.react.bridge.NativeModule; + +/** + * Provider for an already initialized and non-lazy NativeModule. + */ +public class EagerModuleProvider implements Provider { + + private final NativeModule mModule; + + public EagerModuleProvider(NativeModule module) { + mModule = module; + } + + @Override + public NativeModule get() { + return mModule; + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/XReactInstanceManagerImpl.java b/ReactAndroid/src/main/java/com/facebook/react/XReactInstanceManagerImpl.java index 239e2e5b3..3802b2b23 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/XReactInstanceManagerImpl.java +++ b/ReactAndroid/src/main/java/com/facebook/react/XReactInstanceManagerImpl.java @@ -14,8 +14,10 @@ import javax.annotation.Nullable; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; @@ -35,6 +37,7 @@ import com.facebook.react.bridge.CatalystInstance; import com.facebook.react.bridge.JavaJSExecutor; import com.facebook.react.bridge.JavaScriptModule; import com.facebook.react.bridge.JavaScriptModuleRegistry; +import com.facebook.react.bridge.ModuleSpec; import com.facebook.react.bridge.NativeModule; import com.facebook.react.bridge.NativeModuleCallExceptionHandler; import com.facebook.react.bridge.NotThreadSafeBridgeIdleDebugListener; @@ -61,6 +64,8 @@ import com.facebook.react.devsupport.DevSupportManager; import com.facebook.react.devsupport.DevSupportManagerFactory; import com.facebook.react.devsupport.ReactInstanceDevCommandsHandler; import com.facebook.react.devsupport.RedBoxHandler; +import com.facebook.react.module.model.ReactModuleInfo; +import com.facebook.react.module.model.ReactModuleInfoProvider; import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler; import com.facebook.react.modules.core.DeviceEventManagerModule; import com.facebook.react.modules.debug.DeveloperSettings; @@ -72,6 +77,7 @@ import com.facebook.react.uimanager.ViewManager; import com.facebook.react.views.imagehelper.ResourceDrawableIdHelper; import com.facebook.soloader.SoLoader; import com.facebook.systrace.Systrace; +import com.facebook.systrace.SystraceMessage; import static com.facebook.react.bridge.ReactMarkerConstants.BUILD_NATIVE_MODULE_REGISTRY_END; import static com.facebook.react.bridge.ReactMarkerConstants.BUILD_NATIVE_MODULE_REGISTRY_START; @@ -107,6 +113,8 @@ import static com.facebook.systrace.Systrace.TRACE_TAG_REACT_JAVA_BRIDGE; */ /* package */ class XReactInstanceManagerImpl extends ReactInstanceManager { + private static final String TAG = XReactInstanceManagerImpl.class.getSimpleName(); + /* should only be accessed from main thread (UI thread) */ private final List mAttachedRootViews = new ArrayList<>(); private LifecycleState mLifecycleState; @@ -836,7 +844,8 @@ import static com.facebook.systrace.Systrace.TRACE_TAG_REACT_JAVA_BRIDGE; FLog.i(ReactConstants.TAG, "Creating react context."); ReactMarker.logMarker(CREATE_REACT_CONTEXT_START); mSourceUrl = jsBundleLoader.getSourceUrl(); - NativeModuleRegistry.Builder nativeRegistryBuilder = new NativeModuleRegistry.Builder(); + List moduleSpecs = new ArrayList<>(); + Map reactModuleInfoMap = new HashMap<>(); JavaScriptModuleRegistry.Builder jsModulesBuilder = new JavaScriptModuleRegistry.Builder(); final ReactApplicationContext reactContext = new ReactApplicationContext(mApplicationContext); @@ -851,7 +860,12 @@ import static com.facebook.systrace.Systrace.TRACE_TAG_REACT_JAVA_BRIDGE; try { CoreModulesPackage coreModulesPackage = new CoreModulesPackage(this, mBackBtnHandler, mUIImplementationProvider); - processPackage(coreModulesPackage, reactContext, nativeRegistryBuilder, jsModulesBuilder); + processPackage( + coreModulesPackage, + reactContext, + moduleSpecs, + reactModuleInfoMap, + jsModulesBuilder); } finally { Systrace.endSection(TRACE_TAG_REACT_JAVA_BRIDGE); } @@ -862,7 +876,12 @@ import static com.facebook.systrace.Systrace.TRACE_TAG_REACT_JAVA_BRIDGE; TRACE_TAG_REACT_JAVA_BRIDGE, "createAndProcessCustomReactPackage"); try { - processPackage(reactPackage, reactContext, nativeRegistryBuilder, jsModulesBuilder); + processPackage( + reactPackage, + reactContext, + moduleSpecs, + reactModuleInfoMap, + jsModulesBuilder); } finally { Systrace.endSection(TRACE_TAG_REACT_JAVA_BRIDGE); } @@ -873,7 +892,7 @@ import static com.facebook.systrace.Systrace.TRACE_TAG_REACT_JAVA_BRIDGE; Systrace.beginSection(TRACE_TAG_REACT_JAVA_BRIDGE, "buildNativeModuleRegistry"); NativeModuleRegistry nativeModuleRegistry; try { - nativeModuleRegistry = nativeRegistryBuilder.build(); + nativeModuleRegistry = new NativeModuleRegistry(moduleSpecs, reactModuleInfoMap); } finally { Systrace.endSection(TRACE_TAG_REACT_JAVA_BRIDGE); ReactMarker.logMarker(BUILD_NATIVE_MODULE_REGISTRY_END); @@ -939,14 +958,78 @@ import static com.facebook.systrace.Systrace.TRACE_TAG_REACT_JAVA_BRIDGE; private void processPackage( ReactPackage reactPackage, ReactApplicationContext reactContext, - NativeModuleRegistry.Builder nativeRegistryBuilder, + List moduleSpecs, + Map reactModuleInfoMap, JavaScriptModuleRegistry.Builder jsModulesBuilder) { - for (NativeModule nativeModule : reactPackage.createNativeModules(reactContext)) { - nativeRegistryBuilder.add(nativeModule); + SystraceMessage.beginSection(TRACE_TAG_REACT_JAVA_BRIDGE, "processPackage") + .arg("className", reactPackage.getClass().getSimpleName()) + .flush(); + if (mLazyNativeModulesEnabled && reactPackage instanceof LazyReactPackage) { + LazyReactPackage lazyReactPackage = (LazyReactPackage) reactPackage; + if (addReactModuleInfos(lazyReactPackage, reactContext, moduleSpecs, reactModuleInfoMap)) { + moduleSpecs.addAll(lazyReactPackage.getNativeModules(reactContext)); + } + } else { + FLog.d( + ReactConstants.TAG, + reactPackage.getClass().getSimpleName() + + " is not a LazyReactPackage, falling back to old version"); + addEagerModuleProviders(reactPackage, reactContext, moduleSpecs); } + for (Class jsModuleClass : reactPackage.createJSModules()) { jsModulesBuilder.add(jsModuleClass); } + Systrace.endSection(TRACE_TAG_REACT_JAVA_BRIDGE); + } + + private boolean addReactModuleInfos( + LazyReactPackage lazyReactPackage, + ReactApplicationContext reactApplicationContext, + List moduleSpecs, + Map reactModuleInfoMap) { + Class reactModuleInfoProviderClass = null; + try { + reactModuleInfoProviderClass = Class.forName( + lazyReactPackage.getClass().getCanonicalName() + "$$ReactModuleInfoProvider"); + } catch (ClassNotFoundException e) { + FLog.w( + TAG, + "Could not find generated ReactModuleInfoProvider for " + lazyReactPackage.getClass()); + // Fallback to non-lazy method. + addEagerModuleProviders(lazyReactPackage, reactApplicationContext, moduleSpecs); + return false; + } + + if (reactModuleInfoProviderClass != null) { + ReactModuleInfoProvider instance; + try { + instance = (ReactModuleInfoProvider) reactModuleInfoProviderClass.newInstance(); + } catch (InstantiationException e) { + throw new RuntimeException( + "Unable to instantiate ReactModuleInfoProvider for " + lazyReactPackage.getClass(), + e); + } catch (IllegalAccessException e) { + throw new RuntimeException( + "Unable to instantiate ReactModuleInfoProvider for " + lazyReactPackage.getClass(), + e); + } + Map map = instance.getReactModuleInfos(); + if (!map.isEmpty()) { + reactModuleInfoMap.putAll(map); + } + } + return true; + } + + private void addEagerModuleProviders( + ReactPackage reactPackage, + ReactApplicationContext reactApplicationContext, + List moduleSpecs) { + for (NativeModule nativeModule : reactPackage.createNativeModules(reactApplicationContext)) { + moduleSpecs.add( + new ModuleSpec(nativeModule.getClass(), new EagerModuleProvider(nativeModule))); + } } private void moveReactContextToCurrentLifecycleState() { diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/CatalystInstance.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/CatalystInstance.java index 46235d223..7690a213e 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/bridge/CatalystInstance.java +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/CatalystInstance.java @@ -11,9 +11,9 @@ package com.facebook.react.bridge; import java.util.Collection; +import com.facebook.proguard.annotations.DoNotStrip; import com.facebook.react.bridge.queue.ReactQueueConfiguration; import com.facebook.react.common.annotations.VisibleForTesting; -import com.facebook.proguard.annotations.DoNotStrip; /** * A higher level API on top of the asynchronous JSC bridge. This provides an diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/LifecycleEventListener.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/LifecycleEventListener.java index c78d947a0..169874ddf 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/bridge/LifecycleEventListener.java +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/LifecycleEventListener.java @@ -29,8 +29,9 @@ package com.facebook.react.bridge; public interface LifecycleEventListener { /** - * Called when host activity receives resume event (e.g. {@link Activity#onResume}. Always called - * for the most current activity. + * Called either when the host activity receives a resume event (e.g. {@link Activity#onResume} or + * if the native module that implements this is initialized while the host activity is already + * resumed. Always called for the most current activity. */ void onHostResume(); diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/ReactContext.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/ReactContext.java index 9bef44bc5..7d0416903 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/bridge/ReactContext.java +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/ReactContext.java @@ -26,6 +26,11 @@ import android.view.LayoutInflater; import com.facebook.infer.annotation.Assertions; import com.facebook.react.bridge.queue.MessageQueueThread; import com.facebook.react.bridge.queue.ReactQueueConfiguration; +import com.facebook.react.common.LifecycleState; + +import static com.facebook.react.common.LifecycleState.BEFORE_CREATE; +import static com.facebook.react.common.LifecycleState.BEFORE_RESUME; +import static com.facebook.react.common.LifecycleState.RESUMED; /** * Abstract ContextWrapper for Android application or activity {@link Context} and @@ -49,6 +54,7 @@ public class ReactContext extends ContextWrapper { private @Nullable MessageQueueThread mJSMessageQueueThread; private @Nullable NativeModuleCallExceptionHandler mNativeModuleCallExceptionHandler; private @Nullable WeakReference mCurrentActivity; + private LifecycleState mLifecycleState = LifecycleState.BEFORE_RESUME; public ReactContext(Context base) { super(base); @@ -103,7 +109,9 @@ public class ReactContext extends ContextWrapper { return mCatalystInstance.getJSModule(jsInterface); } - public T getJSModule(ExecutorToken executorToken, Class jsInterface) { + public T getJSModule( + ExecutorToken executorToken, + Class jsInterface) { if (mCatalystInstance == null) { throw new RuntimeException(EARLY_JS_ACCESS_EXCEPTION_MESSAGE); } @@ -137,8 +145,25 @@ public class ReactContext extends ContextWrapper { return mCatalystInstance != null && !mCatalystInstance.isDestroyed(); } - public void addLifecycleEventListener(LifecycleEventListener listener) { + public void addLifecycleEventListener(final LifecycleEventListener listener) { mLifecycleEventListeners.add(listener); + if (hasActiveCatalystInstance()) { + switch (mLifecycleState) { + case BEFORE_CREATE: + case BEFORE_RESUME: + break; + case RESUMED: + runOnUiQueueThread(new Runnable() { + @Override + public void run() { + listener.onHostResume(); + } + }); + break; + default: + throw new RuntimeException("Unhandled lifecycle state."); + } + } } public void removeLifecycleEventListener(LifecycleEventListener listener) { @@ -173,6 +198,7 @@ public class ReactContext extends ContextWrapper { public void onHostResume(@Nullable Activity activity) { UiThreadUtil.assertOnUiThread(); mCurrentActivity = new WeakReference(activity); + mLifecycleState = LifecycleState.RESUMED; for (LifecycleEventListener listener : mLifecycleEventListeners) { listener.onHostResume(); } @@ -191,6 +217,7 @@ public class ReactContext extends ContextWrapper { */ public void onHostPause() { UiThreadUtil.assertOnUiThread(); + mLifecycleState = LifecycleState.BEFORE_RESUME; for (LifecycleEventListener listener : mLifecycleEventListeners) { listener.onHostPause(); } diff --git a/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/BUCK b/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/BUCK index 6a3be04bd..5155d6e98 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/BUCK +++ b/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/BUCK @@ -27,7 +27,8 @@ android_library( react_native_target('java/com/facebook/react/bridge:bridge'), react_native_target('java/com/facebook/react/common:common'), react_native_target('java/com/facebook/react/devsupport:devsupport'), -], + react_native_target('java/com/facebook/react/module/model:model'), + ], visibility = [ 'PUBLIC', ], diff --git a/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/JavaModuleWrapper.java b/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/JavaModuleWrapper.java index 0794497d9..de474adee 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/JavaModuleWrapper.java +++ b/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/JavaModuleWrapper.java @@ -49,23 +49,23 @@ import static com.facebook.systrace.Systrace.TRACE_TAG_REACT_JAVA_BRIDGE; } private final CatalystInstance mCatalystInstance; - private final BaseJavaModule mModule; + private final ModuleHolder mModuleHolder; private final ArrayList mMethods; - public JavaModuleWrapper(CatalystInstance catalystinstance, BaseJavaModule module) { + public JavaModuleWrapper(CatalystInstance catalystinstance, ModuleHolder moduleHolder) { mCatalystInstance = catalystinstance; - mModule = module; - mMethods = new ArrayList(); + mModuleHolder = moduleHolder; + mMethods = new ArrayList<>(); } @DoNotStrip public BaseJavaModule getModule() { - return mModule; + return (BaseJavaModule) mModuleHolder.getModule(); } @DoNotStrip public String getName() { - return mModule.getName(); + return mModuleHolder.getInfo().name(); } @DoNotStrip @@ -73,7 +73,7 @@ import static com.facebook.systrace.Systrace.TRACE_TAG_REACT_JAVA_BRIDGE; ArrayList descs = new ArrayList<>(); for (Map.Entry entry : - mModule.getMethods().entrySet()) { + getModule().getMethods().entrySet()) { MethodDescriptor md = new MethodDescriptor(); md.name = entry.getKey(); md.type = entry.getValue().getType(); @@ -92,7 +92,7 @@ import static com.facebook.systrace.Systrace.TRACE_TAG_REACT_JAVA_BRIDGE; ArrayList descs = new ArrayList<>(); for (Map.Entry entry : - mModule.getMethods().entrySet()) { + getModule().getMethods().entrySet()) { MethodDescriptor md = new MethodDescriptor(); md.name = entry.getKey(); md.type = entry.getValue().getType(); @@ -105,7 +105,7 @@ import static com.facebook.systrace.Systrace.TRACE_TAG_REACT_JAVA_BRIDGE; } for (Map.Entry entry : - mModule.getSyncHooks().entrySet()) { + getModule().getSyncHooks().entrySet()) { MethodDescriptor md = new MethodDescriptor(); md.name = entry.getKey(); md.type = BaseJavaModule.METHOD_TYPE_SYNC; @@ -127,7 +127,7 @@ import static com.facebook.systrace.Systrace.TRACE_TAG_REACT_JAVA_BRIDGE; SystraceMessage.beginSection(TRACE_TAG_REACT_JAVA_BRIDGE, "Map constants") .arg("moduleName", getName()) .flush(); - Map map = mModule.getConstants(); + Map map = getModule().getConstants(); Systrace.endSection(TRACE_TAG_REACT_JAVA_BRIDGE); SystraceMessage.beginSection(TRACE_TAG_REACT_JAVA_BRIDGE, "WritableNativeMap constants") @@ -146,7 +146,7 @@ import static com.facebook.systrace.Systrace.TRACE_TAG_REACT_JAVA_BRIDGE; @DoNotStrip public boolean supportsWebWorkers() { - return mModule.supportsWebWorkers(); + return getModule().supportsWebWorkers(); } @DoNotStrip diff --git a/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/ModuleHolder.java b/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/ModuleHolder.java new file mode 100644 index 000000000..0e7826c58 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/ModuleHolder.java @@ -0,0 +1,159 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +package com.facebook.react.cxxbridge; + +import javax.annotation.Nullable; +import javax.inject.Provider; + +import java.util.concurrent.ExecutionException; + +import com.facebook.react.bridge.NativeModule; +import com.facebook.react.common.futures.SimpleSettableFuture; +import com.facebook.react.module.model.Info; +import com.facebook.react.module.model.ReactModuleInfo; +import com.facebook.systrace.Systrace; +import com.facebook.systrace.SystraceMessage; + +import static com.facebook.infer.annotation.Assertions.assertNotNull; +import static com.facebook.systrace.Systrace.TRACE_TAG_REACT_JAVA_BRIDGE; + +/** + * Holder to enable us to lazy create native modules. + * + * This works by taking a provider instead of an instance, when it is first required we'll create + * and initialize it. Initialization currently always happens on the UI thread but this is due to + * change for performance reasons. + * + * Lifecycle events via a {@link LifecycleEventListener} will still always happen on the UI thread. + */ +public class ModuleHolder { + + private final Info mInfo; + private @Nullable Provider mProvider; + private @Nullable NativeModule mModule; + private boolean mInitializeNeeded; + + public ModuleHolder( + Class clazz, + @Nullable ReactModuleInfo reactModuleInfo, + Provider provider) { + mInfo = reactModuleInfo == null ? new LegacyModuleInfo(clazz) : reactModuleInfo; + mProvider = provider; + + if (mInfo.needsEagerInit()) { + mModule = doCreate(); + } + } + + public synchronized void initialize() { + if (mModule != null) { + doInitialize(mModule); + } else { + mInitializeNeeded = true; + } + } + + public synchronized void destroy() { + if (mModule != null) { + mModule.onCatalystInstanceDestroy(); + } + } + + public Info getInfo() { + return mInfo; + } + + public synchronized NativeModule getModule() { + if (mModule == null) { + mModule = doCreate(); + } + return mModule; + } + + private NativeModule doCreate() { + NativeModule module = create(); + mProvider = null; + return module; + } + + private NativeModule create() { + String name = mInfo instanceof LegacyModuleInfo ? + ((LegacyModuleInfo) mInfo).mType.getSimpleName() : + mInfo.name(); + SystraceMessage.beginSection(TRACE_TAG_REACT_JAVA_BRIDGE, "createModule") + .arg("name", name) + .flush(); + NativeModule module = assertNotNull(mProvider).get(); + if (mInitializeNeeded) { + doInitialize(module); + mInitializeNeeded = false; + } + Systrace.endSection(TRACE_TAG_REACT_JAVA_BRIDGE); + return module; + } + + private void doInitialize(NativeModule module) { + SystraceMessage.Builder section = + SystraceMessage.beginSection(TRACE_TAG_REACT_JAVA_BRIDGE, "initialize"); + if (module instanceof CxxModuleWrapper) { + section.arg("className", module.getClass().getSimpleName()); + } else { + section.arg("name", mInfo.name()); + } + section.flush(); + callInitializeOnUiThread(module); + Systrace.endSection(TRACE_TAG_REACT_JAVA_BRIDGE); + } + + // TODO(t11394264): Use the native module thread here after the old bridge is gone + private static void callInitializeOnUiThread(final NativeModule module) { + if (UiThreadUtil.isOnUiThread()) { + module.initialize(); + return; + } + final SimpleSettableFuture future = new SimpleSettableFuture<>(); + UiThreadUtil.runOnUiThread(new Runnable() { + @Override + public void run() { + Systrace.beginSection(TRACE_TAG_REACT_JAVA_BRIDGE, "initializeOnUiThread"); + try { + module.initialize(); + future.set(null); + } catch (Exception e) { + future.setException(e); + } + Systrace.endSection(TRACE_TAG_REACT_JAVA_BRIDGE); + } + }); + try { + future.get(); + } catch (InterruptedException | ExecutionException e) { + throw new RuntimeException(e); + } + } + + private class LegacyModuleInfo implements Info { + + public final Class mType; + + public LegacyModuleInfo(Class type) { + mType = type; + } + + public String name() { + return getModule().getName(); + } + + public boolean canOverrideExistingModule() { + return getModule().canOverrideExistingModule(); + } + + public boolean supportsWebWorkers() { + return getModule().supportsWebWorkers(); + } + + public boolean needsEagerInit() { + return true; + } + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/NativeModuleRegistry.java b/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/NativeModuleRegistry.java index 0d8e85f62..2c15fab2a 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/NativeModuleRegistry.java +++ b/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/NativeModuleRegistry.java @@ -10,47 +10,85 @@ package com.facebook.react.cxxbridge; import java.util.ArrayList; -import java.util.Collection; import java.util.HashMap; +import java.util.List; import java.util.Map; +import android.util.Pair; + import com.facebook.infer.annotation.Assertions; import com.facebook.react.bridge.BaseJavaModule; +import com.facebook.react.bridge.ModuleSpec; import com.facebook.react.bridge.NativeModule; import com.facebook.react.bridge.OnBatchCompleteListener; import com.facebook.react.bridge.ReactMarker; import com.facebook.react.bridge.ReactMarkerConstants; -import com.facebook.react.common.MapBuilder; +import com.facebook.react.module.model.ReactModuleInfo; import com.facebook.systrace.Systrace; /** * A set of Java APIs to expose to a particular JavaScript instance. */ public class NativeModuleRegistry { - private final Map, NativeModule> mModuleInstances; + + private final Map, ModuleHolder> mModules; private final ArrayList mBatchCompleteListenerModules; - private NativeModuleRegistry(Map, NativeModule> moduleInstances) { - mModuleInstances = moduleInstances; - mBatchCompleteListenerModules = new ArrayList(mModuleInstances.size()); - for (NativeModule module : mModuleInstances.values()) { - if (module instanceof OnBatchCompleteListener) { - mBatchCompleteListenerModules.add((OnBatchCompleteListener) module); + public NativeModuleRegistry( + List moduleSpecList, + Map reactModuleInfoMap) { + Map, ModuleHolder>> namesToSpecs = new HashMap<>(); + for (ModuleSpec module : moduleSpecList) { + Class type = module.getType(); + ModuleHolder holder = new ModuleHolder( + type, + reactModuleInfoMap.get(type), + module.getProvider()); + String name = holder.getInfo().name(); + Class existing = namesToSpecs.containsKey(name) ? + namesToSpecs.get(name).first : + null; + if (existing != null && !holder.getInfo().canOverrideExistingModule()) { + throw new IllegalStateException("Native module " + type.getSimpleName() + + " tried to override " + existing.getSimpleName() + " for module name " + name + + ". If this was your intention, set canOverrideExistingModule=true"); + } + namesToSpecs.put(name, new Pair, ModuleHolder>(type, holder)); + } + + mModules = new HashMap<>(); + for (Pair, ModuleHolder> pair : namesToSpecs.values()) { + mModules.put(pair.first, pair.second); + } + + mBatchCompleteListenerModules = new ArrayList<>(); + for (Class type : mModules.keySet()) { + if (OnBatchCompleteListener.class.isAssignableFrom(type)) { + final ModuleHolder holder = mModules.get(type); + mBatchCompleteListenerModules.add(new OnBatchCompleteListener() { + @Override + public void onBatchComplete() { + OnBatchCompleteListener listener = (OnBatchCompleteListener) holder.getModule(); + listener.onBatchComplete(); + } + }); } } } /* package */ ModuleRegistryHolder getModuleRegistryHolder( - CatalystInstanceImpl catalystInstanceImpl) { + CatalystInstanceImpl catalystInstanceImpl) { ArrayList javaModules = new ArrayList<>(); ArrayList cxxModules = new ArrayList<>(); - for (NativeModule module : mModuleInstances.values()) { - if (module instanceof BaseJavaModule) { - javaModules.add(new JavaModuleWrapper(catalystInstanceImpl, (BaseJavaModule) module)); - } else if (module instanceof CxxModuleWrapper) { - cxxModules.add((CxxModuleWrapper) module); + for (Map.Entry, ModuleHolder> entry : mModules.entrySet()) { + Class type = entry.getKey(); + ModuleHolder moduleHolder = entry.getValue(); + if (BaseJavaModule.class.isAssignableFrom(type)) { + javaModules.add(new JavaModuleWrapper(catalystInstanceImpl, moduleHolder)); + } else if (CxxModuleWrapper.class.isAssignableFrom(type)) { + cxxModules.add((CxxModuleWrapper) moduleHolder.getModule()); } else { - throw new IllegalArgumentException("Unknown module type " + module.getClass()); + throw new IllegalArgumentException("Unknown module type " + type); } } return new ModuleRegistryHolder(catalystInstanceImpl, javaModules, cxxModules); @@ -62,8 +100,8 @@ public class NativeModuleRegistry { Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, "NativeModuleRegistry_notifyCatalystInstanceDestroy"); try { - for (NativeModule nativeModule : mModuleInstances.values()) { - nativeModule.onCatalystInstanceDestroy(); + for (ModuleHolder module : mModules.values()) { + module.destroy(); } } finally { Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE); @@ -78,8 +116,8 @@ public class NativeModuleRegistry { Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, "NativeModuleRegistry_notifyCatalystInstanceInitialized"); try { - for (NativeModule nativeModule : mModuleInstances.values()) { - nativeModule.initialize(); + for (ModuleHolder module : mModules.values()) { + module.initialize(); } } finally { Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE); @@ -94,38 +132,18 @@ public class NativeModuleRegistry { } public boolean hasModule(Class moduleInterface) { - return mModuleInstances.containsKey(moduleInterface); + return mModules.containsKey(moduleInterface); } public T getModule(Class moduleInterface) { - return (T) Assertions.assertNotNull(mModuleInstances.get(moduleInterface)); + return (T) Assertions.assertNotNull(mModules.get(moduleInterface)).getModule(); } - public Collection getAllModules() { - return mModuleInstances.values(); - } - - public static class Builder { - private final HashMap mModules = MapBuilder.newHashMap(); - - public Builder add(NativeModule module) { - NativeModule existing = mModules.get(module.getName()); - if (existing != null && !module.canOverrideExistingModule()) { - throw new IllegalStateException("Native module " + module.getClass().getSimpleName() + - " tried to override " + existing.getClass().getSimpleName() + " for module name " + - module.getName() + ". If this was your intention, return true from " + - module.getClass().getSimpleName() + "#canOverrideExistingModule()"); - } - mModules.put(module.getName(), module); - return this; - } - - public NativeModuleRegistry build() { - Map, NativeModule> moduleInstances = new HashMap<>(); - for (NativeModule module : mModules.values()) { - moduleInstances.put((Class) module.getClass(), module); - } - return new NativeModuleRegistry(moduleInstances); + public List getAllModules() { + List modules = new ArrayList<>(); + for (ModuleHolder module : mModules.values()) { + modules.add(module.getModule()); } + return modules; } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/module/model/Info.java b/ReactAndroid/src/main/java/com/facebook/react/module/model/Info.java new file mode 100644 index 000000000..c280b8833 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/module/model/Info.java @@ -0,0 +1,17 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +package com.facebook.react.module.model; + +/** + * Interface for static information about native modules. + */ +public interface Info { + + String name(); + + boolean canOverrideExistingModule(); + + boolean supportsWebWorkers(); + + boolean needsEagerInit(); +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/module/model/ReactModuleInfo.java b/ReactAndroid/src/main/java/com/facebook/react/module/model/ReactModuleInfo.java index 3c0a32b8c..bb3328bac 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/module/model/ReactModuleInfo.java +++ b/ReactAndroid/src/main/java/com/facebook/react/module/model/ReactModuleInfo.java @@ -3,14 +3,15 @@ package com.facebook.react.module.model; /** - * Data holder class holding native module specifications. + * Data holder class holding native module specifications. {@link ReactModuleSpecProcessor} creates + * these so Java modules don't have to be instantiated at React Native start up. */ -public class ReactModuleInfo { +public class ReactModuleInfo implements Info { - public final String mName; - public final boolean mCanOverrideExistingModule; - public final boolean mSupportsWebWorkers; - public final boolean mNeedsEagerInit; + private final String mName; + private final boolean mCanOverrideExistingModule; + private final boolean mSupportsWebWorkers; + private final boolean mNeedsEagerInit; public ReactModuleInfo( String name, @@ -22,4 +23,24 @@ public class ReactModuleInfo { mSupportsWebWorkers = supportsWebWorkers; mNeedsEagerInit = needsEagerInit; } + + @Override + public String name() { + return mName; + } + + @Override + public boolean canOverrideExistingModule() { + return mCanOverrideExistingModule; + } + + @Override + public boolean supportsWebWorkers() { + return mSupportsWebWorkers; + } + + @Override + public boolean needsEagerInit() { + return mNeedsEagerInit; + } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/module/model/ReactModuleInfoProvider.java b/ReactAndroid/src/main/java/com/facebook/react/module/model/ReactModuleInfoProvider.java new file mode 100644 index 000000000..8c30f955e --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/module/model/ReactModuleInfoProvider.java @@ -0,0 +1,13 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +package com.facebook.react.module.model; + +import java.util.Map; + +/** + * Interface for auto-generated class by ReactModuleSpecProcessor. + */ +public interface ReactModuleInfoProvider { + + Map getReactModuleInfos(); +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/module/processing/ReactModuleSpecProcessor.java b/ReactAndroid/src/main/java/com/facebook/react/module/processing/ReactModuleSpecProcessor.java index 4d582c9a9..1552c3cd7 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/module/processing/ReactModuleSpecProcessor.java +++ b/ReactAndroid/src/main/java/com/facebook/react/module/processing/ReactModuleSpecProcessor.java @@ -28,6 +28,7 @@ import com.facebook.infer.annotation.SuppressFieldNotInitialized; import com.facebook.react.module.annotations.ReactModule; import com.facebook.react.module.annotations.ReactModuleList; import com.facebook.react.module.model.ReactModuleInfo; +import com.facebook.react.module.model.ReactModuleInfoProvider; import com.squareup.javapoet.ClassName; import com.squareup.javapoet.CodeBlock; @@ -97,9 +98,8 @@ public class ReactModuleSpecProcessor extends AbstractProcessor { MethodSpec getReactModuleInfosMethod; try { getReactModuleInfosMethod = MethodSpec.methodBuilder("getReactModuleInfos") + .addAnnotation(Override.class) .addModifiers(PUBLIC) - // TODO add function to native module interface -// .addAnnotation(Override.class) .addCode(getCodeBlockForReactModuleInfos(nativeModules)) .returns(MAP_TYPE) .build(); @@ -108,11 +108,12 @@ public class ReactModuleSpecProcessor extends AbstractProcessor { return false; } - TypeSpec reactModulesInfosTypeSpec = TypeSpec.classBuilder( - fileName + "$$ReactModuleInfoProvider") - .addModifiers(Modifier.PUBLIC) - .addMethod(getReactModuleInfosMethod) - .build(); + TypeSpec reactModulesInfosTypeSpec = TypeSpec.classBuilder( + fileName + "$$ReactModuleInfoProvider") + .addModifiers(Modifier.PUBLIC) + .addMethod(getReactModuleInfosMethod) + .addSuperinterface(ReactModuleInfoProvider.class) + .build(); JavaFile javaFile = JavaFile.builder(packageName, reactModulesInfosTypeSpec) .addFileComment("Generated by " + getClass().getName()) @@ -130,30 +131,35 @@ public class ReactModuleSpecProcessor extends AbstractProcessor { private CodeBlock getCodeBlockForReactModuleInfos(List nativeModules) throws ReactModuleSpecException { - CodeBlock.Builder builder = CodeBlock.builder() - .addStatement("$T map = new $T()", MAP_TYPE, INSTANTIATED_MAP_TYPE); + CodeBlock.Builder builder = CodeBlock.builder(); + if (nativeModules == null || nativeModules.isEmpty()) { + builder.addStatement("return Collections.emptyMap()"); + } else { + builder.addStatement("$T map = new $T()", MAP_TYPE, INSTANTIATED_MAP_TYPE); - for (String nativeModule : nativeModules) { - String keyString = nativeModule + ".class"; + for (String nativeModule : nativeModules) { + String keyString = nativeModule + ".class"; - TypeElement typeElement = mElements.getTypeElement(nativeModule); - ReactModule reactModule = typeElement.getAnnotation(ReactModule.class); - if (reactModule == null) { - throw new ReactModuleSpecException(keyString + " not found by ReactModuleSpecProcessor. " + - "Did you forget to add the @ReactModule annotation the the native module?"); + TypeElement typeElement = mElements.getTypeElement(nativeModule); + ReactModule reactModule = typeElement.getAnnotation(ReactModule.class); + if (reactModule == null) { + throw new ReactModuleSpecException( + keyString + " not found by ReactModuleSpecProcessor. " + + "Did you forget to add the @ReactModule annotation the the native module?"); + } + String valueString = new StringBuilder() + .append("new ReactModuleInfo(") + .append("\"").append(reactModule.name()).append("\"").append(", ") + .append(reactModule.canOverrideExistingModule()).append(", ") + .append(reactModule.supportsWebWorkers()).append(", ") + .append(reactModule.needsEagerInit()) + .append(")") + .toString(); + + builder.addStatement("map.put(" + keyString + ", " + valueString + ")"); } - String valueString = new StringBuilder() - .append("new ReactModuleInfo(") - .append("\"").append(reactModule.name()).append("\"").append(", ") - .append(reactModule.canOverrideExistingModule()).append(", ") - .append(reactModule.supportsWebWorkers()).append(", ") - .append(reactModule.needsEagerInit()) - .append(")") - .toString(); - - builder.addStatement("map.put(" + keyString + ", " + valueString + ")"); + builder.addStatement("return map"); } - builder.addStatement("return map"); return builder.build(); }