-
Notifications
You must be signed in to change notification settings - Fork 24.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix: iOS app crash caused by the request operation canceling #48350
base: main
Are you sure you want to change the base?
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -1189,86 +1189,88 @@ - (void)dispatchBlock:(dispatch_block_t)block queue:(dispatch_queue_t)queue | |
|
||
- (void)invalidate | ||
{ | ||
if (_didInvalidate) { | ||
return; | ||
} | ||
RCTUnsafeExecuteOnMainQueueSync(^{ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This could potentially deadlock. We should not run the unsafe variant of this method. Can you change it with There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Changed. |
||
if (_didInvalidate) { | ||
return; | ||
} | ||
|
||
RCTAssertMainQueue(); | ||
RCTLogInfo(@"Invalidating %@ (parent: %@, executor: %@)", self, _parentBridge, [self executorClass]); | ||
RCTAssertMainQueue(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. One thing that confuses me is that, in your stacktrace, the crash is happening in Thread 26... but this assert should force the app to be on the main thread, which is not the Thread 26... how's this possible? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @cipolleschi Good question, that's because of actually the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's why I added the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's a good explanation, but then we should see crashes in development happening because of the assertion. And IIUC, the app does not crash in development, right? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. By looking at the crash log, the JS thread is triggering the invalidation. I think that this is the root of the problem: after the JS thread detect the invalidation, we should jump on the UI thread to invalidate everything... There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I didn't encounter this crash in development, but I am not sure if it happens in it, you know, it's a occasional issue on itself. |
||
RCTLogInfo(@"Invalidating %@ (parent: %@, executor: %@)", self, _parentBridge, [self executorClass]); | ||
|
||
if (self->_reactInstance) { | ||
// Do this synchronously on the main thread to fulfil unregisterFromInspector's | ||
// requirements. | ||
self->_reactInstance->unregisterFromInspector(); | ||
} | ||
if (self->_reactInstance) { | ||
// Do this synchronously on the main thread to fulfil unregisterFromInspector's | ||
// requirements. | ||
self->_reactInstance->unregisterFromInspector(); | ||
} | ||
|
||
_loading = NO; | ||
_valid = NO; | ||
_didInvalidate = YES; | ||
_moduleRegistryCreated = NO; | ||
_loading = NO; | ||
_valid = NO; | ||
_didInvalidate = YES; | ||
_moduleRegistryCreated = NO; | ||
|
||
if ([RCTBridge currentBridge] == self) { | ||
[RCTBridge setCurrentBridge:nil]; | ||
} | ||
if ([RCTBridge currentBridge] == self) { | ||
[RCTBridge setCurrentBridge:nil]; | ||
} | ||
|
||
// Stop JS instance and message thread | ||
[self ensureOnJavaScriptThread:^{ | ||
[self->_displayLink invalidate]; | ||
self->_displayLink = nil; | ||
// Stop JS instance and message thread | ||
[self ensureOnJavaScriptThread:^{ | ||
[self->_displayLink invalidate]; | ||
self->_displayLink = nil; | ||
|
||
if (RCTProfileIsProfiling()) { | ||
RCTProfileUnhookModules(self); | ||
} | ||
if (RCTProfileIsProfiling()) { | ||
RCTProfileUnhookModules(self); | ||
} | ||
|
||
// Invalidate modules | ||
// Invalidate modules | ||
|
||
[[NSNotificationCenter defaultCenter] postNotificationName:RCTBridgeWillInvalidateModulesNotification | ||
object:self->_parentBridge | ||
userInfo:@{@"bridge" : self}]; | ||
[[NSNotificationCenter defaultCenter] postNotificationName:RCTBridgeWillInvalidateModulesNotification | ||
object:self->_parentBridge | ||
userInfo:@{@"bridge" : self}]; | ||
|
||
// We're on the JS thread (which we'll be suspending soon), so no new calls will be made to native modules after | ||
// this completes. We must ensure all previous calls were dispatched before deallocating the instance (and module | ||
// wrappers) or we may have invalid pointers still in flight. | ||
dispatch_group_t moduleInvalidation = dispatch_group_create(); | ||
for (RCTModuleData *moduleData in self->_moduleDataByID) { | ||
// Be careful when grabbing an instance here, we don't want to instantiate | ||
// any modules just to invalidate them. | ||
if (![moduleData hasInstance]) { | ||
continue; | ||
} | ||
// We're on the JS thread (which we'll be suspending soon), so no new calls will be made to native modules after | ||
// this completes. We must ensure all previous calls were dispatched before deallocating the instance (and module | ||
// wrappers) or we may have invalid pointers still in flight. | ||
dispatch_group_t moduleInvalidation = dispatch_group_create(); | ||
for (RCTModuleData *moduleData in self->_moduleDataByID) { | ||
// Be careful when grabbing an instance here, we don't want to instantiate | ||
// any modules just to invalidate them. | ||
if (![moduleData hasInstance]) { | ||
continue; | ||
} | ||
|
||
if ([moduleData.instance respondsToSelector:@selector(invalidate)]) { | ||
dispatch_group_enter(moduleInvalidation); | ||
[self | ||
dispatchBlock:^{ | ||
[(id<RCTInvalidating>)moduleData.instance invalidate]; | ||
dispatch_group_leave(moduleInvalidation); | ||
} | ||
queue:moduleData.methodQueue]; | ||
if ([moduleData.instance respondsToSelector:@selector(invalidate)]) { | ||
dispatch_group_enter(moduleInvalidation); | ||
[self | ||
dispatchBlock:^{ | ||
[(id<RCTInvalidating>)moduleData.instance invalidate]; | ||
dispatch_group_leave(moduleInvalidation); | ||
} | ||
queue:moduleData.methodQueue]; | ||
} | ||
[moduleData invalidate]; | ||
} | ||
[moduleData invalidate]; | ||
} | ||
|
||
if (dispatch_group_wait(moduleInvalidation, dispatch_time(DISPATCH_TIME_NOW, 10 * NSEC_PER_SEC))) { | ||
RCTLogError(@"Timed out waiting for modules to be invalidated"); | ||
} | ||
if (dispatch_group_wait(moduleInvalidation, dispatch_time(DISPATCH_TIME_NOW, 10 * NSEC_PER_SEC))) { | ||
RCTLogError(@"Timed out waiting for modules to be invalidated"); | ||
} | ||
|
||
[[NSNotificationCenter defaultCenter] postNotificationName:RCTBridgeDidInvalidateModulesNotification | ||
object:self->_parentBridge | ||
userInfo:@{@"bridge" : self}]; | ||
[[NSNotificationCenter defaultCenter] postNotificationName:RCTBridgeDidInvalidateModulesNotification | ||
object:self->_parentBridge | ||
userInfo:@{@"bridge" : self}]; | ||
|
||
self->_reactInstance.reset(); | ||
self->_jsMessageThread.reset(); | ||
self->_reactInstance.reset(); | ||
self->_jsMessageThread.reset(); | ||
|
||
self->_moduleDataByName = nil; | ||
self->_moduleDataByID = nil; | ||
self->_moduleClassesByID = nil; | ||
self->_pendingCalls = nil; | ||
self->_moduleDataByName = nil; | ||
self->_moduleDataByID = nil; | ||
self->_moduleClassesByID = nil; | ||
self->_pendingCalls = nil; | ||
|
||
[self->_jsThread cancel]; | ||
self->_jsThread = nil; | ||
CFRunLoopStop(CFRunLoopGetCurrent()); | ||
}]; | ||
[self->_jsThread cancel]; | ||
self->_jsThread = nil; | ||
CFRunLoopStop(CFRunLoopGetCurrent()); | ||
}]; | ||
}); | ||
} | ||
|
||
- (void)logMessage:(NSString *)message level:(NSString *)level | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is not necessary. You can only add
NSOperation
to anNSOperationQueue
andcancellAllOperations
runs the same code you are manually writing.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@cipolleschi Seems that the
cancellAllOperations
won't do the status checks for the operation, and in the crash report of my iOS app, the stack trace exactly tells me the crash point is just in thecancellAllOperations
internal.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the official Apple docs for
cancelAllOperations
.And this is the docs of
NSOperation cancel
.In any case, calling
cancel
on an already cancelled or finished operation does not crash the app.I believe that the crash is happening inside one of the operations that are being cancelled and that's why the crash reporter reports the crash there.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it possible that even though we already put assurance for the corresponding code to make it only executed on the main thread or another sole thread, e.g. the JS thread, but because of the nature of object pointer reference, the operation object could still be shared among multiple thread contexts, then the situation you said above happens.