Skip to content

Commit 9c90ba6

Browse files
javacheide
authored andcommitted
Improve RCTCxxBridge invalidation
Reviewed By: fromcelticpark Differential Revision: D5536063 fbshipit-source-id: b0f19bebea839c7da84890c338ad4d38293bc99c
1 parent de9c421 commit 9c90ba6

5 files changed

Lines changed: 71 additions & 62 deletions

File tree

React/Base/RCTBatchedBridge.mm

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -921,6 +921,10 @@ - (void)handleBuffer:(id)buffer batchEnded:(BOOL)batchEnded
921921
{
922922
RCTAssertJSThread();
923923

924+
if (!self.valid) {
925+
return;
926+
}
927+
924928
if (buffer != nil && buffer != (id)kCFNull) {
925929
_wasBatchActive = YES;
926930
[self handleBuffer:buffer];

React/Base/RCTModuleData.mm

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,8 @@ - (NSArray *)config
357357
- (dispatch_queue_t)methodQueue
358358
{
359359
(void)[self instance];
360+
RCTAssert(_methodQueue != nullptr, @"Module %@ has no methodQueue (instance: %@, bridge.valid: %d)",
361+
self, _instance, _bridge.valid);
360362
return _methodQueue;
361363
}
362364

React/CxxBridge/RCTCxxBridge.mm

Lines changed: 62 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -125,13 +125,12 @@ void onBatchComplete() override {
125125
[bridge_ partialBatchDidFlush];
126126
[bridge_ batchDidComplete];
127127
}
128-
void incrementPendingJSCalls() override {}
129-
void decrementPendingJSCalls() override {}
130128
};
131129

132130
@implementation RCTCxxBridge
133131
{
134132
BOOL _wasBatchActive;
133+
BOOL _didInvalidate;
135134

136135
NSMutableArray<dispatch_block_t> *_pendingCalls;
137136
std::atomic<NSInteger> _pendingCount;
@@ -169,7 +168,7 @@ - (JSContext *)jsContext
169168

170169
- (JSGlobalContextRef)jsContextRef
171170
{
172-
return (JSGlobalContextRef)self->_reactInstance->getJavaScriptContext();
171+
return (JSGlobalContextRef)(self->_reactInstance ? self->_reactInstance->getJavaScriptContext() : nullptr);
173172
}
174173

175174
- (instancetype)initWithParentBridge:(RCTBridge *)bridge
@@ -200,7 +199,7 @@ - (instancetype)initWithParentBridge:(RCTBridge *)bridge
200199
return self;
201200
}
202201

203-
- (void)runJSRunLoop
202+
+ (void)runRunLoop
204203
{
205204
@autoreleasepool {
206205
RCT_PROFILE_BEGIN_EVENT(RCTProfileTagAlways, @"-[RCTCxxBridge runJSRunLoop] setup", nil);
@@ -263,8 +262,8 @@ - (void)start
263262
object:_parentBridge userInfo:@{@"bridge": self}];
264263

265264
// Set up the JS thread early
266-
_jsThread = [[NSThread alloc] initWithTarget:self
267-
selector:@selector(runJSRunLoop)
265+
_jsThread = [[NSThread alloc] initWithTarget:[self class]
266+
selector:@selector(runRunLoop)
268267
object:nil];
269268
_jsThread.name = RCTJSThreadName;
270269
_jsThread.qualityOfService = NSOperationQualityOfServiceUserInteractive;
@@ -469,7 +468,7 @@ - (void)_initializeBridge:(std::shared_ptr<JSExecutorFactory>)executorFactory
469468
if (_reactInstance) {
470469
// This is async, but any calls into JS are blocked by the m_syncReady CV in Instance
471470
_reactInstance->initializeBridge(
472-
std::unique_ptr<RCTInstanceCallback>(new RCTInstanceCallback(self)),
471+
std::make_unique<RCTInstanceCallback>(self),
473472
executorFactory,
474473
_jsMessageThread,
475474
[self _buildModuleRegistry]);
@@ -769,6 +768,7 @@ - (void)handleError:(NSError *)error
769768
}
770769

771770
RCTFatal(error);
771+
772772
// RN will stop, but let the rest of the app keep going.
773773
return;
774774
}
@@ -779,27 +779,27 @@ - (void)handleError:(NSError *)error
779779

780780
// Hack: once the bridge is invalidated below, it won't initialize any new native
781781
// modules. Initialize the redbox module now so we can still report this error.
782-
[self redBox];
782+
RCTRedBox *redBox = [self redBox];
783783

784784
_loading = NO;
785785
_valid = NO;
786786

787787
dispatch_async(dispatch_get_main_queue(), ^{
788788
if (self->_jsMessageThread) {
789-
auto thread = self->_jsMessageThread;
790-
self->_jsMessageThread->runOnQueue([thread] {
791-
thread->quitSynchronous();
792-
});
793-
self->_jsMessageThread.reset();
789+
// Make sure initializeBridge completed
790+
self->_jsMessageThread->runOnQueueSync([] {});
794791
}
795792

793+
self->_reactInstance.reset();
794+
self->_jsMessageThread.reset();
795+
796796
[[NSNotificationCenter defaultCenter]
797797
postNotificationName:RCTJavaScriptDidFailToLoadNotification
798798
object:self->_parentBridge userInfo:@{@"bridge": self, @"error": error}];
799799

800800
if ([error userInfo][RCTJSRawStackTraceKey]) {
801-
[self.redBox showErrorMessage:[error localizedDescription]
802-
withRawStack:[error userInfo][RCTJSRawStackTraceKey]];
801+
[redBox showErrorMessage:[error localizedDescription]
802+
withRawStack:[error userInfo][RCTJSRawStackTraceKey]];
803803
}
804804

805805
RCTFatal(error);
@@ -866,63 +866,68 @@ - (void)dispatchBlock:(dispatch_block_t)block
866866

867867
- (void)invalidate
868868
{
869-
if (!_valid) {
869+
if (_didInvalidate) {
870870
return;
871871
}
872872

873873
RCTAssertMainQueue();
874-
RCTAssert(_reactInstance != nil, @"Can't complete invalidation without a react instance");
874+
RCTLogInfo(@"Invalidating %@ (parent: %@, executor: %@)", self, _parentBridge, [self executorClass]);
875875

876876
_loading = NO;
877877
_valid = NO;
878+
_didInvalidate = YES;
879+
878880
if ([RCTBridge currentBridge] == self) {
879881
[RCTBridge setCurrentBridge:nil];
880882
}
881883

882-
// Invalidate modules
883-
dispatch_group_t group = dispatch_group_create();
884-
for (RCTModuleData *moduleData in _moduleDataByID) {
885-
// Be careful when grabbing an instance here, we don't want to instantiate
886-
// any modules just to invalidate them.
887-
if (![moduleData hasInstance]) {
888-
continue;
889-
}
884+
// Stop JS instance and message thread
885+
[self ensureOnJavaScriptThread:^{
886+
[self->_displayLink invalidate];
887+
self->_displayLink = nil;
890888

891-
if ([moduleData.instance respondsToSelector:@selector(invalidate)]) {
892-
dispatch_group_enter(group);
893-
[self dispatchBlock:^{
894-
[(id<RCTInvalidating>)moduleData.instance invalidate];
895-
dispatch_group_leave(group);
896-
} queue:moduleData.methodQueue];
889+
if (RCTProfileIsProfiling()) {
890+
RCTProfileUnhookModules(self);
897891
}
898-
[moduleData invalidate];
899-
}
900-
901-
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
902-
[self ensureOnJavaScriptThread:^{
903-
[self->_displayLink invalidate];
904-
self->_displayLink = nil;
905892

906-
self->_reactInstance.reset();
907-
if (self->_jsMessageThread) {
908-
self->_jsMessageThread->quitSynchronous();
909-
self->_jsMessageThread.reset();
893+
// Invalidate modules
894+
// We're on the JS thread (which we'll be suspending soon), so no new calls will be made to native modules after
895+
// this completes. We must ensure all previous calls were dispatched before deallocating the instance (and module
896+
// wrappers) or we may have invalid pointers still in flight.
897+
dispatch_group_t moduleInvalidation = dispatch_group_create();
898+
for (RCTModuleData *moduleData in self->_moduleDataByID) {
899+
// Be careful when grabbing an instance here, we don't want to instantiate
900+
// any modules just to invalidate them.
901+
if (![moduleData hasInstance]) {
902+
continue;
910903
}
911904

912-
if (RCTProfileIsProfiling()) {
913-
RCTProfileUnhookModules(self);
905+
if ([moduleData.instance respondsToSelector:@selector(invalidate)]) {
906+
dispatch_group_enter(moduleInvalidation);
907+
[self dispatchBlock:^{
908+
[(id<RCTInvalidating>)moduleData.instance invalidate];
909+
dispatch_group_leave(moduleInvalidation);
910+
} queue:moduleData.methodQueue];
914911
}
912+
[moduleData invalidate];
913+
}
915914

916-
self->_moduleDataByName = nil;
917-
self->_moduleDataByID = nil;
918-
self->_moduleClassesByID = nil;
919-
self->_pendingCalls = nil;
915+
if (dispatch_group_wait(moduleInvalidation, dispatch_time(DISPATCH_TIME_NOW, 10 * NSEC_PER_SEC))) {
916+
RCTLogError(@"Timed out waiting for modules to be invalidated");
917+
}
920918

921-
[self->_jsThread cancel];
922-
self->_jsThread = nil;
923-
CFRunLoopStop(CFRunLoopGetCurrent());
924-
}];
925-
});
919+
self->_reactInstance.reset();
920+
self->_jsMessageThread.reset();
921+
922+
self->_moduleDataByName = nil;
923+
self->_moduleDataByID = nil;
924+
self->_moduleClassesByID = nil;
925+
self->_pendingCalls = nil;
926+
927+
[self->_jsThread cancel];
928+
self->_jsThread = nil;
929+
CFRunLoopStop(CFRunLoopGetCurrent());
930+
}];
926931
}
927932

928933
- (void)logMessage:(NSString *)message level:(NSString *)level
@@ -1051,7 +1056,6 @@ - (void)enqueueCallback:(NSNumber *)cbID args:(NSArray *)args
10511056
*/
10521057

10531058
RCTProfileBeginFlowEvent();
1054-
10551059
[self _runAfterLoad:^{
10561060
RCTProfileEndFlowEvent();
10571061

@@ -1142,25 +1146,25 @@ - (JSValue *)callFunctionOnModule:(NSString *)module
11421146
if (!_reactInstance) {
11431147
if (error) {
11441148
*error = RCTErrorWithMessage(
1145-
@"Attempt to call sync callFunctionOnModule: on uninitialized bridge");
1149+
@"callFunctionOnModule was called on uninitialized bridge");
11461150
}
11471151
return nil;
11481152
} else if (self.executorClass) {
11491153
if (error) {
11501154
*error = RCTErrorWithMessage(
1151-
@"sync callFunctionOnModule: can only be used with JSC executor");
1155+
@"callFunctionOnModule can only be used with JSC executor");
11521156
}
11531157
return nil;
11541158
} else if (!self.valid) {
11551159
if (error) {
11561160
*error = RCTErrorWithMessage(
1157-
@"sync callFunctionOnModule: bridge is no longer valid");
1161+
@"Bridge is no longer valid");
11581162
}
11591163
return nil;
11601164
} else if (self.loading) {
11611165
if (error) {
11621166
*error = RCTErrorWithMessage(
1163-
@"sync callFunctionOnModule: bridge is still loading");
1167+
@"Bridge is still loading");
11641168
}
11651169
return nil;
11661170
}

React/CxxModule/DispatchMessageQueueThread.h

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ class DispatchMessageQueueThread : public MessageQueueThread {
2424

2525
void runOnQueue(std::function<void()>&& func) override {
2626
dispatch_queue_t queue = moduleData_.methodQueue;
27-
RCTAssert(queue != nullptr, @"Module %@ provided invalid queue", moduleData_);
2827
dispatch_block_t block = [func=std::move(func)] { func(); };
2928
RCTAssert(block != nullptr, @"Invalid block generated in call to %@", moduleData_);
3029
if (queue && block) {

ReactCommon/cxxreact/Instance.h

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,9 @@ class ModuleRegistry;
2727

2828
struct InstanceCallback {
2929
virtual ~InstanceCallback() {}
30-
virtual void onBatchComplete() = 0;
31-
virtual void incrementPendingJSCalls() = 0;
32-
virtual void decrementPendingJSCalls() = 0;
30+
virtual void onBatchComplete() {}
31+
virtual void incrementPendingJSCalls() {}
32+
virtual void decrementPendingJSCalls() {}
3333
};
3434

3535
class RN_EXPORT Instance {

0 commit comments

Comments
 (0)