Matt Rajca

A Note on Memory Management, WebViews, and JSExports

August 13, 2016

Archimedes for Mac uses (WebKit 1) WebViews to render live previews of documents. While working on improved cache management for the next release of Archimedes, I noticed WebView allocations were not going down as documents were being closed. What’s more, the PreviewViewControllers that own the WebViews were also not being deallocated.

I had a hunch retain cycles were to blame, so I launched Instruments and ran the Allocations instrument with the Record reference counts option turned on. Immediately, it became clear what was happening. PreviewViewController, which maintains a strong reference to the WebView, was being exported to JavaScriptCore as a JSExport as such:

- (void)webView:(WebView *)webView didClearWindowObject:(WebScriptObject *)windowObject forFrame:(WebFrame *)frame {
	frame.javaScriptContext[@"archimedes"] = self;
}

Here, JavaScriptCore was creating a strong reference back to the PreviewViewController, thus creating a strong reference cycle.

I tried unloading the page right before the document closes hoping the WebView would relinquish control of the PreviewViewController, but it did not work as I expected.

Next, instead of passing the entire view controller to JavaScriptCore, I decided to create a new class whose sole purpose is to relay messages to a weakly-referenced delegate object:

@protocol PreviewViewMessengerDelegate
- (void)previewViewMessengerWantsToGoToRange:(NSRange)range;
@end


@interface PreviewViewMessenger : NSObject <PreviewViewExport>
@property (nonatomic, weak) id <PreviewViewMessengerDelegate> delegate;
@end


@implementation PreviewViewMessenger

- (void)goToRange:(int)location length:(int)length {
	[self.delegate previewViewMessengerWantsToGoToRange:NSMakeRange(location, length)];
}

@end

And the JavaScriptContext was now set up as follows:

- (void)webView:(WebView *)webView didClearWindowObject:(WebScriptObject *)windowObject forFrame:(WebFrame *)frame {
	PreviewViewMessenger *messenger = [[PreviewViewMessenger alloc] init];
	messenger.delegate = self;
	frame.javaScriptContext[@"archimedes"] = messenger;
}

Now, while JavaScriptCore still retains the proxy object, it no longer retains our PreviewViewController since it is weakly-referenced. This allows the WebView to get destroyed when the document is closed, and a moment later the PreviewViewMessenger disappears along with it. As the saying by David Wheeler goes, maybe all problems in computer science really can be solved by adding a level of indirection. 😊