From 3a743ef228a14e07c77c5488b080413643ec9c4b Mon Sep 17 00:00:00 2001 From: Matthew Arbesfeld Date: Wed, 28 Oct 2015 04:56:26 -0700 Subject: [PATCH] Allow developers to load JavaScript bundle from any source. Summary: This patch adds two pieces of functionality: - Exposes `JSBundleLoader` to allow a developer to load JavaScript bundles as they choose. - Adds `ReactBridge.loadScripFromFile` method which loads a JavaScript bundle from an arbitrary file path. Example usage: ``` JSBundleLoader jsBundleLoader = new JSBundleLoader() { Override public void loadScript(ReactBridge reactBridge) { reactBridge.loadScriptFromFile("/sdcard/Download/index.android.bundle"); } }; mReactInstanceManager = ReactInstanceManager.builder() .setApplication(getApplication()) .setJSBundleLoader(jsBundleLoader) .setJSMainModuleName("") /* necessary due to TODO(6803830) */ .addPackage(new MainReactPackage()) .setInitialLifecycleState(LifecycleState.RESUMED) .build(); ``` cc ide Closes https://github.com/facebook/react-native/pull/3189 Reviewed By: svcscm Differential Revision: D2535819 Pulled By: mkonicek fb-gh-sync-id: f319299dbe29bab3b7e91f94249c14b270d9fec3 --- .../facebook/react/ReactInstanceManager.java | 55 +++++++++++-------- .../facebook/react/bridge/JSBundleLoader.java | 20 ++++--- .../facebook/react/bridge/ReactBridge.java | 2 +- .../react/devsupport/DevSupportManager.java | 8 +++ .../src/main/jni/react/jni/OnLoad.cpp | 25 ++++----- 5 files changed, 65 insertions(+), 45 deletions(-) diff --git a/ReactAndroid/src/main/java/com/facebook/react/ReactInstanceManager.java b/ReactAndroid/src/main/java/com/facebook/react/ReactInstanceManager.java index 729fadaee..76126952b 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/ReactInstanceManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/ReactInstanceManager.java @@ -77,7 +77,7 @@ public class ReactInstanceManager { private @Nullable ReactContextInitParams mPendingReactContextInitParams; /* accessed from any thread */ - private final @Nullable String mBundleAssetName; /* name of JS bundle file in assets folder */ + private @Nullable String mJSBundleFile; /* path to JS bundle on file system */ private final @Nullable String mJSMainModuleName; /* path to JS bundle root on packager server */ private final List mPackages; private final DevSupportManager mDevSupportManager; @@ -176,7 +176,7 @@ public class ReactInstanceManager { private ReactInstanceManager( Context applicationContext, - @Nullable String bundleAssetName, + @Nullable String jsBundleFile, @Nullable String jsMainModuleName, List packages, boolean useDeveloperSupport, @@ -185,7 +185,7 @@ public class ReactInstanceManager { initializeSoLoaderIfNecessary(applicationContext); mApplicationContext = applicationContext; - mBundleAssetName = bundleAssetName; + mJSBundleFile = jsBundleFile; mJSMainModuleName = jsMainModuleName; mPackages = packages; mUseDeveloperSupport = useDeveloperSupport; @@ -224,30 +224,29 @@ public class ReactInstanceManager { SoLoader.init(applicationContext, /* native exopackage */ false); } + public void setJSBundleFile(String jsBundleFile) { + mJSBundleFile = jsBundleFile; + } + /** * Trigger react context initialization asynchronously in a background async task. This enables * applications to pre-load the application JS, and execute global code before * {@link ReactRootView} is available and measured. */ public void createReactContextInBackground() { - if (mUseDeveloperSupport) { + if (mUseDeveloperSupport && mJSMainModuleName != null) { if (mDevSupportManager.hasUpToDateJSBundleInCache()) { // If there is a up-to-date bundle downloaded from server, always use that onJSBundleLoadedFromServer(); - return; - } else if (mBundleAssetName == null || - !mDevSupportManager.hasBundleInAssets(mBundleAssetName)) { - // Bundle not available in assets, fetch from the server + } else { mDevSupportManager.handleReloadJS(); - return; } + return; } - // Use JS file from assets + recreateReactContextInBackground( new JSCJavaScriptExecutor(), - JSBundleLoader.createAssetLoader( - mApplicationContext.getAssets(), - mBundleAssetName)); + JSBundleLoader.createFileLoader(mApplicationContext, mJSBundleFile)); } /** @@ -564,7 +563,7 @@ public class ReactInstanceManager { private final List mPackages = new ArrayList<>(); - private @Nullable String mBundleAssetName; + private @Nullable String mJSBundleFile; private @Nullable String mJSMainModuleName; private @Nullable NotThreadSafeBridgeIdleDebugListener mBridgeIdleDebugListener; private @Nullable Application mApplication; @@ -575,11 +574,21 @@ public class ReactInstanceManager { } /** - * Name of the JS budle file to be loaded from application's raw assets. + * Name of the JS bundle file to be loaded from application's raw assets. + * * Example: {@code "index.android.js"} */ public Builder setBundleAssetName(String bundleAssetName) { - mBundleAssetName = bundleAssetName; + return this.setJSBundleFile("assets://" + bundleAssetName); + } + + /** + * Path to the JS bundle file to be loaded from the file system. + * + * Example: {@code "assets://index.android.js" or "/sdcard/main.jsbundle} + */ + public Builder setJSBundleFile(String jsBundleFile) { + mJSBundleFile = jsBundleFile; return this; } @@ -639,21 +648,23 @@ public class ReactInstanceManager { * Before calling {@code build}, the following must be called: *
    *
  • {@link #setApplication} - *
  • {@link #setBundleAssetName} or {@link #setJSMainModuleName} + *
  • {@link #setJSBundleFile} or {@link #setJSMainModuleName} *
*/ public ReactInstanceManager build() { Assertions.assertCondition( - mUseDeveloperSupport || mBundleAssetName != null, - "JS Bundle has to be provided in app assets when dev support is disabled"); + mUseDeveloperSupport || mJSBundleFile != null, + "JS Bundle File has to be provided when dev support is disabled"); + Assertions.assertCondition( - mBundleAssetName != null || mJSMainModuleName != null, - "Either BundleAssetName or MainModuleName needs to be provided"); + mJSMainModuleName != null || mJSBundleFile != null, + "Either MainModuleName or JS Bundle File needs to be provided"); + return new ReactInstanceManager( Assertions.assertNotNull( mApplication, "Application property has not been set with this builder"), - mBundleAssetName, + mJSBundleFile, mJSMainModuleName, mPackages, mUseDeveloperSupport, diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/JSBundleLoader.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/JSBundleLoader.java index d703bbc4f..c784cd007 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/bridge/JSBundleLoader.java +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/JSBundleLoader.java @@ -9,7 +9,7 @@ package com.facebook.react.bridge; -import android.content.res.AssetManager; +import android.content.Context; /** * A class that stores JS bundle information and allows {@link CatalystInstance} to load a correct @@ -22,18 +22,22 @@ public abstract class JSBundleLoader { * should be used. JS bundle will be read from assets directory in native code to save on passing * large strings from java to native memory. */ - public static JSBundleLoader createAssetLoader( - final AssetManager assetManager, - final String assetFileName) { + public static JSBundleLoader createFileLoader( + final Context context, + final String fileName) { return new JSBundleLoader() { @Override public void loadScript(ReactBridge bridge) { - bridge.loadScriptFromAssets(assetManager, assetFileName); + if (fileName.startsWith("assets://")) { + bridge.loadScriptFromAssets(context.getAssets(), fileName.replaceFirst("assets://", "")); + } else { + bridge.loadScriptFromFile(fileName, fileName); + } } @Override public String getSourceUrl() { - return "file:///android_asset/" + assetFileName; + return fileName; } }; } @@ -51,7 +55,7 @@ public abstract class JSBundleLoader { return new JSBundleLoader() { @Override public void loadScript(ReactBridge bridge) { - bridge.loadScriptFromNetworkCached(sourceURL, cachedFileLocation); + bridge.loadScriptFromFile(cachedFileLocation, sourceURL); } @Override @@ -70,7 +74,7 @@ public abstract class JSBundleLoader { return new JSBundleLoader() { @Override public void loadScript(ReactBridge bridge) { - bridge.loadScriptFromNetworkCached(sourceURL, null); + bridge.loadScriptFromFile(null, sourceURL); } @Override diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/ReactBridge.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/ReactBridge.java index 624de7506..d84c7b844 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/bridge/ReactBridge.java +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/ReactBridge.java @@ -65,7 +65,7 @@ public class ReactBridge extends Countable { * All native functions are not thread safe and appropriate queues should be used */ public native void loadScriptFromAssets(AssetManager assetManager, String assetName); - public native void loadScriptFromNetworkCached(String sourceURL, @Nullable String tempFileName); + public native void loadScriptFromFile(@Nullable String fileName, @Nullable String sourceURL); public native void callFunction(int moduleId, int methodId, NativeArray arguments); public native void invokeCallback(int callbackID, NativeArray arguments); public native void setGlobalVariable(String propertyName, String jsonEncodedArgument); diff --git a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManager.java b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManager.java index 1fb477b13..09e237354 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManager.java @@ -367,10 +367,18 @@ public class DevSupportManager implements NativeModuleCallExceptionHandler { } public String getSourceMapUrl() { + if (mJSAppBundleName == null) { + return ""; + } + return mDevServerHelper.getSourceMapUrl(Assertions.assertNotNull(mJSAppBundleName)); } public String getSourceUrl() { + if (mJSAppBundleName == null) { + return ""; + } + return mDevServerHelper.getSourceUrl(Assertions.assertNotNull(mJSAppBundleName)); } diff --git a/ReactAndroid/src/main/jni/react/jni/OnLoad.cpp b/ReactAndroid/src/main/jni/react/jni/OnLoad.cpp index e34cba0db..4865942f7 100644 --- a/ReactAndroid/src/main/jni/react/jni/OnLoad.cpp +++ b/ReactAndroid/src/main/jni/react/jni/OnLoad.cpp @@ -638,7 +638,7 @@ static void loadScriptFromAssets(JNIEnv* env, jobject obj, jobject assetManager, auto bridge = extractRefPtr(env, obj); auto assetNameStr = fromJString(env, assetName); - env->CallStaticVoidMethod(markerClass, gLogMarkerMethod, env->NewStringUTF("loadScriptFromNetworkCached_start")); + env->CallStaticVoidMethod(markerClass, gLogMarkerMethod, env->NewStringUTF("loadScriptFromAssets_start")); auto script = react::loadScriptFromAssets(env, assetManager, assetNameStr); #ifdef WITH_FBSYSTRACE FbSystraceSection s(TRACE_TAG_REACT_CXX_BRIDGE, "reactbridge_jni_" @@ -646,30 +646,27 @@ static void loadScriptFromAssets(JNIEnv* env, jobject obj, jobject assetManager, "assetName", assetNameStr); #endif - env->CallStaticVoidMethod(markerClass, gLogMarkerMethod, env->NewStringUTF("loadScriptFromNetworkCached_read")); + env->CallStaticVoidMethod(markerClass, gLogMarkerMethod, env->NewStringUTF("loadScriptFromAssets_read")); executeApplicationScript(bridge, script, assetNameStr); - env->CallStaticVoidMethod(markerClass, gLogMarkerMethod, env->NewStringUTF("loadScriptFromNetworkCached_done")); + env->CallStaticVoidMethod(markerClass, gLogMarkerMethod, env->NewStringUTF("loadScriptFromAssets_done")); } -static void loadScriptFromNetworkCached(JNIEnv* env, jobject obj, jstring sourceURL, - jstring tempFileName) { +static void loadScriptFromFile(JNIEnv* env, jobject obj, jstring fileName, jstring sourceURL) { jclass markerClass = env->FindClass("com/facebook/react/bridge/ReactMarker"); auto bridge = jni::extractRefPtr(env, obj); - std::string script = ""; - env->CallStaticVoidMethod(markerClass, gLogMarkerMethod, env->NewStringUTF("loadScriptFromNetworkCached_start")); - if (tempFileName != NULL) { - script = react::loadScriptFromFile(jni::fromJString(env, tempFileName)); - } + auto fileNameStr = fileName == NULL ? "" : fromJString(env, fileName); + env->CallStaticVoidMethod(markerClass, gLogMarkerMethod, env->NewStringUTF("loadScriptFromFile_start")); + auto script = fileName == NULL ? "" : react::loadScriptFromFile(fileNameStr); #ifdef WITH_FBSYSTRACE - auto sourceURLStr = fromJString(env, sourceURL); + auto sourceURLStr = sourceURL == NULL ? fileNameStr : fromJString(env, sourceURL); FbSystraceSection s(TRACE_TAG_REACT_CXX_BRIDGE, "reactbridge_jni_" "executeApplicationScript", "sourceURL", sourceURLStr); #endif - env->CallStaticVoidMethod(markerClass, gLogMarkerMethod, env->NewStringUTF("loadScriptFromNetworkCached_read")); + env->CallStaticVoidMethod(markerClass, gLogMarkerMethod, env->NewStringUTF("loadScriptFromFile_read")); executeApplicationScript(bridge, script, jni::fromJString(env, sourceURL)); - env->CallStaticVoidMethod(markerClass, gLogMarkerMethod, env->NewStringUTF("loadScriptFromNetworkCached_exec")); + env->CallStaticVoidMethod(markerClass, gLogMarkerMethod, env->NewStringUTF("loadScriptFromFile_exec")); } static void callFunction(JNIEnv* env, jobject obj, jint moduleId, jint methodId, @@ -821,7 +818,7 @@ extern "C" JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) { makeNativeMethod( "loadScriptFromAssets", "(Landroid/content/res/AssetManager;Ljava/lang/String;)V", bridge::loadScriptFromAssets), - makeNativeMethod("loadScriptFromNetworkCached", bridge::loadScriptFromNetworkCached), + makeNativeMethod("loadScriptFromFile", bridge::loadScriptFromFile), makeNativeMethod("callFunction", bridge::callFunction), makeNativeMethod("invokeCallback", bridge::invokeCallback), makeNativeMethod("setGlobalVariable", bridge::setGlobalVariable),