Caution! There’s a strong reference in Apple’s code that needs attention.
When learning Swift, strong and weak references are always on the agenda. The delegate pattern is a typical example of weak reference. However, there’s a strong reference in Apple’s code for delegates.
Today, let’s have a light and fun discussion about Swift. When developing apps with Swift, there are many famous and convenient open-source networks, so you might not have encountered the following issue firsthand.
😱 URLSessionDelegate
Let’s open the header of URLSession.
@available(iOS 7.0, *)
open class URLSession : NSObject, @unchecked Sendable {
open class var shared: URLSession { get }
public /*not inherited*/ init(configuration: URLSessionConfiguration)
public /*not inherited*/ init(configuration: URLSessionConfiguration, delegate: (any URLSessionDelegate)?, delegateQueue queue: OperationQueue?)
open var delegateQueue: OperationQueue { get }
open var delegate: (any URLSessionDelegate)? { get }
@NSCopying open var configuration: URLSessionConfiguration { get }
// blahblah
}
Did you find it? There’s no weak in the delegate declaration. Are we seeing it wrong? Let’s also check the Objective-C header.
@interface NSURLSession : NSObject
/*
* The shared session uses the currently set global NSURLCache,
* NSHTTPCookieStorage and NSURLCredentialStorage objects.
*/
@property (class, readonly, strong) NSURLSession *sharedSession;
/*
* Customization of NSURLSession occurs during creation of a new session.
* If you only need to use the convenience routines with custom
* configuration options it is not necessary to specify a delegate.
* If you do specify a delegate, the delegate will be retained until after
* the delegate has been sent the URLSession:didBecomeInvalidWithError: message.
*/
+ (NSURLSession *)sessionWithConfiguration:(NSURLSessionConfiguration *)configuration;
+ (NSURLSession *)sessionWithConfiguration:(NSURLSessionConfiguration *)configuration delegate:(nullable id <NSURLSessionDelegate>)delegate delegateQueue:(nullable NSOperationQueue *)queue;
@property (readonly, retain) NSOperationQueue *delegateQueue;
@property (nullable, readonly, retain) id <NSURLSessionDelegate> delegate;
@property (readonly, copy) NSURLSessionConfiguration *configuration;
// blahblah
Here, the delegate property has a ‘retain’ keyword.
Why would Apple do this?
The company that develops and distributes the Swift language, Apple, couldn’t have made a mistake, right?
In fact, if you look at the quick help for the delegate, it is explained, and it is also detailed in the developer documents.
The session object keeps a strong reference to this delegate until your app exits or explicitly invalidates the session. If you do not invalidate the session, your app leaks memory until it exits.
This code was made with a special purpose. However, if not examined closely, even experienced developers could easily make a mistake. If you just implement the delegate code as usual and assign it, the guidelines talk about calling invalidate, but what if you want to use strong reference code as a weak reference in an unchangeable part of the developer’s code? It’s simple.
struct WeakContainer<Object: AnyObject> {
weak var object: Object?
}
The example used a struct, but you could also rewrite it using a class. Let’s go back to the URLSession example and make more code.
class WeakURLSessionDelegate: NSObject, URLSessionDelegate {
weak var delegate: URLSessionDelegate?
func urlSession(_ session: URLSession, didBecomeInvalidWithError error: (any Error)?) {
delegate?.urlSession(session, didBecomeInvalidWithError: error)
}
// blahblah
}
class Foo: URLSessionDelegate {
let session: URLSession
func foo() {
session.delegate = WeakURLSessionDelegate(delegate: self)
}
}
Voila! You’ve now passed a weak reference to session.delegate.
But here’s the webview
There are not just one but many such precautions to take. WKWebView, which is used for communication between the web and apps through WKUserContentController, also has easily overlooked strong references.
func add(_ scriptMessageHandler: any WKScriptMessageHandler, name: String)
The function seen above doesn’t have any special references mentioned in the developer documents.
However, those in your team who have developed WKScriptMessageHandlers know from experience. Thus, they would have resolved this by writing a class or struct with a weak property as described earlier.
Aren’t you curious why? It’s fine because there’s a solution? Well, let’s open it up.
First, open WKUserContentController.mm in the WebKit open source.
- (void)_addScriptMessageHandler:(WebKit::WebScriptMessageHandler&)scriptMessageHandler
{
if (!_userContentControllerProxy->addUserScriptMessageHandler(scriptMessageHandler))
[NSException raise:NSInvalidArgumentException format:@"Attempt to add script message handler with name '%@' when one already exists.", (NSString *)scriptMessageHandler.name()];
}
- (void)addScriptMessageHandler:(id <WKScriptMessageHandler>)scriptMessageHandler name:(NSString *)name
{
auto handler = WebKit::WebScriptMessageHandler::create(makeUnique<ScriptMessageHandlerDelegate>(self, scriptMessageHandler, name), name, API::ContentWorld::pageContentWorld());
[self _addScriptMessageHandler:handler.get()];
}
Knowing the function name, you can easily follow along. Let’s open WebUserContentControllerProxy.cpp.
bool WebUserContentControllerProxy::addUserScriptMessageHandler(WebScriptMessageHandler& handler)
{
auto& world = handler.world();
for (auto& existingHandler : m_scriptMessageHandlers.values()) {
if (existingHandler->name() == handler.name() && existingHandler->world().identifier() == world.identifier())
return false;
}
addContentWorld(world);
m_scriptMessageHandlers.add(handler.identifier(), &handler);
for (auto& process : m_processes)
process.send(Messages::WebUserContentController::AddUserScriptMessageHandlers({ { handler.identifier(), world.identifier(), handler.name() } }), identifier());
return true;
}
The code line to note is m_scriptMessageHandlers.add(handler.identifier(), &handler); implemented as a hashmap in the form of HashMap<uint64_t, RefPtr<WebScriptMessageHandler>>. As commonly known, it’s a key-value storage form, and it uses strong references declared with RefPtr. A weak reference hashmap is separately implemented as WeakHashMap. It was briefly a C++ world, but if explained in the Swift world, it’s as if using a Dictionary.
Conclusion
What do you think? Today we looked at code provided by Apple that can create memory leaks if not used carefully. Even familiar APIs can lead to unforeseen issues until problems are encountered. Sometimes, understanding the development intent leads to a resolution, but there are also limitations created by internal implementations, like with the WebView API.
If you were disappointed by the discussions of strong and weak references in iOS interview preparations or question examples, consider furthering your thoughts on how to resolve strong references in unchangeable code. This could turn into a much more interesting and discussion-rich process.