8

My app is crashing in iOS 5 because I have some code that is calling UIKit instances from a secondary thread. You know you have this problem when you see the following error:

bool _WebTryThreadLock(bool), 0x811bf20: Multiple locks on web thread not allowed! Please file a bug. Crashing now…

So my question is what are some ways that I can find the code that is calling the UIKit instances from a secondary thread?

Here are some things I’ve tried already:

  1. Commented out blocks that could be violating the rule
  2. Added assert([NSThread isMainThread]) in places that might be processing in secondary thread
  3. Added a symbolic breakpoint for _WebTryThreadLock

These things have helped me to find problem areas. However, in my final crash the _WebTryThreadLock breakpoint has no stack trace in any of the other threads. So, how I can find the code that causing the problem without a stack trace?

Thanks for your time!

johnnieb
  • 3,982
  • 4
  • 29
  • 32
  • This might be a simulator bug. Does the issue also occur when running on an actual iOS device? – Craig Otis Oct 20 '11 at 16:42
  • Yes, it occurs in the iOS 5 Simulator and iOS devices running iOS 5. – johnnieb Oct 20 '11 at 16:58
  • Ah, all right. A quick Google search of that error message led me to believe it was a simulator bug, but if it's occurring elsewhere then I'm not entirely sure. Do you by any chance have 2+ UIWebViews displaying on the screen/loading at the same time? – Craig Otis Oct 20 '11 at 17:06
  • No web views but a lot of background processing. Finding the culprit has proved to be a challenge. – johnnieb Oct 20 '11 at 17:32
  • 1
    @johnnieb Did you ever find the issue? I am getting the same error when in my app. After I quickly dismiss 2 modal views, the next time I try to present a new modal view it crashes. Ive attempted to change things around but I keep getting the crash. – RyanG Oct 22 '11 at 17:34
  • 4
    @RyanGarchinsky We did find the problem. It wasn't at all what we expected. We had a login screen that was automatically logging the user into the app. It was presenting a keyboard by setting the firstResponder. UIKit was retaining the keyboard firstResponder reference, which in turn was blocking our WebThread. We burned a lot of hours trying to track this one down. Good Luck! – johnnieb Oct 23 '11 at 14:38
  • @johnnieb Thanks for the info! Yeah I am getting this crash when I try to push a new modal view OR when I tap a text field; it crashes before the keyboard comes up fully. But that thing is I am not using a web view anywhere .. I am doing some asynchronous web service calls in places but not when its crashing – RyanG Oct 24 '11 at 02:50
  • @RyanGarchinsky The keyboard is suspect. Turn it off and see if you still see the problem. – johnnieb Oct 24 '11 at 18:24

4 Answers4

3

I adapted the PSPDFUIKitMainThreadGuard.m to allow one to not have to worry about these things. Here: https://gist.github.com/k3zi/98ca835b15077d11dafc :

#import <objc/runtime.h>
#import <objc/message.h>

// Compile-time selector checks.

#define PROPERTY(propName) NSStringFromSelector(@selector(propName))

// A better assert. NSAssert is too runtime dependant, and assert() doesn't log.
// http://www.mikeash.com/pyblog/friday-qa-2013-05-03-proper-use-of-asserts.html
// Accepts both:
// - PSPDFAssert(x > 0);
// - PSPDFAssert(y > 3, @"Bad value for y");
#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)

///////////////////////////////////////////////////////////////////////////////////////////
#pragma mark - Helper for Swizzling

BOOL PSPDFReplaceMethodWithBlock(Class c, SEL origSEL, SEL newSEL, id block) {
    PSPDFAssert(c && origSEL && newSEL && block);
    Method origMethod = class_getInstanceMethod(c, origSEL);
    const char *encoding = method_getTypeEncoding(origMethod);

    // Add the new method.
    IMP impl = imp_implementationWithBlock(block);
    if (!class_addMethod(c, newSEL, impl, encoding)) {
        NSLog(@"Failed to add method: %@ on %@", NSStringFromSelector(newSEL), c);
        return NO;
    }else {
        // Ensure the new selector has the same parameters as the existing selector.
        Method newMethod = class_getInstanceMethod(c, newSEL);
        PSPDFAssert(strcmp(method_getTypeEncoding(origMethod), method_getTypeEncoding(newMethod)) == 0, @"Encoding must be the same.");

        // If original doesn't implement the method we want to swizzle, create it.
        if (class_addMethod(c, origSEL, method_getImplementation(newMethod), encoding)) {
            class_replaceMethod(c, newSEL, method_getImplementation(origMethod), encoding);
        }else {
            method_exchangeImplementations(origMethod, newMethod);
        }
    }
    return YES;
}

// This installs a small guard that checks for the most common threading-errors in UIKit.
// This won't really slow down performance but still only is compiled in DEBUG versions of PSPDFKit.
// @note No private API is used here.
__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) {
                    if(!NSThread.isMainThread){
                        dispatch_async(dispatch_get_main_queue(), ^{
                            ((void ( *)(id, SEL, CGRect))objc_msgSend)(_self, newSelector, r);
                        });
                    }else{
                        ((void ( *)(id, SEL, CGRect))objc_msgSend)(_self, newSelector, r);
                    }
                });
            }else {
                PSPDFReplaceMethodWithBlock(UIView.class, selector, newSelector, ^(__unsafe_unretained UIView *_self) {
                    if(!NSThread.isMainThread){
                        dispatch_async(dispatch_get_main_queue(), ^{
                            ((void ( *)(id, SEL))objc_msgSend)(_self, newSelector);
                        });
                    }else
                        ((void ( *)(id, SEL))objc_msgSend)(_self, newSelector);
                });
            }
        }
    }
}

It automatically kicks calls into the main thread and thus you wouldn't even have to do anything but plop the code in.

keji
  • 5,947
  • 3
  • 31
  • 47
3

Your assert() is probably the most valuable tool in this. I've been known to put a similar assertion at the beginning of every method in my Controller classes. If that doesn't find it, I add the assertion to my View classes. If that doesn't find it, I add it to any Model classes that I think are main-thread only.

To @craig's comment, the fact that it claims to be an internal bug might be accurate. But I think you're on the right path to closely examine your own code first.

Rob Napier
  • 286,113
  • 34
  • 456
  • 610
1

This problem comes because you want to access to UI from secondary thread somehow, it can from webview of whatever else. It is not permitted because UIKit is not thread safe and can be accessed only from MainThread. The very first thing you can do is to change your thread call to [self performSelectorOnMainThread:@selector(myMethod) withObject:nil waitUntilDone:NO]; (look for documentation). In case when you have no other choice you can use GCD(Grand Central Dispathc)...

Garnik
  • 423
  • 1
  • 6
  • 20
1

This code (just add to project and compile this file without ARC) causes assertions on UIKit access outside the main thread: https://gist.github.com/steipete/5664345

I've just used it to pickup numerous UIKit/main thread issues in some code I've just picked up.

Andrew Ebling
  • 10,175
  • 10
  • 58
  • 75