Skip to content

Instantly share code, notes, and snippets.

@steipete
Last active May 27, 2024 12:11
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);
});
}
}
}
}
@dlo
Copy link

dlo commented Sep 21, 2014

Having the same issue(s) as @jomnius.

@danydev
Copy link

danydev commented Oct 11, 2014

@dlo you just need to use NSLog instead of PSPDFLogError and move PSPDFAssert on line 16. I just created a fork that should compile right away with a few minor changes.
https://gist.github.com/danydev/9cb3539198c6af3446ed

@onmyway133
Copy link

@andrewebling I think we should do compile time check around the constructor
@steipete How to avoid checking certain scenarios ? Like this http://stackoverflow.com/questions/25963367/mpvolumeview-initwithframestyle-not-called-on-main-thread-when-loading-uiweb

@vandadschibsted
Copy link

Thanks for this. Even though I didn't find anything, but it's good to know this exists...

I just did a test with this code:

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        UIButton *btn = [UIButton buttonWithType:UIButtonTypeSystem];
        btn.tag = 1;
        [self.view addSubview:btn];
    });

and this gist could not find this simple UI work on a non UI thread... what could be wrong?

@jerry-sl
Copy link

Anyone use this gist in Swift project?

@jeremangnr
Copy link

@vandadschibsted I was having the same issue and came up with a simple fix. I removed the if (_self.window) checks from the swizzled methods, that did the trick for me... thanks to this I found the cause of an issue that was disabling all animations across the app!

@RameshAran
Copy link

Thank you @jeremangnr.

After commenting the following condition check "_self.window", its working. Its capturing the inconsistencies like the following

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//UI updation
});

NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue addOperationWithBlock:^{
//UI updation
})];

I am getting the following Assertion failure.
Assertion failure: NSThread.isMainThread in void PSPDFAssertIfNotMainThread() on line /Users/<account_name>/Documents/Tutorials/TestSample/PSPDFUIKitMainThreadGuard.m:146.

Thanks you @steipete for sharing this. Its really useful.

@Yaro812
Copy link

Yaro812 commented Sep 25, 2015

How to properly add this code to the project?

Is adding PSPDFUIKitMainThreadGuard();
to + (void)initialize of my AppDelegate is a proper way to use this code?

@kconnor
Copy link

kconnor commented Sep 26, 2015

@Yaro812 Just add the file to your project (and Compile Build Phase). It runs on load:
attribute((constructor)) static void PSPDFUIKitMainThreadGuard(void)

@kconnor
Copy link

kconnor commented Sep 26, 2015

@steipete My osx version of this gist is unhappy on El Capitan. Looks like it does more UI work off the main thread. It's asserting where it didn't before. Thanks very much for making it public.

@nrbrook
Copy link

nrbrook commented Oct 11, 2015

I made a swift version

@ponchorage
Copy link

Still not clear to me how to actually use this. The file is in my project and it appears in the Compile Build Phase. Do I need to import the file in the AppDelegate or somewhere and is there a call I need to make? I ask because it's not catching any problems but the new version of Xcode 7.1 seems to think there are issues. It's giving me a stack trace saying that I'm modifying the autolayout engine from a background thread.

This application is modifying the autolayout engine from a background thread, which can lead to engine corruption and weird crashes. This will cause an exception in a future release.

@jdarowski
Copy link

How much can one love a single gist? <3

@drct
Copy link

drct commented Nov 12, 2015

Minor improvements: Moved "#define PSPDFAssert(expression, ...) ...", commented "PSPDFLogError ..." out and added "#import <UIKit/UIKit.h>". If interested, you can pull from here: https://gist.github.com/drct/bb4b1f93f8790cca9c9f (since no pull requests are possible with gists.

@fbartho
Copy link

fbartho commented Nov 14, 2015

Is there an OSX version of this?

@umut-genlik
Copy link

How to use it:
@ponchorage just add the .m file (the one @drct enhanced) to your project, then after it is added to your project go to Build Phases -> Compile Sources and find the PSPDFUIKitMainThreadGuard.m double click it add the non-arc compiling flag. -fno-objc-arc
That is it.
screen shot 2015-11-20 at 2 06 20 pm

screen shot 2015-11-20 at 2 05 35 pm

@alexbartisro
Copy link

Doesn't work for me. No exception is caught.

@onmyway133
Copy link

@jerry-sl You can use swizzle on UIView in Swift, here is an example https://github.com/onmyway133/MainThreadGuard
You can use DEBUG custom flag http://stackoverflow.com/questions/24003291/ifdef-replacement-in-swift-language or use assert

@sahabe1
Copy link

sahabe1 commented May 3, 2016

How to use PSPDFUIKitMainThreadGuard.m . I am getting error

Undefined symbols for architecture armv7:
"_PSPDFAssert", referenced from:
_PSPDFReplaceMethodWithBlock in PSPDFUIKitMainThreadGuard.o
(maybe you meant: _PSPDFAssertIfNotMainThread)
ld: symbol(s) not found for architecture armv7
clang: error: linker command failed with exit code 1 (use -v to see invocation)

@panliming-tuya
Copy link

@sahabe1 same as me, but look this

drct commented on 12 Nov 2015
Minor improvements: Moved "#define PSPDFAssert(expression, ...) ...", commented "PSPDFLogError ..." out and added "#import ". If interested, you can pull from here: https://gist.github.com/drct/bb4b1f93f8790cca9c9f (since no pull requests are possible with gists.

@steipete
Copy link
Author

steipete commented Jul 6, 2016

Please dupe https://openradar.appspot.com/27192338 if you want to see this in UIKit.

@vedon
Copy link

vedon commented Jul 17, 2016

I set the image of UIImageView in another thread ,it can't detect the illegal operation.
Is there another way to detect it ?

@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