Skip to content

Commit

Permalink
Add ability to lazy load Native Java Modules
Browse files Browse the repository at this point in the history
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
  • Loading branch information
aaronechiu authored and Facebook Github Bot 3 committed Sep 23, 2016
1 parent 1f9b765 commit 797ca6c
Show file tree
Hide file tree
Showing 16 changed files with 500 additions and 120 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,16 @@

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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@

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;
Expand All @@ -18,39 +21,44 @@
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<ModuleSpec> mModuleSpecList = new ArrayList<>();
private final JavaScriptModuleRegistry.Builder mJSModuleRegistryBuilder =
new JavaScriptModuleRegistry.Builder();

private @Nullable Context mContext;

@Override
public ReactInstanceEasyBuilder setContext(Context context) {
mContext = context;
return this;
}

@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;
}

Expand All @@ -71,7 +79,9 @@ public CatalystInstance build() {
return new CatalystInstanceImpl.Builder()
.setReactQueueConfigurationSpec(ReactQueueConfigurationSpec.createDefault())
.setJSExecutor(executor)
.setRegistry(mNativeModuleRegistryBuilder.build())
.setRegistry(new NativeModuleRegistry(
mModuleSpecList,
Collections.<Class, ReactModuleInfo>emptyMap()))
.setJSModuleRegistry(mJSModuleRegistryBuilder.build())
.setJSBundleLoader(JSBundleLoader.createAssetLoader(
mContext,
Expand Down
Original file line number Diff line number Diff line change
@@ -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<NativeModule> {

private final NativeModule mModule;

public EagerModuleProvider(NativeModule module) {
mModule = module;
}

@Override
public NativeModule get() {
return mModule;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@
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;

Expand All @@ -35,6 +37,7 @@
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;
Expand All @@ -61,6 +64,8 @@
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;
Expand All @@ -72,6 +77,7 @@
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;
Expand Down Expand Up @@ -107,6 +113,8 @@
*/
/* 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<ReactRootView> mAttachedRootViews = new ArrayList<>();
private LifecycleState mLifecycleState;
Expand Down Expand Up @@ -836,7 +844,8 @@ private ReactApplicationContext createReactContext(
FLog.i(ReactConstants.TAG, "Creating react context.");
ReactMarker.logMarker(CREATE_REACT_CONTEXT_START);
mSourceUrl = jsBundleLoader.getSourceUrl();
NativeModuleRegistry.Builder nativeRegistryBuilder = new NativeModuleRegistry.Builder();
List<ModuleSpec> moduleSpecs = new ArrayList<>();
Map<Class, ReactModuleInfo> reactModuleInfoMap = new HashMap<>();
JavaScriptModuleRegistry.Builder jsModulesBuilder = new JavaScriptModuleRegistry.Builder();

final ReactApplicationContext reactContext = new ReactApplicationContext(mApplicationContext);
Expand All @@ -851,7 +860,12 @@ private ReactApplicationContext createReactContext(
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);
}
Expand All @@ -862,7 +876,12 @@ private ReactApplicationContext createReactContext(
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);
}
Expand All @@ -873,7 +892,7 @@ private ReactApplicationContext createReactContext(
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);
Expand Down Expand Up @@ -939,14 +958,78 @@ public Void call() throws Exception {
private void processPackage(
ReactPackage reactPackage,
ReactApplicationContext reactContext,
NativeModuleRegistry.Builder nativeRegistryBuilder,
List<ModuleSpec> moduleSpecs,
Map<Class, ReactModuleInfo> 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<? extends JavaScriptModule> jsModuleClass : reactPackage.createJSModules()) {
jsModulesBuilder.add(jsModuleClass);
}
Systrace.endSection(TRACE_TAG_REACT_JAVA_BRIDGE);
}

private boolean addReactModuleInfos(
LazyReactPackage lazyReactPackage,
ReactApplicationContext reactApplicationContext,
List<ModuleSpec> moduleSpecs,
Map<Class, ReactModuleInfo> 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<Class, ReactModuleInfo> map = instance.getReactModuleInfos();
if (!map.isEmpty()) {
reactModuleInfoMap.putAll(map);
}
}
return true;
}

private void addEagerModuleProviders(
ReactPackage reactPackage,
ReactApplicationContext reactApplicationContext,
List<ModuleSpec> moduleSpecs) {
for (NativeModule nativeModule : reactPackage.createNativeModules(reactApplicationContext)) {
moduleSpecs.add(
new ModuleSpec(nativeModule.getClass(), new EagerModuleProvider(nativeModule)));
}
}

private void moveReactContextToCurrentLifecycleState() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@

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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,9 @@
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();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@
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
Expand All @@ -49,6 +54,7 @@ public class ReactContext extends ContextWrapper {
private @Nullable MessageQueueThread mJSMessageQueueThread;
private @Nullable NativeModuleCallExceptionHandler mNativeModuleCallExceptionHandler;
private @Nullable WeakReference<Activity> mCurrentActivity;
private LifecycleState mLifecycleState = LifecycleState.BEFORE_RESUME;

public ReactContext(Context base) {
super(base);
Expand Down Expand Up @@ -103,7 +109,9 @@ public <T extends JavaScriptModule> T getJSModule(Class<T> jsInterface) {
return mCatalystInstance.getJSModule(jsInterface);
}

public <T extends JavaScriptModule> T getJSModule(ExecutorToken executorToken, Class<T> jsInterface) {
public <T extends JavaScriptModule> T getJSModule(
ExecutorToken executorToken,
Class<T> jsInterface) {
if (mCatalystInstance == null) {
throw new RuntimeException(EARLY_JS_ACCESS_EXCEPTION_MESSAGE);
}
Expand Down Expand Up @@ -137,8 +145,25 @@ public boolean hasActiveCatalystInstance() {
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();

This comment has been minimized.

Copy link
@richardhuaaa

richardhuaaa Nov 12, 2016

I'm not sure I agree with this change, because it seems inconsistent with how the onResume() method of Android Activities work.

But even if we want to do it this way, isn't it inconsistent that onHostPause() is not also called immediately when added while the app's state is BEFORE_RESUME? Note also that without this addition, it seems like there's no way for a consumer to detect specifically the first time an app transitions from BEFORE_RESUME to RESUMED, as opposed to the first time an app is in the RESUMED state.

}
});
break;
default:
throw new RuntimeException("Unhandled lifecycle state.");
}
}
}

public void removeLifecycleEventListener(LifecycleEventListener listener) {
Expand Down Expand Up @@ -173,6 +198,7 @@ public void removeActivityEventListener(ActivityEventListener listener) {
public void onHostResume(@Nullable Activity activity) {
UiThreadUtil.assertOnUiThread();
mCurrentActivity = new WeakReference(activity);
mLifecycleState = LifecycleState.RESUMED;
for (LifecycleEventListener listener : mLifecycleEventListeners) {
listener.onHostResume();
}
Expand All @@ -191,6 +217,7 @@ public void onNewIntent(@Nullable Activity activity, Intent intent) {
*/
public void onHostPause() {
UiThreadUtil.assertOnUiThread();
mLifecycleState = LifecycleState.BEFORE_RESUME;
for (LifecycleEventListener listener : mLifecycleEventListeners) {
listener.onHostPause();
}
Expand Down
Loading

2 comments on commit 797ca6c

@vonovak
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how to make use of this commit in an RN android app?

@miguelespinoza
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm also trying to figure out how to make use of this commit (@vonovak). For many developers they use the default build tool Gradle but there's no clear way to use annotationProcessor which is where I'm somewhat stuck

Please sign in to comment.