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(); }