Stopping a Runaway WebView
It turns out, all of the WebViews you create on the iPhone are managed by a central object called WebCore. It seems to be some sort of singleton that takes care of loading content and managing resource loading threads – but it can lead to some interesting problems if you try to rapidly create, display, and release a series of WebViews.
The upcoming version of NetSketch uses WebViews to display community content, but it wraps the web content in an iPhone native navigation bar. That way, there’s a feeling of “drilling down” into pages in the community. (Think: facebook app). It makes it hard to tell the entire thing is done with WebViews and you get some more user-friendly behavior (like using the top right button in the navigation bar to login/logout). After several iterations, I chose to implement this “drilldown” approach using a custom navigation bar and separate WebViews for each page. A “navigation stack” holds the WebViews of previous pages, and if you drill down too far, they are converted to UIImages and the actual WebViews are released. This behavior helps limit memory consumption, while allowing you to perform common actions quickly (going back and clicking another thumbnail, for example). Keeping images of very old views allows you to rapidly exit the interface without seeing blank pages or waiting for old pages to reload.
Unfortunately, my implementation seems to have run afoul of WebCore. It turns out that if you request a page in a WebView and attempt to release the WebView before it’s done loading, WebCore will attempt to send it delegate messages once it’s been destroyed – and throw exceptions all over the place. My first instinct was to call [webView stopLoading], but that is (apparently) asynchronous and doesn’t actually stop the WebCore from preparing some of the content. If you register an object as a WebView delegate, you’ll notice that you can still recieve webViewDidFinishLoad:(UIWebView *)webView after a stopLoading call.
I messed around with this for a while, and was almost ready to create a “ready-to-release” stack for webViews that were still loading. As a last resort, I tried calling [webView loadRequest: nil], and got some promising results. When you call loadRequest, the WebView makes an asynchronous call to
- (BOOL)webView:(UIWebView *)w shouldStartLoadWithRequest:(NSURLRequest *)r navigationType:(UIWebViewNavigationType)navigationType
When this call returns true, the WebView attempts to load the URL and realizes it is nil. It will almost immediately call – (void)webView:(UIWebView *)w didFailLoadWithError:(NSError *)error, and then it is officially DEAD. You can safetly release the WebView and its delegate without running the risk of further callbacks hitting deallocated objects.
This isn’t a perfect solution (read: it’s a ridiculous hack). It makes stopping and releasing a WebView a multi-step asynchronous process. However, unlike the [webView stopLoading] approach, it results in a consistent series of delegate callbacks that you can observe and also appears to work 100% of the time.
In my implementation, the navigation controller tells a WebView wrapper object to “unlink” the WebView. It detaches itself from everything and calls loadRequest:nil on the WebView. When the didFailLoadWithError: delegate call is received, the WebView is released, the delegate connection is broken and the wrapper is automatically released as well.
So far, so good.
But I still wonder – what exactly does stopLoading do?