Hook up native delta client
Summary: Adds support for native clients to `ReactAndroid`: - `.devsupport.BundleDeltaClient` is now abstract with two implementations: the existing Java client, and a native client - `BundleDeltaClient#processDelta(...)` can now return a native delta client object - if that client object is non-null, the bridge is started up with that client rather than a script written to disk Reviewed By: fromcelticpark Differential Revision: D7845135 fbshipit-source-id: 379a9c6f9319c62eec3c370cda9ffa0969266a29
This commit is contained in:
parent
8f85abdb14
commit
dd036c2328
|
@ -50,6 +50,7 @@ import com.facebook.react.bridge.JSIModulesProvider;
|
|||
import com.facebook.react.bridge.JavaJSExecutor;
|
||||
import com.facebook.react.bridge.JavaScriptExecutor;
|
||||
import com.facebook.react.bridge.JavaScriptExecutorFactory;
|
||||
import com.facebook.react.bridge.NativeDeltaClient;
|
||||
import com.facebook.react.bridge.NativeModuleCallExceptionHandler;
|
||||
import com.facebook.react.bridge.NativeModuleRegistry;
|
||||
import com.facebook.react.bridge.NotThreadSafeBridgeIdleDebugListener;
|
||||
|
@ -84,6 +85,7 @@ import com.facebook.react.views.imagehelper.ResourceDrawableIdHelper;
|
|||
import com.facebook.soloader.SoLoader;
|
||||
import com.facebook.systrace.Systrace;
|
||||
import com.facebook.systrace.SystraceMessage;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
|
@ -270,8 +272,8 @@ public class ReactInstanceManager {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void onJSBundleLoadedFromServer() {
|
||||
ReactInstanceManager.this.onJSBundleLoadedFromServer();
|
||||
public void onJSBundleLoadedFromServer(@Nullable NativeDeltaClient nativeDeltaClient) {
|
||||
ReactInstanceManager.this.onJSBundleLoadedFromServer(nativeDeltaClient);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -360,7 +362,7 @@ public class ReactInstanceManager {
|
|||
!devSettings.isRemoteJSDebugEnabled()) {
|
||||
// If there is a up-to-date bundle downloaded from server,
|
||||
// with remote JS debugging disabled, always use that.
|
||||
onJSBundleLoadedFromServer();
|
||||
onJSBundleLoadedFromServer(null);
|
||||
} else if (mBundleLoader == null) {
|
||||
mDevSupportManager.handleReloadJS();
|
||||
} else {
|
||||
|
@ -848,12 +850,17 @@ public class ReactInstanceManager {
|
|||
}
|
||||
|
||||
@ThreadConfined(UI)
|
||||
private void onJSBundleLoadedFromServer() {
|
||||
private void onJSBundleLoadedFromServer(@Nullable NativeDeltaClient nativeDeltaClient) {
|
||||
Log.d(ReactConstants.TAG, "ReactInstanceManager.onJSBundleLoadedFromServer()");
|
||||
recreateReactContextInBackground(
|
||||
mJavaScriptExecutorFactory,
|
||||
JSBundleLoader.createCachedBundleFromNetworkLoader(
|
||||
mDevSupportManager.getSourceUrl(), mDevSupportManager.getDownloadedJSBundleFile()));
|
||||
|
||||
JSBundleLoader bundleLoader = nativeDeltaClient == null
|
||||
? JSBundleLoader.createCachedBundleFromNetworkLoader(
|
||||
mDevSupportManager.getSourceUrl(),
|
||||
mDevSupportManager.getDownloadedJSBundleFile())
|
||||
: JSBundleLoader.createDeltaFromNetworkLoader(
|
||||
mDevSupportManager.getSourceUrl(), nativeDeltaClient);
|
||||
|
||||
recreateReactContextInBackground(mJavaScriptExecutorFactory, bundleLoader);
|
||||
}
|
||||
|
||||
@ThreadConfined(UI)
|
||||
|
|
|
@ -1,54 +1,106 @@
|
|||
/**
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
package com.facebook.react.devsupport;
|
||||
|
||||
import android.util.JsonReader;
|
||||
import android.util.JsonToken;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.util.LinkedHashMap;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import android.util.JsonReader;
|
||||
import android.util.JsonToken;
|
||||
import android.util.Pair;
|
||||
import com.facebook.react.bridge.NativeDeltaClient;
|
||||
import okhttp3.Headers;
|
||||
import okio.Buffer;
|
||||
import okio.BufferedSource;
|
||||
|
||||
public class BundleDeltaClient {
|
||||
public abstract class BundleDeltaClient {
|
||||
|
||||
final LinkedHashMap<Number, byte[]> mPreModules = new LinkedHashMap<Number, byte[]>();
|
||||
final LinkedHashMap<Number, byte[]> mDeltaModules = new LinkedHashMap<Number, byte[]>();
|
||||
final LinkedHashMap<Number, byte[]> mPostModules = new LinkedHashMap<Number, byte[]>();
|
||||
@Nullable String mDeltaId;
|
||||
private static final String METRO_DELTA_ID_HEADER = "X-Metro-Delta-ID";
|
||||
@Nullable private String mDeltaId;
|
||||
|
||||
public enum ClientType {
|
||||
NONE,
|
||||
DEV_SUPPORT,
|
||||
NATIVE
|
||||
}
|
||||
|
||||
static boolean isDeltaUrl(String bundleUrl) {
|
||||
return bundleUrl.indexOf(".delta?") != -1;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
static BundleDeltaClient create(ClientType type) {
|
||||
switch (type) {
|
||||
case DEV_SUPPORT:
|
||||
return new BundleDeltaJavaClient();
|
||||
case NATIVE:
|
||||
return new BundleDeltaNativeClient();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
abstract public boolean canHandle(ClientType type);
|
||||
|
||||
abstract protected Pair<Boolean, NativeDeltaClient> processDelta(
|
||||
BufferedSource body,
|
||||
File outputFile) throws IOException;
|
||||
|
||||
final public String extendUrlForDelta(String bundleURL) {
|
||||
return mDeltaId != null ? bundleURL + "&deltaBundleId=" + mDeltaId : bundleURL;
|
||||
}
|
||||
|
||||
public void reset() {
|
||||
mDeltaId = null;
|
||||
}
|
||||
|
||||
public Pair<Boolean, NativeDeltaClient> processDelta(
|
||||
Headers headers,
|
||||
BufferedSource body,
|
||||
File outputFile) throws IOException {
|
||||
|
||||
mDeltaId = headers.get(METRO_DELTA_ID_HEADER);
|
||||
return processDelta(body, outputFile);
|
||||
}
|
||||
|
||||
private static class BundleDeltaJavaClient extends BundleDeltaClient {
|
||||
|
||||
final LinkedHashMap<Number, byte[]> mPreModules = new LinkedHashMap<Number, byte[]>();
|
||||
final LinkedHashMap<Number, byte[]> mDeltaModules = new LinkedHashMap<Number, byte[]>();
|
||||
final LinkedHashMap<Number, byte[]> mPostModules = new LinkedHashMap<Number, byte[]>();
|
||||
|
||||
@Override
|
||||
public boolean canHandle(ClientType type) {
|
||||
return type == ClientType.DEV_SUPPORT;
|
||||
}
|
||||
|
||||
public void reset() {
|
||||
super.reset();
|
||||
mDeltaModules.clear();
|
||||
mPreModules.clear();
|
||||
mPostModules.clear();
|
||||
}
|
||||
|
||||
public String toDeltaUrl(String bundleURL) {
|
||||
if (isDeltaUrl(bundleURL) && mDeltaId != null) {
|
||||
return bundleURL + "&deltaBundleId=" + mDeltaId;
|
||||
}
|
||||
return bundleURL;
|
||||
}
|
||||
|
||||
public synchronized boolean storeDeltaInFile(BufferedSource body, File outputFile)
|
||||
throws IOException {
|
||||
@Override
|
||||
public synchronized Pair<Boolean, NativeDeltaClient> processDelta(
|
||||
BufferedSource body,
|
||||
File outputFile) throws IOException {
|
||||
|
||||
JsonReader jsonReader = new JsonReader(new InputStreamReader(body.inputStream()));
|
||||
|
||||
jsonReader.beginObject();
|
||||
|
||||
int numChangedModules = 0;
|
||||
|
||||
while (jsonReader.hasNext()) {
|
||||
String name = jsonReader.nextName();
|
||||
if (name.equals("id")) {
|
||||
mDeltaId = jsonReader.nextString();
|
||||
} else if (name.equals("pre")) {
|
||||
if (name.equals("pre")) {
|
||||
numChangedModules += patchDelta(jsonReader, mPreModules);
|
||||
} else if (name.equals("post")) {
|
||||
numChangedModules += patchDelta(jsonReader, mPostModules);
|
||||
|
@ -65,7 +117,7 @@ public class BundleDeltaClient {
|
|||
if (numChangedModules == 0) {
|
||||
// If we receive an empty delta, we don't need to save the file again (it'll have the
|
||||
// same content).
|
||||
return false;
|
||||
return Pair.create(Boolean.FALSE, null);
|
||||
}
|
||||
|
||||
FileOutputStream fileOutputStream = new FileOutputStream(outputFile);
|
||||
|
@ -90,7 +142,7 @@ public class BundleDeltaClient {
|
|||
fileOutputStream.close();
|
||||
}
|
||||
|
||||
return true;
|
||||
return Pair.create(Boolean.TRUE, null);
|
||||
}
|
||||
|
||||
private static int patchDelta(JsonReader jsonReader, LinkedHashMap<Number, byte[]> map)
|
||||
|
@ -119,3 +171,27 @@ public class BundleDeltaClient {
|
|||
return numModules;
|
||||
}
|
||||
}
|
||||
|
||||
private static class BundleDeltaNativeClient extends BundleDeltaClient {
|
||||
private final NativeDeltaClient nativeClient = new NativeDeltaClient();
|
||||
|
||||
@Override
|
||||
public boolean canHandle(ClientType type) {
|
||||
return type == ClientType.NATIVE;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Pair<Boolean, NativeDeltaClient> processDelta(
|
||||
BufferedSource body,
|
||||
File outputFile) throws IOException {
|
||||
nativeClient.processDelta(body);
|
||||
return Pair.create(Boolean.FALSE, nativeClient);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset() {
|
||||
super.reset();
|
||||
nativeClient.reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,8 +8,10 @@
|
|||
package com.facebook.react.devsupport;
|
||||
|
||||
import android.util.Log;
|
||||
import android.util.Pair;
|
||||
import com.facebook.common.logging.FLog;
|
||||
import com.facebook.infer.annotation.Assertions;
|
||||
import com.facebook.react.bridge.NativeDeltaClient;
|
||||
import com.facebook.react.common.DebugServerException;
|
||||
import com.facebook.react.common.ReactConstants;
|
||||
import com.facebook.react.devsupport.interfaces.DevBundleDownloadListener;
|
||||
|
@ -40,7 +42,7 @@ public class BundleDownloader {
|
|||
|
||||
private final OkHttpClient mClient;
|
||||
|
||||
private final BundleDeltaClient mBundleDeltaClient = new BundleDeltaClient();
|
||||
private BundleDeltaClient mBundleDeltaClient;
|
||||
|
||||
private @Nullable Call mDownloadBundleFromURLCall;
|
||||
|
||||
|
@ -98,11 +100,12 @@ public class BundleDownloader {
|
|||
final DevBundleDownloadListener callback,
|
||||
final File outputFile,
|
||||
final String bundleURL,
|
||||
final @Nullable BundleInfo bundleInfo) {
|
||||
final @Nullable BundleInfo bundleInfo,
|
||||
final BundleDeltaClient.ClientType clientType) {
|
||||
|
||||
final Request request =
|
||||
new Request.Builder()
|
||||
.url(mBundleDeltaClient.toDeltaUrl(bundleURL))
|
||||
.url(formatBundleUrl(bundleURL, clientType))
|
||||
// FIXME: there is a bug that makes MultipartStreamReader to never find the end of the
|
||||
// multipart message. This temporarily disables the multipart mode to work around it,
|
||||
// but
|
||||
|
@ -146,7 +149,7 @@ public class BundleDownloader {
|
|||
try (Response r = response) {
|
||||
if (match.find()) {
|
||||
processMultipartResponse(
|
||||
url, r, match.group(1), outputFile, bundleInfo, callback);
|
||||
url, r, match.group(1), outputFile, bundleInfo, clientType, callback);
|
||||
} else {
|
||||
// In case the server doesn't support multipart/mixed responses, fallback to normal
|
||||
// download.
|
||||
|
@ -157,6 +160,7 @@ public class BundleDownloader {
|
|||
Okio.buffer(r.body().source()),
|
||||
outputFile,
|
||||
bundleInfo,
|
||||
clientType,
|
||||
callback);
|
||||
}
|
||||
}
|
||||
|
@ -164,12 +168,19 @@ public class BundleDownloader {
|
|||
});
|
||||
}
|
||||
|
||||
private String formatBundleUrl(String bundleURL, BundleDeltaClient.ClientType clientType) {
|
||||
return BundleDeltaClient.isDeltaUrl(bundleURL) && mBundleDeltaClient != null && mBundleDeltaClient.canHandle(clientType)
|
||||
? mBundleDeltaClient.extendUrlForDelta(bundleURL)
|
||||
: bundleURL;
|
||||
}
|
||||
|
||||
private void processMultipartResponse(
|
||||
final String url,
|
||||
final Response response,
|
||||
String boundary,
|
||||
final File outputFile,
|
||||
@Nullable final BundleInfo bundleInfo,
|
||||
final BundleDeltaClient.ClientType clientType,
|
||||
final DevBundleDownloadListener callback)
|
||||
throws IOException {
|
||||
|
||||
|
@ -193,7 +204,7 @@ public class BundleDownloader {
|
|||
status = Integer.parseInt(headers.get("X-Http-Status"));
|
||||
}
|
||||
processBundleResult(
|
||||
url, status, Headers.of(headers), body, outputFile, bundleInfo, callback);
|
||||
url, status, Headers.of(headers), body, outputFile, bundleInfo, clientType, callback);
|
||||
} else {
|
||||
if (!headers.containsKey("Content-Type")
|
||||
|| !headers.get("Content-Type").equals("application/json")) {
|
||||
|
@ -249,6 +260,7 @@ public class BundleDownloader {
|
|||
BufferedSource body,
|
||||
File outputFile,
|
||||
BundleInfo bundleInfo,
|
||||
BundleDeltaClient.ClientType clientType,
|
||||
DevBundleDownloadListener callback)
|
||||
throws IOException {
|
||||
// Check for server errors. If the server error has the expected form, fail with more info.
|
||||
|
@ -274,24 +286,36 @@ public class BundleDownloader {
|
|||
|
||||
File tmpFile = new File(outputFile.getPath() + ".tmp");
|
||||
|
||||
boolean bundleUpdated;
|
||||
boolean bundleWritten;
|
||||
NativeDeltaClient nativeDeltaClient = null;
|
||||
|
||||
if (BundleDeltaClient.isDeltaUrl(url)) {
|
||||
// If the bundle URL has the delta extension, we need to use the delta patching logic.
|
||||
bundleUpdated = mBundleDeltaClient.storeDeltaInFile(body, tmpFile);
|
||||
BundleDeltaClient deltaClient = getBundleDeltaClient(clientType);
|
||||
Assertions.assertNotNull(deltaClient);
|
||||
Pair<Boolean, NativeDeltaClient> result = deltaClient.processDelta(headers, body, tmpFile);
|
||||
bundleWritten = result.first;
|
||||
nativeDeltaClient = result.second;
|
||||
} else {
|
||||
mBundleDeltaClient.reset();
|
||||
bundleUpdated = storePlainJSInFile(body, tmpFile);
|
||||
mBundleDeltaClient = null;
|
||||
bundleWritten = storePlainJSInFile(body, tmpFile);
|
||||
}
|
||||
|
||||
if (bundleUpdated) {
|
||||
if (bundleWritten) {
|
||||
// If we have received a new bundle from the server, move it to its final destination.
|
||||
if (!tmpFile.renameTo(outputFile)) {
|
||||
throw new IOException("Couldn't rename " + tmpFile + " to " + outputFile);
|
||||
}
|
||||
}
|
||||
|
||||
callback.onSuccess();
|
||||
callback.onSuccess(nativeDeltaClient);
|
||||
}
|
||||
|
||||
private BundleDeltaClient getBundleDeltaClient(BundleDeltaClient.ClientType clientType) {
|
||||
if (mBundleDeltaClient == null || !mBundleDeltaClient.canHandle(clientType)) {
|
||||
mBundleDeltaClient = BundleDeltaClient.create(clientType);
|
||||
}
|
||||
return mBundleDeltaClient;
|
||||
}
|
||||
|
||||
private static boolean storePlainJSInFile(BufferedSource body, File outputFile)
|
||||
|
|
|
@ -375,7 +375,17 @@ public class DevServerHelper {
|
|||
public void downloadBundleFromURL(
|
||||
DevBundleDownloadListener callback,
|
||||
File outputFile, String bundleURL, BundleDownloader.BundleInfo bundleInfo) {
|
||||
mBundleDownloader.downloadBundleFromURL(callback, outputFile, bundleURL, bundleInfo);
|
||||
mBundleDownloader.downloadBundleFromURL(callback, outputFile, bundleURL, bundleInfo, getDeltaClientType());
|
||||
}
|
||||
|
||||
private BundleDeltaClient.ClientType getDeltaClientType() {
|
||||
if (mSettings.isBundleDeltasCppEnabled()) {
|
||||
return BundleDeltaClient.ClientType.NATIVE;
|
||||
} else if (mSettings.isBundleDeltasEnabled()) {
|
||||
return BundleDeltaClient.ClientType.DEV_SUPPORT;
|
||||
} else {
|
||||
return BundleDeltaClient.ClientType.NONE;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -32,6 +32,7 @@ import com.facebook.react.bridge.CatalystInstance;
|
|||
import com.facebook.react.bridge.DefaultNativeModuleCallExceptionHandler;
|
||||
import com.facebook.react.bridge.JavaJSExecutor;
|
||||
import com.facebook.react.bridge.JavaScriptContextHolder;
|
||||
import com.facebook.react.bridge.NativeDeltaClient;
|
||||
import com.facebook.react.bridge.ReactContext;
|
||||
import com.facebook.react.bridge.ReactMarker;
|
||||
import com.facebook.react.bridge.ReactMarkerConstants;
|
||||
|
@ -973,7 +974,7 @@ public class DevSupportManagerImpl implements
|
|||
mDevServerHelper.downloadBundleFromURL(
|
||||
new DevBundleDownloadListener() {
|
||||
@Override
|
||||
public void onSuccess() {
|
||||
public void onSuccess(final @Nullable NativeDeltaClient nativeDeltaClient) {
|
||||
mDevLoadingViewController.hide();
|
||||
mDevLoadingViewVisible = false;
|
||||
synchronized (DevSupportManagerImpl.this) {
|
||||
|
@ -981,14 +982,14 @@ public class DevSupportManagerImpl implements
|
|||
mBundleStatus.updateTimestamp = System.currentTimeMillis();
|
||||
}
|
||||
if (mBundleDownloadListener != null) {
|
||||
mBundleDownloadListener.onSuccess();
|
||||
mBundleDownloadListener.onSuccess(nativeDeltaClient);
|
||||
}
|
||||
UiThreadUtil.runOnUiThread(
|
||||
new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
ReactMarker.logMarker(ReactMarkerConstants.DOWNLOAD_END, bundleInfo.toJSONString());
|
||||
mReactInstanceManagerHelper.onJSBundleLoadedFromServer();
|
||||
mReactInstanceManagerHelper.onJSBundleLoadedFromServer(nativeDeltaClient);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -8,9 +8,9 @@
|
|||
package com.facebook.react.devsupport;
|
||||
|
||||
import android.app.Activity;
|
||||
|
||||
import com.facebook.react.bridge.JavaJSExecutor;
|
||||
|
||||
import com.facebook.react.bridge.NativeDeltaClient;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/**
|
||||
|
@ -27,7 +27,7 @@ public interface ReactInstanceManagerDevHelper {
|
|||
/**
|
||||
* Notify react instance manager about new JS bundle version downloaded from the server.
|
||||
*/
|
||||
void onJSBundleLoadedFromServer();
|
||||
void onJSBundleLoadedFromServer(@Nullable NativeDeltaClient nativeDeltaClient);
|
||||
|
||||
/**
|
||||
* Request to toggle the react element inspector.
|
||||
|
|
|
@ -7,10 +7,11 @@
|
|||
|
||||
package com.facebook.react.devsupport.interfaces;
|
||||
|
||||
import com.facebook.react.bridge.NativeDeltaClient;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
public interface DevBundleDownloadListener {
|
||||
void onSuccess();
|
||||
void onSuccess(@Nullable NativeDeltaClient nativeDeltaClient);
|
||||
void onProgress(@Nullable String status, @Nullable Integer done, @Nullable Integer total);
|
||||
void onFailure(Exception cause);
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue