Create a gist now

Instantly share code, notes, and snippets.

What would you like to do?
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);
});
}
}
}
}

PSPDFAssert doesn't seem to be defined.

Owner

@NorrinRadd: Thanks! I knew something was missing. Added it in the latest version of my gist.

Triggers on WebThread

* thread #5: tid = 0x2a03, 0x3bd97350 libsystem_kernel.dylib`__pthread_kill + 8, stop reason = signal SIGABRT
    frame #0: 0x3bd97350 libsystem_kernel.dylib`__pthread_kill + 8
    frame #1: 0x3bd0dfb6 libsystem_c.dylib`pthread_kill + 58
    frame #2: 0x3bd4a36a libsystem_c.dylib`abort + 94
    frame #3: 0x002b5236 XXX`PSPDFAssertIfNotMainThread + 358 at PSPDFUIKitMainThreadGuard.m:41
    frame #4: 0x002b5254 XXX`__PSPDFUIKitMainThreadGuard_block_invoke_2(.block_descriptor=0x1e0202d0, _self=0x1e19cdb0) + 24 at PSPDFUIKitMainThreadGuard.m:59
    frame #5: 0x3576a608 QuartzCore`CA::Layer::property_did_change(CA::Transaction*, unsigned int) + 1308
    frame #6: 0x3576a0c4 QuartzCore`CA::Layer::end_change(CA::Transaction*, unsigned int, objc_object*) + 64
    frame #7: 0x357695a0 QuartzCore`CA::Layer::remove_sublayer(CA::Transaction*, CALayer*) + 200
    frame #8: 0x3576946e QuartzCore`CA::Layer::remove_from_superlayer() + 38
    frame #9: 0x39c862a8 WebCore`WebCore::TileGridTile::~TileGridTile() + 132
    frame #10: 0x39c8621e WebCore`WebCore::TileGridTile::~TileGridTile() + 10
    frame #11: 0x39b80fa8 WebCore`WTF::HashTable<WebCore::IntPoint, std::__1::pair<WebCore::IntPoint, WTF::RefPtr<WebCore::TileGridTile> >, WTF::PairFirstExtractor<std::__1::pair<WebCore::IntPoint, WTF::RefPtr<WebCore::TileGridTile> > >, WTF::IntPointHash, WTF::HashMapValueTraits<WTF::HashTraits<WebCore::IntPoint>, WTF::HashTraits<WTF::RefPtr<WebCore::TileGridTile> > >, WTF::HashTraits<WebCore::IntPoint> >::deallocateTable(std::__1::pair<WebCore::IntPoint, WTF::RefPtr<WebCore::TileGridTile> >*, int) + 152
    frame #12: 0x39d746f4 WebCore`WebCore::TileGrid::~TileGrid() + 92
    frame #13: 0x39d74528 WebCore`WebCore::TileCache::~TileCache() + 228
    frame #14: 0x39d7443e WebCore`WebCore::TileCache::~TileCache() + 10
    frame #15: 0x39d74398 WebCore`-[WAKWindow dealloc] + 132
    frame #16: 0x39b8b840 WebCore`HandleWebThreadReleaseSource(void*) + 116
    frame #17: 0x33b7d8f6 CoreFoundation`__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 14
    frame #18: 0x33b7d15c CoreFoundation`__CFRunLoopDoSources0 + 212
    frame #19: 0x33b7bf2e CoreFoundation`__CFRunLoopRun + 646
    frame #20: 0x33aef23c CoreFoundation`CFRunLoopRunSpecific + 356
    frame #21: 0x33aef0c8 CoreFoundation`CFRunLoopRunInMode + 104
    frame #22: 0x39af7394 WebCore`RunWebThread(void*) + 444
    frame #23: 0x3bcf00e0 libsystem_c.dylib`_pthread_start + 308
Owner

@kolyuchiy Just noticed that myself. Nasty UIWebView accesses CALayer from a background thread. I've updated my gist accordingly.

Hi Peter,
I found an issue when I alloc UIWebview in iOS 5 on main thread, EXC_BAD_ACCESS will show.
However the environment is ARC enabled, Do you have any idea about this?

UIKit`-[UIWebView retain]:
0x1d60d98: pushl %ebp
0x1d60d99: movl %esp, %ebp
0x1d60d9b: pushl %esi
0x1d60d9c: calll 0x1d60da1 ; -[UIWebView retain] + 9
0x1d60da1: popl %ecx
0x1d60da2: movl 5217911(%ecx), %edx
0x1d60da8: movl 8(%ebp), %eax
0x1d60dab: movl (%eax,%edx), %edx
0x1d60dae: movl 5217887(%ecx), %ecx
0x1d60db4: movl $2, %esi
0x1d60db9: lock <---------------------------------------------------------------------------------------Where showed BAD_ACCESS
0x1d60dba: xaddl %esi, (%edx,%ecx)
0x1d60dbe: testl %esi, %esi
0x1d60dc0: jns 0x1d60dc4 ; -[UIWebView retain] + 44
0x1d60dc2: ud2
0x1d60dc4: popl %esi
0x1d60dc5: popl %ebp
0x1d60dc6: ret

Owner

@WayneWeiZhang You might want to compile this debug helper without ARC.

PSPDFLogError isn't defined, either. Any reason PSPDFReplaceMethod isn't static?

Owner

Just use NSLog then. I really copied this from my commercial framework PSPDFKit, where the swizzle/replace is defined in the header. For this example having it static makes sense, yes.

posburn commented May 31, 2013

Super helpful @steipete - thanks! Helped me track down a bug that has been in the codebase for a while now and was tricky to reproduce.

I'm considering enabling this in non-debug builds, changing the assert to a Crashlytics log call. The idea is to have a clue in the crash report that this is what (likely) caused the crash. How bad an idea is that?

Peter, how can I use this gist? Just include the .m file in the project? Is that all? Thanks.

Owner
steipete commented Jun 2, 2013

@jklundell It doesn't use any private API and the performance impact is minimal - so you should be fine. Just don't hard-crash here, we don't know if Apple always plays by the rules (e.g. they use CALayer in a thread with UIWebView...)

@chandrahasan1put it in a file and compile without ARC.

@steipete why does the file have to be compiled without ARC?

Owner
steipete commented Jun 2, 2013

@segiddins It probably will work as well with ARC, but the compiler might put retains/releases in there where we don't want them, e.g. when calls happen during dealloc where we would then create a (short) resurrection of the object.

Update: I've added __unsafe_unretained, that should do the trick as well.

This is excellent. I was just recently wondering why this isn't already in Cocoa. Many thanks!

That's great! I wonder if you can use the same approach to check if an NSManagedObjectContext is being accessed from a thread other than the one it was created on

When I run it in my app delegate, it tells me "failed to add method: pspdf_..." for every method.

Same as michaeleisel. Have the following output in the console: 2013-08-02 09:26:31:640 [846:3079] Failed to add method: pspdf_setNeedsLayout on UIView
[846:3079] Failed to add method: pspdf_setNeedsDisplay on UIView
[846:3079] Failed to add method: pspdf_setNeedsDisplayInRect: on UIView

This is after calling: PSPDFAssertIfNotMainThread()

Ok don't worry - makes sense now.

I have DEBUG=1 preprocessor macro enabled for only debug builds (and have confirmed #if DEBUG ...#endif works correctly elsewhere in my code). However this assertion code still seems to be present in Release builds - I've written a little test app which deliberately hits UIKit from a background thread to test it.

This just saved me a few hours! Love it

For those interested, I've forked the gist to make trivial changes so it works with AppKit on OS X.

soniccat commented Mar 7, 2014

Great tool, thanks!

jomnius commented Jun 26, 2014

Thank you! Just found a brand new hibernating UI crash bug in code I'm maintaining, much appreciated!

k3zi commented Jul 11, 2014

Why don't we make a swizzle that automatically kicks every call to a UIKit object into the main thread???
Like this: https://gist.github.com/kdogisthebest/98ca835b15077d11dafc

@kdogisthebest because that would be hack, this gist is meant to find misuses instead of allowing them :)

This is incredible!

jomnius commented Sep 17, 2014

Line 30: Implicit declaration of function "PSPDFAssert' is invalid in C99
Line 38: Implicit declaration of function "PSPDFLogError' is invalid in C99

First one can be fixed by moving PSPDFAssert definition higher in code, but the other one is a mystery. Guess it can be replaced by NSLog (or similar)? I put there PSPDFAssert for now.

dlo commented Sep 21, 2014

Having the same issue(s) as @jomnius.

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

@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

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?

Anyone use this gist in Swift project?

@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!

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 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 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 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 commented Oct 11, 2015

I made a swift version

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.

How much can one love a single gist? <3

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 commented Nov 14, 2015

Is there an OSX version of this?

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

trusk89 commented Nov 27, 2015

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

@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 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)

smarxpan commented Jun 7, 2016

@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.

Owner
steipete commented Jul 6, 2016

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

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 ?

it just worked for me, thank you!

@nrbrook you are literally god

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

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