From dcae4bada046c68f028e59e805da7430a8539894 Mon Sep 17 00:00:00 2001 From: Olivier Notteghem Date: Fri, 25 Sep 2015 12:52:10 -0700 Subject: [PATCH] Sync diff : Enable initializing react context off UI thread Reviewed By: @astreet Differential Revision: D2480130 --- .../facebook/react/ReactInstanceManager.java | 181 +++++++++++++----- .../react/devsupport/DevServerHelper.java | 11 +- .../facebook/react/modules/core/Timing.java | 12 +- 3 files changed, 146 insertions(+), 58 deletions(-) diff --git a/ReactAndroid/src/main/java/com/facebook/react/ReactInstanceManager.java b/ReactAndroid/src/main/java/com/facebook/react/ReactInstanceManager.java index b18f88ca3..688c97294 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/ReactInstanceManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/ReactInstanceManager.java @@ -16,6 +16,7 @@ import java.util.List; import android.app.Application; import android.content.Context; +import android.os.AsyncTask; import android.os.Bundle; import android.view.View; @@ -72,6 +73,8 @@ public class ReactInstanceManager { /* should only be accessed from main thread (UI thread) */ private final List mAttachedRootViews = new ArrayList<>(); private LifecycleState mLifecycleState; + private boolean mIsContextInitAsyncTaskRunning; + private @Nullable ReactContextInitParams mPendingReactContextInitParams; /* accessed from any thread */ private final @Nullable String mBundleAssetName; /* name of JS bundle file in assets folder */ @@ -105,11 +108,70 @@ public class ReactInstanceManager { private final DefaultHardwareBackBtnHandler mBackBtnHandler = new DefaultHardwareBackBtnHandler() { - @Override - public void invokeDefaultOnBackPressed() { - ReactInstanceManager.this.invokeDefaultOnBackPressed(); + @Override + public void invokeDefaultOnBackPressed() { + ReactInstanceManager.this.invokeDefaultOnBackPressed(); + } + }; + + 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 { + + @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( Context applicationContext, @@ -161,10 +223,35 @@ public class ReactInstanceManager { 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 - * consume the event, mDefaultBackButtonImpl will be invoked at the end of the round trip - * to JS. + * consume the event, mDefaultBackButtonImpl will be invoked at the end of the round trip to JS. */ public void onBackPressed() { UiThreadUtil.assertOnUiThread(); @@ -255,16 +342,24 @@ public class ReactInstanceManager { /** * 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 - * by this manager and in case of catalyst instance restart it will be re-attached. + * JS module provided by {@link ReactRootView#getJSModuleName}. If the react context is currently + * 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) { UiThreadUtil.assertOnUiThread(); mAttachedRootViews.add(rootView); - if (mCurrentReactContext == null) { - initializeReactContext(); - } else { - attachMeasuredRootViewToInstance(rootView, mCurrentReactContext.getCatalystInstance()); + + // 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) { + createReactContextInBackground(); + } else { + attachMeasuredRootViewToInstance(rootView, mCurrentReactContext.getCatalystInstance()); + } } } @@ -300,53 +395,51 @@ public class ReactInstanceManager { } private void onReloadWithJSDebugger(ProxyJavaScriptExecutor proxyExecutor) { - recreateReactContext( + recreateReactContextInBackground( proxyExecutor, JSBundleLoader.createRemoteDebuggerBundleLoader( mDevSupportManager.getJSBundleURLForRemoteDebugging())); } private void onJSBundleLoadedFromServer() { - recreateReactContext( + recreateReactContextInBackground( new JSCJavaScriptExecutor(), JSBundleLoader.createCachedBundleFromNetworkLoader( mDevSupportManager.getSourceUrl(), mDevSupportManager.getDownloadedJSBundleFile())); } - private void initializeReactContext() { - 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( + private void recreateReactContextInBackground( JavaScriptExecutor jsExecutor, JSBundleLoader jsBundleLoader) { 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) { - attachMeasuredRootViewToInstance( - rootView, - mCurrentReactContext.getCatalystInstance()); + attachMeasuredRootViewToInstance(rootView, catalystInstance); } } @@ -430,10 +523,6 @@ public class ReactInstanceManager { } reactContext.initializeWithInstance(catalystInstance); - catalystInstance.initialize(); - mDevSupportManager.onNewReactContextCreated(reactContext); - - moveReactContextToCurrentLifecycleState(reactContext); return reactContext; } diff --git a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevServerHelper.java b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevServerHelper.java index 46260652e..99a9da485 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevServerHelper.java +++ b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevServerHelper.java @@ -78,10 +78,10 @@ import okio.Sink; private final DevInternalSettings mSettings; private final OkHttpClient mClient; + private final Handler mRestartOnChangePollingHandler; private boolean mOnChangePollingEnabled; private @Nullable OkHttpClient mOnChangePollingClient; - private @Nullable Handler mRestartOnChangePollingHandler; private @Nullable OnServerContentChangeListener mOnServerContentChangeListener; public DevServerHelper(DevInternalSettings settings) { @@ -92,6 +92,7 @@ import okio.Sink; // No read or write timeouts by default mClient.setReadTimeout(0, TimeUnit.MILLISECONDS); mClient.setWriteTimeout(0, TimeUnit.MILLISECONDS); + mRestartOnChangePollingHandler = new Handler(); } /** Intent action for reloading the JS */ @@ -193,10 +194,7 @@ import okio.Sink; public void stopPollingOnChangeEndpoint() { mOnChangePollingEnabled = false; - if (mRestartOnChangePollingHandler != null) { - mRestartOnChangePollingHandler.removeCallbacksAndMessages(null); - mRestartOnChangePollingHandler = null; - } + mRestartOnChangePollingHandler.removeCallbacksAndMessages(null); if (mOnChangePollingClient != null) { mOnChangePollingClient.cancel(this); mOnChangePollingClient = null; @@ -216,7 +214,6 @@ import okio.Sink; mOnChangePollingClient .setConnectionPool(new ConnectionPool(1, LONG_POLL_KEEP_ALIVE_DURATION_MS)) .setConnectTimeout(HTTP_CONNECT_TIMEOUT_MS, TimeUnit.MILLISECONDS); - mRestartOnChangePollingHandler = new Handler(); 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 // dev server is down FLog.d(ReactConstants.TAG, "Error while requesting /onchange endpoint", e); - Assertions.assertNotNull(mRestartOnChangePollingHandler).postDelayed( + mRestartOnChangePollingHandler.postDelayed( new Runnable() { @Override public void run() { diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/core/Timing.java b/ReactAndroid/src/main/java/com/facebook/react/modules/core/Timing.java index 326b6c58e..8b0888e08 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/modules/core/Timing.java +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/core/Timing.java @@ -82,7 +82,8 @@ public final class Timing extends ReactContextBaseJavaModule implements Lifecycl 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 mTimers; private final SparseArray mTimerIdsToTimers; private final AtomicBoolean isPaused = new AtomicBoolean(false); - private final ReactChoreographer mReactChoreographer; private final FrameCallback mFrameCallback = new FrameCallback(); + private @Nullable ReactChoreographer mReactChoreographer; private @Nullable JSTimersExecution mJSTimersModule; private boolean mFrameCallbackPosted = false; public Timing(ReactApplicationContext reactContext) { super(reactContext); - mReactChoreographer = ReactChoreographer.getInstance(); // We store timers sorted by finish time. mTimers = new PriorityQueue( 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 public void initialize() { + // Safe to acquire choreographer here, as initialize() is invoked from UI thread. + mReactChoreographer = ReactChoreographer.getInstance(); mJSTimersModule = getReactApplicationContext().getCatalystInstance() .getJSModule(JSTimersExecution.class); getReactApplicationContext().addLifecycleEventListener(this); @@ -151,7 +153,7 @@ public final class Timing extends ReactContextBaseJavaModule implements Lifecycl private void setChoreographerCallback() { if (!mFrameCallbackPosted) { - mReactChoreographer.postFrameCallback( + Assertions.assertNotNull(mReactChoreographer).postFrameCallback( ReactChoreographer.CallbackType.TIMERS_EVENTS, mFrameCallback); mFrameCallbackPosted = true; @@ -160,7 +162,7 @@ public final class Timing extends ReactContextBaseJavaModule implements Lifecycl private void clearChoreographerCallback() { if (mFrameCallbackPosted) { - mReactChoreographer.removeFrameCallback( + Assertions.assertNotNull(mReactChoreographer).removeFrameCallback( ReactChoreographer.CallbackType.TIMERS_EVENTS, mFrameCallback); mFrameCallbackPosted = false;