6
votes

I've found an issue that seems to be causing a deadlock in WebKit. If I run this code from my main thread, I rightly see an alert. I can tap on the "OK" button on the alert and it dismisses and all is working well:

[theWebView stringByEvaluatingJavaScriptFromString:@"alert('hi');"];

If I make a slight modification, then the alert message still appears, but the OK button cannot be tapped on - you cannot dismiss the alert and if you break into the app it is hung in the stringByEvaluatingJavaScriptFromString call:

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    dispatch_async(dispatch_get_main_queue(), ^{
        [theWebView stringByEvaluatingJavaScriptFromString:@"alert('hi');"];
    });
});

The only different in these two is that in the second one, it is running the JS in the main thread in the context of a dispatch queue.

On the other hand, if I do the following, then the hang does not occur:

- (void) showHi:(id) it
{
    [(UIWebView*)it stringByEvaluatingJavaScriptFromString:@"alert('hi');"];
}

....

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    [self performSelectorOnMainThread:@selector(showHi:) withObject:theWebView waitUntilDone:NO];
});

Can someone shine some light on what is going wrong to cause the hang?

EDIT:

Related questions:

Perform UI Changes on main thread using dispatch_async or performSelectorOnMainThread?
Whats the difference between performSelectorOnMainThread and dispatch_async on main queue?
Grand Central Dispatch (GCD) vs. performSelector - need a better explanation

Very similar question:

UIWebView stringByEvaluatingJavaScriptFromString hangs on iOS5.0/5.1 when called using GCD

3

3 Answers

4
votes

This seems to simply be a bug in UIWebView. According to this question, it was introduced in iOS 5 and did not deadlock on iOS 4.3 and below.

Interestingly, presenting a UIAlertView right before the call to stringByEvaluatingJavaScriptFromString: strangely prevents the deadlock:

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    dispatch_async(dispatch_get_main_queue(), ^{
        UIAlertView *message = [[UIAlertView alloc] initWithTitle:@"Test"
                                                          message:@"Test"
                                                         delegate:nil
                                                cancelButtonTitle:@"OK"
                                                otherButtonTitles:nil];
        [message show];

        [theWebView stringByEvaluatingJavaScriptFromString:@"alert('hi');"];
    });
});

However, my theory is this: When I pause execution after the deadlock occurs, I see that the WebThread is halted at __psynch_mutexwait. Since the JavaScript engine is executed on different threads, it must tell the main thread to display the alert view. However, stringByEvaluatingJavaScriptFromString: is a blocking call that returns a value. The value can only be returned once the alert is dismissed by clicking OK. This is where the deadlock seemingly occurs: From a different thread we tell the main thread to tell the web view to run JavaScript (which happens on yet another thread), which in turn tells the main thread to display an alert view, which can only deliver its return value back to JavaScript once OK is clicked. And only when the call to stringByEvaluatingJavaScriptFromString: returns is the block that we passed to GCD complete.

It must be a bug, though. It is strange that the deadlock doesn't occur when I display a UIAlertView first. Maybe iOS puts the second alert view on some kind of queue in that case, which prevents the deadlock. Odd!

5
votes

I think it was stated in webview Image class reference

Now, For your case of dead lock you can simulate the same by replacing this [self performSelectorOnMainThread:@selector(showHi:) withObject:theWebView waitUntilDone:NO];

with

performSelector:withObject:afterDelay:inModes: . With Respective mode by default it was NSDefaultRunLoopMode and which was atomic in nature whereas in your nonatomic case of dispatch_get_main_queue() you have to change the current dispatch mode of threading.

I hope it remained informative. Also, more suggestions are welcomed.

0
votes

There are several things that are probably a factor in this. As others have mentioned, JavaScript does not execute on the main thread, but rather its own background thread. You've probably noticed that execution of your code waits until the JavaScript is completed. I do not know the specific implementation in the code, but it sounds like it is internally using either dispatch_sync or dispatch_barrier_sync.

The next item that you should notice is that there is a significant difference between alerts presented from UIAlertView -show and JavaScript alert(..). UIAlertView -show operates asynchronously. You can tell this because code execution continues after the call to -show before the alert is dismissed. If you perform this same experiment in JavaScript, code execution is stalled until the alert is closed.

Lastly, there is a difference between dispatch queues and threads. You can read more in this thread about it. I suspect the JavaScript execution is performing [NSThread isMainThread] and it is reporting NO because it is on the main queue, but not the main thread. Since this check is reporting NO, it does a dispatch_sync to the main queue, which is already blocked waiting for the JavaScript queue.