-
-
Save steipete/5664345 to your computer and use it in GitHub Desktop.
// 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. | |
#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) { | |
PSPDFAssert(c && origSEL && newSEL && 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); | |
}); | |
} | |
} | |
} | |
} |
@kdogisthebest because that would be hack, this gist is meant to find misuses instead of allowing them :)
This is incredible!
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.
Having the same issue(s) as @jomnius.
@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.
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?
@Yaro812 Just add the file to your project (and Compile Build Phase). It runs on load:
attribute((constructor)) static void PSPDFUIKitMainThreadGuard(void)
@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.
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
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.
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.
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
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)
@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.
Please dupe https://openradar.appspot.com/27192338 if you want to see this in UIKit.
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.
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
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