Add ability to load “unbundles” to android

Summary:
public

This adds the ability to load “unbundles” in RN android apps. Unbundles are created by invoking the packager with the `unbundle` command rather than `bundle`.

The code detects usage of an “unbundle” by checking for the existence of a specific asset.

Reviewed By: astreet

Differential Revision: D2739596

fb-gh-sync-id: d0813c003fe0fa7b47798b970f56707079bfa5d7
This commit is contained in:
David Aurelio 2016-01-21 13:57:32 -08:00 committed by facebook-github-bot-3
parent 0963192b18
commit 17e1ceb543
18 changed files with 313 additions and 27 deletions

View File

@ -9,6 +9,7 @@ LOCAL_SRC_FILES := \
JSCExecutor.cpp \
JSCHelpers.cpp \
JSCWebWorker.cpp \
JSModulesUnbundle.cpp \
MethodCall.cpp \
Value.cpp \

View File

@ -27,6 +27,13 @@ public:
m_jsExecutor->executeApplicationScript(script, sourceURL);
}
void loadApplicationUnbundle(
JSModulesUnbundle&& unbundle,
const std::string& startupCode,
const std::string& sourceURL) {
m_jsExecutor->loadApplicationUnbundle(std::move(unbundle), startupCode, sourceURL);
}
void flush() {
auto returnedJSON = m_jsExecutor->flush();
m_callback(parseMethodCalls(returnedJSON), true /* = isEndOfBatch */);
@ -95,6 +102,13 @@ void Bridge::executeApplicationScript(const std::string& script, const std::stri
m_threadState->executeApplicationScript(script, sourceURL);
}
void Bridge::loadApplicationUnbundle(
JSModulesUnbundle&& unbundle,
const std::string& startupCode,
const std::string& sourceURL) {
m_threadState->loadApplicationUnbundle(std::move(unbundle), startupCode, sourceURL);
}
void Bridge::flush() {
if (*m_destroyed) {
return;

View File

@ -11,6 +11,7 @@
#include "Value.h"
#include "Executor.h"
#include "MethodCall.h"
#include "JSModulesUnbundle.h"
namespace folly {
@ -45,7 +46,20 @@ public:
*/
void invokeCallback(const double callbackId, const folly::dynamic& args);
/**
* Starts the JS application from an "bundle", i.e. a JavaScript file that
* contains code for all modules and a runtime that resolves and
* executes modules.
*/
void executeApplicationScript(const std::string& script, const std::string& sourceURL);
/**
* Starts the JS application from an "unbundle", i.e. a backend that stores
* and injects each module as individual file.
*/
void loadApplicationUnbundle(
JSModulesUnbundle&& unbundle,
const std::string& startupCode,
const std::string& sourceURL);
void setGlobalVariable(const std::string& propName, const std::string& jsonValue);
bool supportsProfiling();
void startProfiler(const std::string& title);

View File

@ -6,6 +6,7 @@
#include <vector>
#include <memory>
#include <jni/Countable.h>
#include "JSModulesUnbundle.h"
namespace folly {
@ -35,6 +36,14 @@ public:
const std::string& script,
const std::string& sourceURL) = 0;
/**
* Add an application "unbundle" file
*/
virtual void loadApplicationUnbundle(
JSModulesUnbundle&& bundle,
const std::string& startupCode,
const std::string& sourceURL) = 0;
/**
* Executes BatchedBridge.flushedQueue in JS to get the next queue of changes.
*/

View File

@ -14,6 +14,7 @@
#include "Value.h"
#include "jni/JMessageQueueThread.h"
#include "jni/OnLoad.h"
#include <react/JSCHelpers.h>
#ifdef WITH_JSC_EXTRA_TRACING
#include <react/JSCTracing.h>
@ -152,12 +153,24 @@ void JSCExecutor::executeApplicationScript(
env->DeleteLocalRef(startStringMarker);
env->DeleteLocalRef(endStringMarker);
String jsSourceURL(sourceURL.c_str());
#ifdef WITH_FBSYSTRACE
FbSystraceSection s(TRACE_TAG_REACT_CXX_BRIDGE, "JSCExecutor::executeApplicationScript",
"sourceURL", sourceURL);
#endif
evaluateScript(m_context, jsScript, jsSourceURL);
evaluateScript(m_context, jsScript, String(sourceURL.c_str()));
}
void JSCExecutor::loadApplicationUnbundle(
JSModulesUnbundle&& unbundle,
const std::string& startupCode,
const std::string& sourceURL) {
m_unbundle = std::move(unbundle);
if (!m_isUnbundleInitialized) {
m_isUnbundleInitialized = true;
installGlobalFunction(m_context, "nativeRequire", nativeRequire);
}
executeApplicationScript(startupCode, sourceURL);
}
std::string JSCExecutor::flush() {
@ -238,6 +251,13 @@ void JSCExecutor::flushQueueImmediate(std::string queueJSON) {
m_flushImmediateCallback(queueJSON, false);
}
void JSCExecutor::loadModule(uint32_t moduleId) {
auto module = m_unbundle.getModule(moduleId);
auto sourceUrl = String::createExpectingAscii(module.name);
auto source = String::createExpectingAscii(module.code);
evaluateScript(m_context, source, sourceUrl);
}
// WebWorker impl
JSGlobalContextRef JSCExecutor::getContext() {
@ -287,12 +307,55 @@ void JSCExecutor::terminateWebWorker(int workerId) {
m_webWorkerJSObjs.erase(workerId);
}
// Native JS hooks
static JSValueRef makeInvalidModuleIdJSCException(
JSContextRef ctx,
const JSValueRef id,
JSValueRef *exception) {
std::string message = "Received invalid module ID: ";
message += String::adopt(JSValueToStringCopy(ctx, id, exception)).str();
return makeJSCException(ctx, message.c_str());
}
JSValueRef JSCExecutor::nativeRequire(
JSContextRef ctx,
JSObjectRef function,
JSObjectRef thisObject,
size_t argumentCount,
const JSValueRef arguments[],
JSValueRef *exception) {
if (argumentCount != 1) {
*exception = makeJSCException(ctx, "Got wrong number of args");
return JSValueMakeUndefined(ctx);
}
JSCExecutor *executor;
try {
executor = s_globalContextRefToJSCExecutor.at(JSContextGetGlobalContext(ctx));
} catch (std::out_of_range& e) {
*exception = makeJSCException(ctx, "Global JS context didn't map to a valid executor");
return JSValueMakeUndefined(ctx);
}
double moduleId = JSValueToNumber(ctx, arguments[0], exception);
if (moduleId <= (double) UINT32_MAX && moduleId >= 0.0) {
try {
executor->loadModule(moduleId);
} catch (JSModulesUnbundle::ModuleNotFound&) {
*exception = makeInvalidModuleIdJSCException(ctx, arguments[0], exception);
}
} else {
*exception = makeInvalidModuleIdJSCException(ctx, arguments[0], exception);
}
return JSValueMakeUndefined(ctx);
}
static JSValueRef createErrorString(JSContextRef ctx, const char *msg) {
return JSValueMakeString(ctx, String(msg));
}
// Native JS hooks
static JSValueRef nativeFlushQueueImmediate(
JSContextRef ctx,
JSObjectRef function,

View File

@ -2,6 +2,7 @@
#pragma once
#include <cstdint>
#include <memory>
#include <unordered_map>
#include <JavaScriptCore/JSContextRef.h>
@ -30,6 +31,10 @@ public:
virtual void executeApplicationScript(
const std::string& script,
const std::string& sourceURL) override;
virtual void loadApplicationUnbundle(
JSModulesUnbundle&& unbundle,
const std::string& startupCode,
const std::string& sourceURL) override;
virtual std::string flush() override;
virtual std::string callFunction(
const double moduleId,
@ -59,10 +64,13 @@ private:
std::unordered_map<int, JSCWebWorker> m_webWorkers;
std::unordered_map<int, Object> m_webWorkerJSObjs;
std::shared_ptr<JMessageQueueThread> m_messageQueueThread;
JSModulesUnbundle m_unbundle;
bool m_isUnbundleInitialized = false;
int addWebWorker(const std::string& script, JSValueRef workerRef);
void postMessageToWebWorker(int worker, JSValueRef message, JSValueRef *exn);
void terminateWebWorker(int worker);
void loadModule(uint32_t moduleId);
static JSValueRef nativeStartWorker(
JSContextRef ctx,
@ -85,6 +93,13 @@ private:
size_t argumentCount,
const JSValueRef arguments[],
JSValueRef *exception);
static JSValueRef nativeRequire(
JSContextRef ctx,
JSObjectRef function,
JSObjectRef thisObject,
size_t argumentCount,
const JSValueRef arguments[],
JSValueRef *exception);
};
} }

View File

@ -41,13 +41,14 @@ JSValueRef evaluateScript(JSContextRef context, JSStringRef script, JSStringRef
FBLOGE("Got JS Exception: %s", exceptionText.c_str());
auto line = exception.asObject().getProperty("line");
std::ostringstream lineInfo;
std::ostringstream locationInfo;
std::string file = String::adopt(source).str();
locationInfo << "(" << (file.length() ? file : "<unknown file>");
if (line != nullptr && line.isNumber()) {
lineInfo << " (line " << line.asInteger() << " in the generated bundle)";
} else {
lineInfo << " (no line info)";
locationInfo << ":" << line.asInteger();
}
throwJSExecutionException("%s%s", exceptionText.c_str(), lineInfo.str().c_str());
locationInfo << ")";
throwJSExecutionException("%s %s", exceptionText.c_str(), locationInfo.str().c_str());
}
return result;
}

View File

@ -8,8 +8,6 @@
#define throwJSExecutionException(...) jni::throwNewJavaException("com/facebook/react/bridge/JSExecutionException", __VA_ARGS__)
#define throwJSExecutionException(...) jni::throwNewJavaException("com/facebook/react/bridge/JSExecutionException", __VA_ARGS__)
namespace facebook {
namespace react {

View File

@ -0,0 +1,94 @@
// Copyright 2004-present Facebook. All Rights Reserved.
#include "JSModulesUnbundle.h"
#include <cstdint>
#include <fb/assert.h>
#include <libgen.h>
#include <memory>
#include <sstream>
#include <sys/endian.h>
#include <utility>
using magic_number_t = uint32_t;
const magic_number_t MAGIC_FILE_HEADER = 0xFB0BD1E5;
const std::string MAGIC_FILE_NAME = "UNBUNDLE";
namespace facebook {
namespace react {
using asset_ptr =
std::unique_ptr<AAsset, std::function<decltype(AAsset_close)>>;
static std::string jsModulesDir(const std::string& entryFile) {
std::string dir = dirname(entryFile.c_str());
// android's asset manager does not work with paths that start with a dot
return dir == "." ? "js-modules/" : dir + "/js-modules/";
}
static asset_ptr openAsset(
AAssetManager *manager,
const std::string& fileName,
int mode = AASSET_MODE_STREAMING) {
return asset_ptr(
AAssetManager_open(manager, fileName.c_str(), mode),
AAsset_close);
}
JSModulesUnbundle::JSModulesUnbundle(AAssetManager *assetManager, const std::string& entryFile) :
m_assetManager(assetManager),
m_moduleDirectory(jsModulesDir(entryFile)) {}
JSModulesUnbundle::JSModulesUnbundle(JSModulesUnbundle&& other) noexcept {
*this = std::move(other);
}
JSModulesUnbundle& JSModulesUnbundle::operator= (JSModulesUnbundle&& other) noexcept {
std::swap(m_assetManager, other.m_assetManager);
std::swap(m_moduleDirectory, other.m_moduleDirectory);
return *this;
}
bool JSModulesUnbundle::isUnbundle(
AAssetManager *assetManager,
const std::string& assetName) {
if (!assetManager) {
return false;
}
auto magicFileName = jsModulesDir(assetName) + MAGIC_FILE_NAME;
auto asset = openAsset(assetManager, magicFileName.c_str());
if (asset == nullptr) {
return false;
}
magic_number_t fileHeader = 0;
AAsset_read(asset.get(), &fileHeader, sizeof(fileHeader));
return fileHeader == htole32(MAGIC_FILE_HEADER);
}
JSModulesUnbundle::Module JSModulesUnbundle::getModule(uint32_t moduleId) const {
// can be nullptr for default constructor.
FBASSERTMSGF(m_assetManager != nullptr, "Unbundle has not been initialized with an asset manager");
std::ostringstream sourceUrlBuilder;
sourceUrlBuilder << moduleId << ".js";
auto sourceUrl = sourceUrlBuilder.str();
auto fileName = m_moduleDirectory + sourceUrl;
auto asset = openAsset(m_assetManager, fileName, AASSET_MODE_BUFFER);
const char *buffer = nullptr;
if (asset != nullptr) {
buffer = static_cast<const char *>(AAsset_getBuffer(asset.get()));
}
if (buffer == nullptr) {
throw ModuleNotFound("Module not found: " + sourceUrl);
}
return {sourceUrl, std::string(buffer, AAsset_getLength(asset.get()))};
}
}
}

View File

@ -0,0 +1,48 @@
// Copyright 2004-present Facebook. All Rights Reserved.
#pragma once
#include <android/asset_manager.h>
#include <cstdint>
#include <fb/noncopyable.h>
#include <string>
#include <stdexcept>
namespace facebook {
namespace react {
class JSModulesUnbundle : noncopyable {
/**
* Represents the set of JavaScript modules that the application consists of.
* The source code of each module can be retrieved by module ID.
* This implementation reads modules as single file from the assets of an apk.
*
* The class is non-copyable because copying instances might involve copying
* several megabytes of memory.
*/
public:
class ModuleNotFound : public std::out_of_range {
using std::out_of_range::out_of_range;
};
struct Module {
std::string name;
std::string code;
};
JSModulesUnbundle() = default;
JSModulesUnbundle(AAssetManager *assetManager, const std::string& entryFile);
JSModulesUnbundle(JSModulesUnbundle&& other) noexcept;
JSModulesUnbundle& operator= (JSModulesUnbundle&& other) noexcept;
static bool isUnbundle(
AAssetManager *assetManager,
const std::string& assetName);
Module getModule(uint32_t moduleId) const;
private:
AAssetManager *m_assetManager = nullptr;
std::string m_moduleDirectory;
};
}
}

View File

@ -3,7 +3,6 @@
#include "Value.h"
#include <jni/fbjni.h>
#include <fb/log.h>
#include "JSCHelpers.h"

View File

@ -2,7 +2,6 @@
#include "JSLoader.h"
#include <android/asset_manager.h>
#include <android/asset_manager_jni.h>
#include <jni/Environment.h>
#include <fstream>
@ -10,7 +9,6 @@
#include <streambuf>
#include <string>
#include <fb/log.h>
#ifdef WITH_FBSYSTRACE
#include <fbsystrace.h>
using fbsystrace::FbSystraceSection;
@ -29,19 +27,16 @@ std::string loadScriptFromAssets(std::string assetName) {
gApplicationHolderClass,
gGetApplicationMethod);
jobject assetManager = env->CallObjectMethod(application, gGetAssetManagerMethod);
return loadScriptFromAssets(env, assetManager, assetName);
return loadScriptFromAssets(AAssetManager_fromJava(env, assetManager), assetName);
}
std::string loadScriptFromAssets(
JNIEnv *env,
jobject assetManager,
AAssetManager *manager,
std::string assetName) {
#ifdef WITH_FBSYSTRACE
FbSystraceSection s(TRACE_TAG_REACT_CXX_BRIDGE, "reactbridge_jni_loadScriptFromAssets",
"assetName", assetName);
#endif
auto manager = AAssetManager_fromJava(env, assetManager);
if (manager) {
auto asset = AAssetManager_open(
manager,
@ -90,7 +85,7 @@ void registerJSLoaderNatives() {
jclass applicationHolderClass = env->FindClass("com/facebook/react/common/ApplicationHolder");
gApplicationHolderClass = (jclass)env->NewGlobalRef(applicationHolderClass);
gGetApplicationMethod = env->GetStaticMethodID(applicationHolderClass, "getApplication", "()Landroid/app/Application;");
jclass appClass = env->FindClass("android/app/Application");
gGetAssetManagerMethod = env->GetMethodID(appClass, "getAssets", "()Landroid/content/res/AssetManager;");
}

View File

@ -2,6 +2,7 @@
#pragma once
#include <android/asset_manager.h>
#include <string>
#include <jni.h>
@ -17,7 +18,7 @@ std::string loadScriptFromAssets(std::string assetName);
/**
* Helper method for loading JS script from android asset
*/
std::string loadScriptFromAssets(JNIEnv *env, jobject assetManager, std::string assetName);
std::string loadScriptFromAssets(AAssetManager *assetManager, std::string assetName);
/**
* Helper method for loading JS script from a file

View File

@ -1,5 +1,6 @@
// Copyright 2004-present Facebook. All Rights Reserved.
#include <android/asset_manager_jni.h>
#include <android/input.h>
#include <fb/log.h>
#include <folly/json.h>
@ -13,6 +14,7 @@
#include <react/Bridge.h>
#include <react/Executor.h>
#include <react/JSCExecutor.h>
#include <react/JSModulesUnbundle.h>
#include "JNativeRunnable.h"
#include "JSLoader.h"
#include "ReadableNativeArray.h"
@ -643,15 +645,33 @@ static void executeApplicationScript(
}
}
static void loadApplicationUnbundle(
const RefPtr<Bridge>& bridge,
AAssetManager *assetManager,
const std::string& startupCode,
const std::string& startupFileName) {
try {
// Load the application unbundle and collect/dispatch any native calls that might have occured
bridge->loadApplicationUnbundle(
JSModulesUnbundle(assetManager, startupFileName),
startupCode,
startupFileName);
bridge->flush();
} catch (...) {
translatePendingCppExceptionToJavaException();
}
}
static void loadScriptFromAssets(JNIEnv* env, jobject obj, jobject assetManager,
jstring assetName) {
jclass markerClass = env->FindClass("com/facebook/react/bridge/ReactMarker");
auto manager = AAssetManager_fromJava(env, assetManager);
auto bridge = extractRefPtr<Bridge>(env, obj);
auto assetNameStr = fromJString(env, assetName);
env->CallStaticVoidMethod(markerClass, gLogMarkerMethod, env->NewStringUTF("loadScriptFromAssets_start"));
auto script = react::loadScriptFromAssets(env, assetManager, assetNameStr);
auto script = react::loadScriptFromAssets(manager, assetNameStr);
#ifdef WITH_FBSYSTRACE
FbSystraceSection s(TRACE_TAG_REACT_CXX_BRIDGE, "reactbridge_jni_"
"executeApplicationScript",
@ -659,7 +679,11 @@ static void loadScriptFromAssets(JNIEnv* env, jobject obj, jobject assetManager,
#endif
env->CallStaticVoidMethod(markerClass, gLogMarkerMethod, env->NewStringUTF("loadScriptFromAssets_read"));
executeApplicationScript(bridge, script, assetNameStr);
if (JSModulesUnbundle::isUnbundle(manager, assetNameStr)) {
loadApplicationUnbundle(bridge, manager, script, assetNameStr);
} else {
executeApplicationScript(bridge, script, assetNameStr);
}
if (env->ExceptionCheck()) {
return;
}

View File

@ -2,6 +2,7 @@
#include "ProxyExecutor.h"
#include <fb/assert.h>
#include <jni/Environment.h>
#include <jni/LocalReference.h>
#include <jni/LocalString.h>
@ -50,6 +51,11 @@ void ProxyExecutor::executeApplicationScript(
jni::make_jstring(sourceURL).get());
}
void ProxyExecutor::loadApplicationUnbundle(JSModulesUnbundle&&, const std::string&, const std::string&) {
jni::throwNewJavaException(
"java/lang/UnsupportedOperationException",
"Loading application unbundles is not supported for proxy executors");
}
std::string ProxyExecutor::flush() {
return executeJSCallWithProxy(m_executor.get(), "flushedQueue", std::vector<folly::dynamic>());

View File

@ -32,6 +32,10 @@ public:
virtual void executeApplicationScript(
const std::string& script,
const std::string& sourceURL) override;
virtual void loadApplicationUnbundle(
JSModulesUnbundle&& bundle,
const std::string& startupCode,
const std::string& sourceURL) override;
virtual std::string flush() override;
virtual std::string callFunction(
const double moduleId,

View File

@ -76,7 +76,7 @@ function uInt32Buffer(n) {
function buildModuleTable(buffers) {
// table format:
// - table_length: uint_32 length of all table entries in bytes
// - table_length: uint_32 length of all table entries in bytes + the table length itself
// - entries: entry...
//
// entry:

View File

@ -1,6 +1,6 @@
'use strict';
const {ErrorUtils, __nativeRequire} = global;
const {ErrorUtils, nativeRequire} = global;
global.require = require;
global.__d = define;
@ -32,7 +32,7 @@ function guardedLoadModule(moduleId, module) {
function loadModuleImplementation(moduleId, module) {
if (!module) {
__nativeRequire(moduleId);
nativeRequire(moduleId);
module = modules[moduleId];
}