Sync diff : Enable initializing react context off UI thread

Reviewed By: @astreet

Differential Revision: D2480130
This commit is contained in:
Olivier Notteghem 2015-09-25 12:52:10 -07:00 committed by facebook-github-bot-2
parent 8f13560fd5
commit dcae4bada0
3 changed files with 146 additions and 58 deletions

View File

@ -16,6 +16,7 @@ import java.util.List;
import android.app.Application; import android.app.Application;
import android.content.Context; import android.content.Context;
import android.os.AsyncTask;
import android.os.Bundle; import android.os.Bundle;
import android.view.View; import android.view.View;
@ -72,6 +73,8 @@ public class ReactInstanceManager {
/* should only be accessed from main thread (UI thread) */ /* should only be accessed from main thread (UI thread) */
private final List<ReactRootView> mAttachedRootViews = new ArrayList<>(); private final List<ReactRootView> mAttachedRootViews = new ArrayList<>();
private LifecycleState mLifecycleState; private LifecycleState mLifecycleState;
private boolean mIsContextInitAsyncTaskRunning;
private @Nullable ReactContextInitParams mPendingReactContextInitParams;
/* accessed from any thread */ /* accessed from any thread */
private final @Nullable String mBundleAssetName; /* name of JS bundle file in assets folder */ private final @Nullable String mBundleAssetName; /* name of JS bundle file in assets folder */
@ -111,6 +114,65 @@ public class ReactInstanceManager {
} }
}; };
private class ReactContextInitParams {
private final JavaScriptExecutor mJsExecutor;
private final JSBundleLoader mJsBundleLoader;
public ReactContextInitParams(
JavaScriptExecutor jsExecutor,
JSBundleLoader jsBundleLoader) {
mJsExecutor = Assertions.assertNotNull(jsExecutor);
mJsBundleLoader = Assertions.assertNotNull(jsBundleLoader);
}
public JavaScriptExecutor getJsExecutor() {
return mJsExecutor;
}
public JSBundleLoader getJsBundleLoader() {
return mJsBundleLoader;
}
}
/*
* Task class responsible for (re)creating react context in the background. These tasks can only
* be executing one at time, see {@link #recreateReactContextInBackground()}.
*/
private final class ReactContextInitAsyncTask extends
AsyncTask<ReactContextInitParams, Void, ReactApplicationContext> {
@Override
protected void onPreExecute() {
if (mCurrentReactContext != null) {
tearDownReactContext(mCurrentReactContext);
mCurrentReactContext = null;
}
}
@Override
protected ReactApplicationContext doInBackground(ReactContextInitParams... params) {
Assertions.assertCondition(params != null && params.length > 0 && params[0] != null);
return createReactContext(params[0].getJsExecutor(), params[0].getJsBundleLoader());
}
@Override
protected void onPostExecute(ReactApplicationContext reactContext) {
try {
setupReactContext(reactContext);
} finally {
mIsContextInitAsyncTaskRunning = false;
}
// Handle enqueued request to re-initialize react context.
if (mPendingReactContextInitParams != null) {
recreateReactContextInBackground(
mPendingReactContextInitParams.getJsExecutor(),
mPendingReactContextInitParams.getJsBundleLoader());
mPendingReactContextInitParams = null;
}
}
}
private ReactInstanceManager( private ReactInstanceManager(
Context applicationContext, Context applicationContext,
@Nullable String bundleAssetName, @Nullable String bundleAssetName,
@ -161,10 +223,35 @@ public class ReactInstanceManager {
SoLoader.init(applicationContext, /* native exopackage */ false); SoLoader.init(applicationContext, /* native exopackage */ false);
} }
/**
* 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 (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
mDevSupportManager.handleReloadJS();
return;
}
}
// Use JS file from assets
recreateReactContextInBackground(
new JSCJavaScriptExecutor(),
JSBundleLoader.createAssetLoader(
mApplicationContext.getAssets(),
mBundleAssetName));
}
/** /**
* This method will give JS the opportunity to consume the back button event. If JS does not * This method will give JS the opportunity to consume the back button event. If JS does not
* consume the event, mDefaultBackButtonImpl will be invoked at the end of the round trip * consume the event, mDefaultBackButtonImpl will be invoked at the end of the round trip to JS.
* to JS.
*/ */
public void onBackPressed() { public void onBackPressed() {
UiThreadUtil.assertOnUiThread(); UiThreadUtil.assertOnUiThread();
@ -255,18 +342,26 @@ public class ReactInstanceManager {
/** /**
* Attach given {@param rootView} to a catalyst instance manager and start JS application using * Attach given {@param rootView} to a catalyst instance manager and start JS application using
* JS module provided by {@link ReactRootView#getJSModuleName}. This view will then be tracked * JS module provided by {@link ReactRootView#getJSModuleName}. If the react context is currently
* by this manager and in case of catalyst instance restart it will be re-attached. * being (re)-created, or if react context has not been created yet, the JS application associated
* with the provided root view will be started asynchronously, i.e this method won't block.
* This view will then be tracked by this manager and in case of catalyst instance restart it will
* be re-attached.
*/ */
/* package */ void attachMeasuredRootView(ReactRootView rootView) { /* package */ void attachMeasuredRootView(ReactRootView rootView) {
UiThreadUtil.assertOnUiThread(); UiThreadUtil.assertOnUiThread();
mAttachedRootViews.add(rootView); mAttachedRootViews.add(rootView);
// If react context is being created in the background, JS application will be started
// automatically when creation completes, as root view is part of the attached root view list.
if (!mIsContextInitAsyncTaskRunning) {
if (mCurrentReactContext == null) { if (mCurrentReactContext == null) {
initializeReactContext(); createReactContextInBackground();
} else { } else {
attachMeasuredRootViewToInstance(rootView, mCurrentReactContext.getCatalystInstance()); attachMeasuredRootViewToInstance(rootView, mCurrentReactContext.getCatalystInstance());
} }
} }
}
/** /**
* Detach given {@param rootView} from current catalyst instance. It's safe to call this method * Detach given {@param rootView} from current catalyst instance. It's safe to call this method
@ -300,53 +395,51 @@ public class ReactInstanceManager {
} }
private void onReloadWithJSDebugger(ProxyJavaScriptExecutor proxyExecutor) { private void onReloadWithJSDebugger(ProxyJavaScriptExecutor proxyExecutor) {
recreateReactContext( recreateReactContextInBackground(
proxyExecutor, proxyExecutor,
JSBundleLoader.createRemoteDebuggerBundleLoader( JSBundleLoader.createRemoteDebuggerBundleLoader(
mDevSupportManager.getJSBundleURLForRemoteDebugging())); mDevSupportManager.getJSBundleURLForRemoteDebugging()));
} }
private void onJSBundleLoadedFromServer() { private void onJSBundleLoadedFromServer() {
recreateReactContext( recreateReactContextInBackground(
new JSCJavaScriptExecutor(), new JSCJavaScriptExecutor(),
JSBundleLoader.createCachedBundleFromNetworkLoader( JSBundleLoader.createCachedBundleFromNetworkLoader(
mDevSupportManager.getSourceUrl(), mDevSupportManager.getSourceUrl(),
mDevSupportManager.getDownloadedJSBundleFile())); mDevSupportManager.getDownloadedJSBundleFile()));
} }
private void initializeReactContext() { private void recreateReactContextInBackground(
if (mUseDeveloperSupport) {
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
mDevSupportManager.handleReloadJS();
return;
}
}
// Use JS file from assets
recreateReactContext(
new JSCJavaScriptExecutor(),
JSBundleLoader.createAssetLoader(
mApplicationContext.getAssets(),
mBundleAssetName));
}
private void recreateReactContext(
JavaScriptExecutor jsExecutor, JavaScriptExecutor jsExecutor,
JSBundleLoader jsBundleLoader) { JSBundleLoader jsBundleLoader) {
UiThreadUtil.assertOnUiThread(); UiThreadUtil.assertOnUiThread();
if (mCurrentReactContext != null) {
tearDownReactContext(mCurrentReactContext); ReactContextInitParams initParams = new ReactContextInitParams(jsExecutor, jsBundleLoader);
if (!mIsContextInitAsyncTaskRunning) {
// No background task to create react context is currently running, create and execute one.
ReactContextInitAsyncTask initTask = new ReactContextInitAsyncTask();
initTask.execute(initParams);
mIsContextInitAsyncTaskRunning = true;
} else {
// Background task is currently running, queue up most recent init params to recreate context
// once task completes.
mPendingReactContextInitParams = initParams;
} }
mCurrentReactContext = createReactContext(jsExecutor, jsBundleLoader); }
private void setupReactContext(ReactApplicationContext reactContext) {
UiThreadUtil.assertOnUiThread();
Assertions.assertCondition(mCurrentReactContext == null);
mCurrentReactContext = Assertions.assertNotNull(reactContext);
CatalystInstance catalystInstance =
Assertions.assertNotNull(reactContext.getCatalystInstance());
catalystInstance.initialize();
mDevSupportManager.onNewReactContextCreated(reactContext);
moveReactContextToCurrentLifecycleState(reactContext);
for (ReactRootView rootView : mAttachedRootViews) { for (ReactRootView rootView : mAttachedRootViews) {
attachMeasuredRootViewToInstance( attachMeasuredRootViewToInstance(rootView, catalystInstance);
rootView,
mCurrentReactContext.getCatalystInstance());
} }
} }
@ -430,10 +523,6 @@ public class ReactInstanceManager {
} }
reactContext.initializeWithInstance(catalystInstance); reactContext.initializeWithInstance(catalystInstance);
catalystInstance.initialize();
mDevSupportManager.onNewReactContextCreated(reactContext);
moveReactContextToCurrentLifecycleState(reactContext);
return reactContext; return reactContext;
} }

View File

@ -78,10 +78,10 @@ import okio.Sink;
private final DevInternalSettings mSettings; private final DevInternalSettings mSettings;
private final OkHttpClient mClient; private final OkHttpClient mClient;
private final Handler mRestartOnChangePollingHandler;
private boolean mOnChangePollingEnabled; private boolean mOnChangePollingEnabled;
private @Nullable OkHttpClient mOnChangePollingClient; private @Nullable OkHttpClient mOnChangePollingClient;
private @Nullable Handler mRestartOnChangePollingHandler;
private @Nullable OnServerContentChangeListener mOnServerContentChangeListener; private @Nullable OnServerContentChangeListener mOnServerContentChangeListener;
public DevServerHelper(DevInternalSettings settings) { public DevServerHelper(DevInternalSettings settings) {
@ -92,6 +92,7 @@ import okio.Sink;
// No read or write timeouts by default // No read or write timeouts by default
mClient.setReadTimeout(0, TimeUnit.MILLISECONDS); mClient.setReadTimeout(0, TimeUnit.MILLISECONDS);
mClient.setWriteTimeout(0, TimeUnit.MILLISECONDS); mClient.setWriteTimeout(0, TimeUnit.MILLISECONDS);
mRestartOnChangePollingHandler = new Handler();
} }
/** Intent action for reloading the JS */ /** Intent action for reloading the JS */
@ -193,10 +194,7 @@ import okio.Sink;
public void stopPollingOnChangeEndpoint() { public void stopPollingOnChangeEndpoint() {
mOnChangePollingEnabled = false; mOnChangePollingEnabled = false;
if (mRestartOnChangePollingHandler != null) {
mRestartOnChangePollingHandler.removeCallbacksAndMessages(null); mRestartOnChangePollingHandler.removeCallbacksAndMessages(null);
mRestartOnChangePollingHandler = null;
}
if (mOnChangePollingClient != null) { if (mOnChangePollingClient != null) {
mOnChangePollingClient.cancel(this); mOnChangePollingClient.cancel(this);
mOnChangePollingClient = null; mOnChangePollingClient = null;
@ -216,7 +214,6 @@ import okio.Sink;
mOnChangePollingClient mOnChangePollingClient
.setConnectionPool(new ConnectionPool(1, LONG_POLL_KEEP_ALIVE_DURATION_MS)) .setConnectionPool(new ConnectionPool(1, LONG_POLL_KEEP_ALIVE_DURATION_MS))
.setConnectTimeout(HTTP_CONNECT_TIMEOUT_MS, TimeUnit.MILLISECONDS); .setConnectTimeout(HTTP_CONNECT_TIMEOUT_MS, TimeUnit.MILLISECONDS);
mRestartOnChangePollingHandler = new Handler();
enqueueOnChangeEndpointLongPolling(); enqueueOnChangeEndpointLongPolling();
} }
@ -246,7 +243,7 @@ import okio.Sink;
// of a failure, so that we don't flood network queue with frequent requests in case when // of a failure, so that we don't flood network queue with frequent requests in case when
// dev server is down // dev server is down
FLog.d(ReactConstants.TAG, "Error while requesting /onchange endpoint", e); FLog.d(ReactConstants.TAG, "Error while requesting /onchange endpoint", e);
Assertions.assertNotNull(mRestartOnChangePollingHandler).postDelayed( mRestartOnChangePollingHandler.postDelayed(
new Runnable() { new Runnable() {
@Override @Override
public void run() { public void run() {

View File

@ -82,7 +82,8 @@ public final class Timing extends ReactContextBaseJavaModule implements Lifecycl
Assertions.assertNotNull(mJSTimersModule).callTimers(timersToCall); Assertions.assertNotNull(mJSTimersModule).callTimers(timersToCall);
} }
mReactChoreographer.postFrameCallback(ReactChoreographer.CallbackType.TIMERS_EVENTS, this); Assertions.assertNotNull(mReactChoreographer)
.postFrameCallback(ReactChoreographer.CallbackType.TIMERS_EVENTS, this);
} }
} }
@ -90,14 +91,13 @@ public final class Timing extends ReactContextBaseJavaModule implements Lifecycl
private final PriorityQueue<Timer> mTimers; private final PriorityQueue<Timer> mTimers;
private final SparseArray<Timer> mTimerIdsToTimers; private final SparseArray<Timer> mTimerIdsToTimers;
private final AtomicBoolean isPaused = new AtomicBoolean(false); private final AtomicBoolean isPaused = new AtomicBoolean(false);
private final ReactChoreographer mReactChoreographer;
private final FrameCallback mFrameCallback = new FrameCallback(); private final FrameCallback mFrameCallback = new FrameCallback();
private @Nullable ReactChoreographer mReactChoreographer;
private @Nullable JSTimersExecution mJSTimersModule; private @Nullable JSTimersExecution mJSTimersModule;
private boolean mFrameCallbackPosted = false; private boolean mFrameCallbackPosted = false;
public Timing(ReactApplicationContext reactContext) { public Timing(ReactApplicationContext reactContext) {
super(reactContext); super(reactContext);
mReactChoreographer = ReactChoreographer.getInstance();
// We store timers sorted by finish time. // We store timers sorted by finish time.
mTimers = new PriorityQueue<Timer>( mTimers = new PriorityQueue<Timer>(
11, // Default capacity: for some reason they don't expose a (Comparator) constructor 11, // Default capacity: for some reason they don't expose a (Comparator) constructor
@ -119,6 +119,8 @@ public final class Timing extends ReactContextBaseJavaModule implements Lifecycl
@Override @Override
public void initialize() { public void initialize() {
// Safe to acquire choreographer here, as initialize() is invoked from UI thread.
mReactChoreographer = ReactChoreographer.getInstance();
mJSTimersModule = getReactApplicationContext().getCatalystInstance() mJSTimersModule = getReactApplicationContext().getCatalystInstance()
.getJSModule(JSTimersExecution.class); .getJSModule(JSTimersExecution.class);
getReactApplicationContext().addLifecycleEventListener(this); getReactApplicationContext().addLifecycleEventListener(this);
@ -151,7 +153,7 @@ public final class Timing extends ReactContextBaseJavaModule implements Lifecycl
private void setChoreographerCallback() { private void setChoreographerCallback() {
if (!mFrameCallbackPosted) { if (!mFrameCallbackPosted) {
mReactChoreographer.postFrameCallback( Assertions.assertNotNull(mReactChoreographer).postFrameCallback(
ReactChoreographer.CallbackType.TIMERS_EVENTS, ReactChoreographer.CallbackType.TIMERS_EVENTS,
mFrameCallback); mFrameCallback);
mFrameCallbackPosted = true; mFrameCallbackPosted = true;
@ -160,7 +162,7 @@ public final class Timing extends ReactContextBaseJavaModule implements Lifecycl
private void clearChoreographerCallback() { private void clearChoreographerCallback() {
if (mFrameCallbackPosted) { if (mFrameCallbackPosted) {
mReactChoreographer.removeFrameCallback( Assertions.assertNotNull(mReactChoreographer).removeFrameCallback(
ReactChoreographer.CallbackType.TIMERS_EVENTS, ReactChoreographer.CallbackType.TIMERS_EVENTS,
mFrameCallback); mFrameCallback);
mFrameCallbackPosted = false; mFrameCallbackPosted = false;