Skip to content

Instantly share code, notes, and snippets.

@steipete
Last active March 10, 2024 19:23
Show Gist options
  • Save steipete/5664345 to your computer and use it in GitHub Desktop.
Save steipete/5664345 to your computer and use it in GitHub Desktop.
This is a guard that tracks down UIKit access on threads other than main. This snippet is taken from the commercial iOS PDF framework http://pspdfkit.com, but relicensed under MIT. Works because a lot of calls internally call setNeedsDisplay or setNeedsLayout. Won't catch everything, but it's very lightweight and usually does the job.You might n…
// Taken from the commercial iOS PDF framework http://pspdfkit.com.
// Copyright (c) 2014 Peter Steinberger, PSPDFKit GmbH. All rights reserved.
// Licensed under MIT (http://opensource.org/licenses/MIT)
//
// You should only use this in debug builds. It doesn't use private API, but I wouldn't ship it.
// PLEASE DUPE rdar://27192338 (https://openradar.appspot.com/27192338) if you would like to see this in UIKit.
#import <objc/runtime.h>
#import <objc/message.h>
// Compile-time selector checks.
#if DEBUG
#define PROPERTY(propName) NSStringFromSelector(@selector(propName))
#else
#define PROPERTY(propName) @#propName
#endif
// http://www.mikeash.com/pyblog/friday-qa-2010-01-29-method-replacement-for-fun-and-profit.html
BOOL PSPDFReplaceMethodWithBlock(Class c, SEL origSEL, SEL newSEL, id block) {
NSCParameterAssert(c);
NSCParameterAssert(origSEL);
NSCParameterAssert(newSEL);
NSCParameterAssert(block);
if ([c instancesRespondToSelector:newSEL]) return YES; // Selector already implemented, skip silently.
Method origMethod = class_getInstanceMethod(c, origSEL);
// Add the new method.
IMP impl = imp_implementationWithBlock(block);
if (!class_addMethod(c, newSEL, impl, method_getTypeEncoding(origMethod))) {
PSPDFLogError(@"Failed to add method: %@ on %@", NSStringFromSelector(newSEL), c);
return NO;
}else {
Method newMethod = class_getInstanceMethod(c, newSEL);
// If original doesn't implement the method we want to swizzle, create it.
if (class_addMethod(c, origSEL, method_getImplementation(newMethod), method_getTypeEncoding(origMethod))) {
class_replaceMethod(c, newSEL, method_getImplementation(origMethod), method_getTypeEncoding(newMethod));
}else {
method_exchangeImplementations(origMethod, newMethod);
}
}
return YES;
}
SEL _PSPDFPrefixedSelector(SEL selector) {
return NSSelectorFromString([NSString stringWithFormat:@"pspdf_%@", NSStringFromSelector(selector)]);
}
#define PSPDFAssert(expression, ...) \
do { if(!(expression)) { \
NSLog(@"%@", [NSString stringWithFormat: @"Assertion failure: %s in %s on line %s:%d. %@", #expression, __PRETTY_FUNCTION__, __FILE__, __LINE__, [NSString stringWithFormat:@"" __VA_ARGS__]]); \
abort(); }} while(0)
void PSPDFAssertIfNotMainThread(void) {
PSPDFAssert(NSThread.isMainThread, @"\nERROR: All calls to UIKit need to happen on the main thread. You have a bug in your code. Use dispatch_async(dispatch_get_main_queue(), ^{ ... }); if you're unsure what thread you're in.\n\nBreak on PSPDFAssertIfNotMainThread to find out where.\n\nStacktrace: %@", NSThread.callStackSymbols);
}
__attribute__((constructor)) static void PSPDFUIKitMainThreadGuard(void) {
@autoreleasepool {
for (NSString *selStr in @[PROPERTY(setNeedsLayout), PROPERTY(setNeedsDisplay), PROPERTY(setNeedsDisplayInRect:)]) {
SEL selector = NSSelectorFromString(selStr);
SEL newSelector = NSSelectorFromString([NSString stringWithFormat:@"pspdf_%@", selStr]);
if ([selStr hasSuffix:@":"]) {
PSPDFReplaceMethodWithBlock(UIView.class, selector, newSelector, ^(__unsafe_unretained UIView *_self, CGRect r) {
// Check for window, since *some* UIKit methods are indeed thread safe.
// https://developer.apple.com/library/ios/#releasenotes/General/WhatsNewIniPhoneOS/Articles/iPhoneOS4.html
/*
Drawing to a graphics context in UIKit is now thread-safe. Specifically:
The routines used to access and manipulate the graphics context can now correctly handle contexts residing on different threads.
String and image drawing is now thread-safe.
Using color and font objects in multiple threads is now safe to do.
*/
if (_self.window) PSPDFAssertIfNotMainThread();
((void ( *)(id, SEL, CGRect))objc_msgSend)(_self, newSelector, r);
});
}else {
PSPDFReplaceMethodWithBlock(UIView.class, selector, newSelector, ^(__unsafe_unretained UIView *_self) {
if (_self.window) {
if (!NSThread.isMainThread) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
dispatch_queue_t queue = dispatch_get_current_queue();
#pragma clang diagnostic pop
// iOS 8 layouts the MFMailComposeController in a background thread on an UIKit queue.
// https://github.com/PSPDFKit/PSPDFKit/issues/1423
if (!queue || !strstr(dispatch_queue_get_label(queue), "UIKit")) {
PSPDFAssertIfNotMainThread();
}
}
}
((void ( *)(id, SEL))objc_msgSend)(_self, newSelector);
});
}
}
}
}
@messihv5
Copy link

it just worked for me, thank you!

@Schemetrical
Copy link

@nrbrook you are literally god

@freak4pc
Copy link

FYI the Swift port by @onmyway133 will probably be broken soon. Any other thoughts on how to get this done? This is a great saver when sometimes forgetting to go back to the UI Thread.
image

@HobiSpace
Copy link

Stacktrace: (
0 MYXJ 0x000000010238faec PSPDFAssertIfNotMainThread + 84
1 MYXJ 0x000000010238fc10 __PSPDFUIKitMainThreadGuard_block_invoke_2 + 36
2 UIKitCore 0x00000001e470ccc4 + 336
3 UIKitCore 0x00000001e4738548 + 1644
4 libobjc.A.dylib 0x00000001b5f1b604 + 68
5 QuartzCore 0x00000001bb3b1f94 + 188
6 QuartzCore 0x00000001bb3b2274 + 328
7 QuartzCore 0x00000001bb316000 + 332
8 QuartzCore 0x00000001bb345518 + 624
9 QuartzCore 0x00000001bb346358 + 96
10 CoreFoundation 0x00000001b6cb3fe0 + 36
11 CoreFoundation 0x00000001b6caeab8 + 408
12 CoreFoundation 0x00000001b6caf03c + 1248
13 CoreFoundation 0x00000001b6cae844 CFRunLoopRunSpecific + 452
14 WebCore 0x00000001bfcc70e0 + 572
15 libsystem_pthread.dylib 0x00000001b6929a04 + 132
16 libsystem_pthread.dylib 0x00000001b6929960 _pthread_start + 52
17 libsystem_pthread.dylib 0x00000001b6931df4 thread_start + 4
)

Triggers on WebThread

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment