Skip to content

Commit dcae4ba

Browse files
olinotteghemfacebook-github-bot-2
authored andcommitted
Sync diff : Enable initializing react context off UI thread
Reviewed By: @astreet Differential Revision: D2480130
1 parent 8f13560 commit dcae4ba

File tree

3 files changed

+145
-57
lines changed

3 files changed

+145
-57
lines changed

ReactAndroid/src/main/java/com/facebook/react/ReactInstanceManager.java

Lines changed: 134 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
import android.app.Application;
1818
import android.content.Context;
19+
import android.os.AsyncTask;
1920
import android.os.Bundle;
2021
import android.view.View;
2122

@@ -72,6 +73,8 @@ public class ReactInstanceManager {
7273
/* should only be accessed from main thread (UI thread) */
7374
private final List<ReactRootView> mAttachedRootViews = new ArrayList<>();
7475
private LifecycleState mLifecycleState;
76+
private boolean mIsContextInitAsyncTaskRunning;
77+
private @Nullable ReactContextInitParams mPendingReactContextInitParams;
7578

7679
/* accessed from any thread */
7780
private final @Nullable String mBundleAssetName; /* name of JS bundle file in assets folder */
@@ -105,11 +108,70 @@ public void toggleElementInspector() {
105108

106109
private final DefaultHardwareBackBtnHandler mBackBtnHandler =
107110
new DefaultHardwareBackBtnHandler() {
111+
@Override
112+
public void invokeDefaultOnBackPressed() {
113+
ReactInstanceManager.this.invokeDefaultOnBackPressed();
114+
}
115+
};
116+
117+
private class ReactContextInitParams {
118+
private final JavaScriptExecutor mJsExecutor;
119+
private final JSBundleLoader mJsBundleLoader;
120+
121+
public ReactContextInitParams(
122+
JavaScriptExecutor jsExecutor,
123+
JSBundleLoader jsBundleLoader) {
124+
mJsExecutor = Assertions.assertNotNull(jsExecutor);
125+
mJsBundleLoader = Assertions.assertNotNull(jsBundleLoader);
126+
}
127+
128+
public JavaScriptExecutor getJsExecutor() {
129+
return mJsExecutor;
130+
}
131+
132+
public JSBundleLoader getJsBundleLoader() {
133+
return mJsBundleLoader;
134+
}
135+
}
136+
137+
/*
138+
* Task class responsible for (re)creating react context in the background. These tasks can only
139+
* be executing one at time, see {@link #recreateReactContextInBackground()}.
140+
*/
141+
private final class ReactContextInitAsyncTask extends
142+
AsyncTask<ReactContextInitParams, Void, ReactApplicationContext> {
143+
144+
@Override
145+
protected void onPreExecute() {
146+
if (mCurrentReactContext != null) {
147+
tearDownReactContext(mCurrentReactContext);
148+
mCurrentReactContext = null;
149+
}
150+
}
151+
152+
@Override
153+
protected ReactApplicationContext doInBackground(ReactContextInitParams... params) {
154+
Assertions.assertCondition(params != null && params.length > 0 && params[0] != null);
155+
return createReactContext(params[0].getJsExecutor(), params[0].getJsBundleLoader());
156+
}
157+
108158
@Override
109-
public void invokeDefaultOnBackPressed() {
110-
ReactInstanceManager.this.invokeDefaultOnBackPressed();
159+
protected void onPostExecute(ReactApplicationContext reactContext) {
160+
try {
161+
setupReactContext(reactContext);
162+
} finally {
163+
mIsContextInitAsyncTaskRunning = false;
164+
}
165+
166+
// Handle enqueued request to re-initialize react context.
167+
if (mPendingReactContextInitParams != null) {
168+
recreateReactContextInBackground(
169+
mPendingReactContextInitParams.getJsExecutor(),
170+
mPendingReactContextInitParams.getJsBundleLoader());
171+
mPendingReactContextInitParams = null;
172+
}
111173
}
112-
};
174+
}
113175

114176
private ReactInstanceManager(
115177
Context applicationContext,
@@ -161,10 +223,35 @@ private static void initializeSoLoaderIfNecessary(Context applicationContext) {
161223
SoLoader.init(applicationContext, /* native exopackage */ false);
162224
}
163225

226+
/**
227+
* Trigger react context initialization asynchronously in a background async task. This enables
228+
* applications to pre-load the application JS, and execute global code before
229+
* {@link ReactRootView} is available and measured.
230+
*/
231+
public void createReactContextInBackground() {
232+
if (mUseDeveloperSupport) {
233+
if (mDevSupportManager.hasUpToDateJSBundleInCache()) {
234+
// If there is a up-to-date bundle downloaded from server, always use that
235+
onJSBundleLoadedFromServer();
236+
return;
237+
} else if (mBundleAssetName == null ||
238+
!mDevSupportManager.hasBundleInAssets(mBundleAssetName)) {
239+
// Bundle not available in assets, fetch from the server
240+
mDevSupportManager.handleReloadJS();
241+
return;
242+
}
243+
}
244+
// Use JS file from assets
245+
recreateReactContextInBackground(
246+
new JSCJavaScriptExecutor(),
247+
JSBundleLoader.createAssetLoader(
248+
mApplicationContext.getAssets(),
249+
mBundleAssetName));
250+
}
251+
164252
/**
165253
* This method will give JS the opportunity to consume the back button event. If JS does not
166-
* consume the event, mDefaultBackButtonImpl will be invoked at the end of the round trip
167-
* to JS.
254+
* consume the event, mDefaultBackButtonImpl will be invoked at the end of the round trip to JS.
168255
*/
169256
public void onBackPressed() {
170257
UiThreadUtil.assertOnUiThread();
@@ -255,16 +342,24 @@ public void showDevOptionsDialog() {
255342

256343
/**
257344
* Attach given {@param rootView} to a catalyst instance manager and start JS application using
258-
* JS module provided by {@link ReactRootView#getJSModuleName}. This view will then be tracked
259-
* by this manager and in case of catalyst instance restart it will be re-attached.
345+
* JS module provided by {@link ReactRootView#getJSModuleName}. If the react context is currently
346+
* being (re)-created, or if react context has not been created yet, the JS application associated
347+
* with the provided root view will be started asynchronously, i.e this method won't block.
348+
* This view will then be tracked by this manager and in case of catalyst instance restart it will
349+
* be re-attached.
260350
*/
261351
/* package */ void attachMeasuredRootView(ReactRootView rootView) {
262352
UiThreadUtil.assertOnUiThread();
263353
mAttachedRootViews.add(rootView);
264-
if (mCurrentReactContext == null) {
265-
initializeReactContext();
266-
} else {
267-
attachMeasuredRootViewToInstance(rootView, mCurrentReactContext.getCatalystInstance());
354+
355+
// If react context is being created in the background, JS application will be started
356+
// automatically when creation completes, as root view is part of the attached root view list.
357+
if (!mIsContextInitAsyncTaskRunning) {
358+
if (mCurrentReactContext == null) {
359+
createReactContextInBackground();
360+
} else {
361+
attachMeasuredRootViewToInstance(rootView, mCurrentReactContext.getCatalystInstance());
362+
}
268363
}
269364
}
270365

@@ -300,53 +395,51 @@ public void showDevOptionsDialog() {
300395
}
301396

302397
private void onReloadWithJSDebugger(ProxyJavaScriptExecutor proxyExecutor) {
303-
recreateReactContext(
398+
recreateReactContextInBackground(
304399
proxyExecutor,
305400
JSBundleLoader.createRemoteDebuggerBundleLoader(
306401
mDevSupportManager.getJSBundleURLForRemoteDebugging()));
307402
}
308403

309404
private void onJSBundleLoadedFromServer() {
310-
recreateReactContext(
405+
recreateReactContextInBackground(
311406
new JSCJavaScriptExecutor(),
312407
JSBundleLoader.createCachedBundleFromNetworkLoader(
313408
mDevSupportManager.getSourceUrl(),
314409
mDevSupportManager.getDownloadedJSBundleFile()));
315410
}
316411

317-
private void initializeReactContext() {
318-
if (mUseDeveloperSupport) {
319-
if (mDevSupportManager.hasUpToDateJSBundleInCache()) {
320-
// If there is a up-to-date bundle downloaded from server, always use that
321-
onJSBundleLoadedFromServer();
322-
return;
323-
} else if (mBundleAssetName == null ||
324-
!mDevSupportManager.hasBundleInAssets(mBundleAssetName)) {
325-
// Bundle not available in assets, fetch from the server
326-
mDevSupportManager.handleReloadJS();
327-
return;
328-
}
329-
}
330-
// Use JS file from assets
331-
recreateReactContext(
332-
new JSCJavaScriptExecutor(),
333-
JSBundleLoader.createAssetLoader(
334-
mApplicationContext.getAssets(),
335-
mBundleAssetName));
336-
}
337-
338-
private void recreateReactContext(
412+
private void recreateReactContextInBackground(
339413
JavaScriptExecutor jsExecutor,
340414
JSBundleLoader jsBundleLoader) {
341415
UiThreadUtil.assertOnUiThread();
342-
if (mCurrentReactContext != null) {
343-
tearDownReactContext(mCurrentReactContext);
416+
417+
ReactContextInitParams initParams = new ReactContextInitParams(jsExecutor, jsBundleLoader);
418+
if (!mIsContextInitAsyncTaskRunning) {
419+
// No background task to create react context is currently running, create and execute one.
420+
ReactContextInitAsyncTask initTask = new ReactContextInitAsyncTask();
421+
initTask.execute(initParams);
422+
mIsContextInitAsyncTaskRunning = true;
423+
} else {
424+
// Background task is currently running, queue up most recent init params to recreate context
425+
// once task completes.
426+
mPendingReactContextInitParams = initParams;
344427
}
345-
mCurrentReactContext = createReactContext(jsExecutor, jsBundleLoader);
428+
}
429+
430+
private void setupReactContext(ReactApplicationContext reactContext) {
431+
UiThreadUtil.assertOnUiThread();
432+
Assertions.assertCondition(mCurrentReactContext == null);
433+
mCurrentReactContext = Assertions.assertNotNull(reactContext);
434+
CatalystInstance catalystInstance =
435+
Assertions.assertNotNull(reactContext.getCatalystInstance());
436+
437+
catalystInstance.initialize();
438+
mDevSupportManager.onNewReactContextCreated(reactContext);
439+
moveReactContextToCurrentLifecycleState(reactContext);
440+
346441
for (ReactRootView rootView : mAttachedRootViews) {
347-
attachMeasuredRootViewToInstance(
348-
rootView,
349-
mCurrentReactContext.getCatalystInstance());
442+
attachMeasuredRootViewToInstance(rootView, catalystInstance);
350443
}
351444
}
352445

@@ -430,10 +523,6 @@ private ReactApplicationContext createReactContext(
430523
}
431524

432525
reactContext.initializeWithInstance(catalystInstance);
433-
catalystInstance.initialize();
434-
mDevSupportManager.onNewReactContextCreated(reactContext);
435-
436-
moveReactContextToCurrentLifecycleState(reactContext);
437526

438527
return reactContext;
439528
}

ReactAndroid/src/main/java/com/facebook/react/devsupport/DevServerHelper.java

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -78,10 +78,10 @@ public interface OnServerContentChangeListener {
7878

7979
private final DevInternalSettings mSettings;
8080
private final OkHttpClient mClient;
81+
private final Handler mRestartOnChangePollingHandler;
8182

8283
private boolean mOnChangePollingEnabled;
8384
private @Nullable OkHttpClient mOnChangePollingClient;
84-
private @Nullable Handler mRestartOnChangePollingHandler;
8585
private @Nullable OnServerContentChangeListener mOnServerContentChangeListener;
8686

8787
public DevServerHelper(DevInternalSettings settings) {
@@ -92,6 +92,7 @@ public DevServerHelper(DevInternalSettings settings) {
9292
// No read or write timeouts by default
9393
mClient.setReadTimeout(0, TimeUnit.MILLISECONDS);
9494
mClient.setWriteTimeout(0, TimeUnit.MILLISECONDS);
95+
mRestartOnChangePollingHandler = new Handler();
9596
}
9697

9798
/** Intent action for reloading the JS */
@@ -193,10 +194,7 @@ public void onResponse(Response response) throws IOException {
193194

194195
public void stopPollingOnChangeEndpoint() {
195196
mOnChangePollingEnabled = false;
196-
if (mRestartOnChangePollingHandler != null) {
197-
mRestartOnChangePollingHandler.removeCallbacksAndMessages(null);
198-
mRestartOnChangePollingHandler = null;
199-
}
197+
mRestartOnChangePollingHandler.removeCallbacksAndMessages(null);
200198
if (mOnChangePollingClient != null) {
201199
mOnChangePollingClient.cancel(this);
202200
mOnChangePollingClient = null;
@@ -216,7 +214,6 @@ public void startPollingOnChangeEndpoint(
216214
mOnChangePollingClient
217215
.setConnectionPool(new ConnectionPool(1, LONG_POLL_KEEP_ALIVE_DURATION_MS))
218216
.setConnectTimeout(HTTP_CONNECT_TIMEOUT_MS, TimeUnit.MILLISECONDS);
219-
mRestartOnChangePollingHandler = new Handler();
220217
enqueueOnChangeEndpointLongPolling();
221218
}
222219

@@ -246,7 +243,7 @@ public void onFailure(Request request, IOException e) {
246243
// of a failure, so that we don't flood network queue with frequent requests in case when
247244
// dev server is down
248245
FLog.d(ReactConstants.TAG, "Error while requesting /onchange endpoint", e);
249-
Assertions.assertNotNull(mRestartOnChangePollingHandler).postDelayed(
246+
mRestartOnChangePollingHandler.postDelayed(
250247
new Runnable() {
251248
@Override
252249
public void run() {

ReactAndroid/src/main/java/com/facebook/react/modules/core/Timing.java

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -82,22 +82,22 @@ public void doFrame(long frameTimeNanos) {
8282
Assertions.assertNotNull(mJSTimersModule).callTimers(timersToCall);
8383
}
8484

85-
mReactChoreographer.postFrameCallback(ReactChoreographer.CallbackType.TIMERS_EVENTS, this);
85+
Assertions.assertNotNull(mReactChoreographer)
86+
.postFrameCallback(ReactChoreographer.CallbackType.TIMERS_EVENTS, this);
8687
}
8788
}
8889

8990
private final Object mTimerGuard = new Object();
9091
private final PriorityQueue<Timer> mTimers;
9192
private final SparseArray<Timer> mTimerIdsToTimers;
9293
private final AtomicBoolean isPaused = new AtomicBoolean(false);
93-
private final ReactChoreographer mReactChoreographer;
9494
private final FrameCallback mFrameCallback = new FrameCallback();
95+
private @Nullable ReactChoreographer mReactChoreographer;
9596
private @Nullable JSTimersExecution mJSTimersModule;
9697
private boolean mFrameCallbackPosted = false;
9798

9899
public Timing(ReactApplicationContext reactContext) {
99100
super(reactContext);
100-
mReactChoreographer = ReactChoreographer.getInstance();
101101
// We store timers sorted by finish time.
102102
mTimers = new PriorityQueue<Timer>(
103103
11, // Default capacity: for some reason they don't expose a (Comparator) constructor
@@ -119,6 +119,8 @@ public int compare(Timer lhs, Timer rhs) {
119119

120120
@Override
121121
public void initialize() {
122+
// Safe to acquire choreographer here, as initialize() is invoked from UI thread.
123+
mReactChoreographer = ReactChoreographer.getInstance();
122124
mJSTimersModule = getReactApplicationContext().getCatalystInstance()
123125
.getJSModule(JSTimersExecution.class);
124126
getReactApplicationContext().addLifecycleEventListener(this);
@@ -151,7 +153,7 @@ public void onCatalystInstanceDestroy() {
151153

152154
private void setChoreographerCallback() {
153155
if (!mFrameCallbackPosted) {
154-
mReactChoreographer.postFrameCallback(
156+
Assertions.assertNotNull(mReactChoreographer).postFrameCallback(
155157
ReactChoreographer.CallbackType.TIMERS_EVENTS,
156158
mFrameCallback);
157159
mFrameCallbackPosted = true;
@@ -160,7 +162,7 @@ private void setChoreographerCallback() {
160162

161163
private void clearChoreographerCallback() {
162164
if (mFrameCallbackPosted) {
163-
mReactChoreographer.removeFrameCallback(
165+
Assertions.assertNotNull(mReactChoreographer).removeFrameCallback(
164166
ReactChoreographer.CallbackType.TIMERS_EVENTS,
165167
mFrameCallback);
166168
mFrameCallbackPosted = false;

0 commit comments

Comments
 (0)