I'm playing around with a NotificationCenter clone that automatically unregisters its "notification blocks" whenever the parent object is deallocated. This was done by having an Obj-C NSMapTable with weak keys that maps observers to the blocks they registered, while a second Swift dictionary with strong keys/values map notification keys to a NSHashTable with weak references to the same blocks that are being retained by the first table.
final class Box<T>: NSObject {
let obj: T
init(obj: T) {
self.obj = obj
super.init()
}
}
final class NotificationCenter {
typealias ClosureBox = Box<() -> Void>
var observers = NSMapTable<AnyObject, NSHashTable<ClosureBox>>.weakToStrongObjects()
var notifications = [String: NSHashTable<ClosureBox>]()
func register(_ closure: @escaping () -> Void, notification string: String, forObserver observer: AnyObject) {
if observers.object(forKey: observer) == nil {
observers.setObject(NSHashTable(), forKey: observer)
}
if notifications[string] == nil {
notifications[string] = NSHashTable.weakObjects()
}
let box = ClosureBox(obj: closure)
observers.object(forKey: observer)?.add(box)
notifications[string]?.add(box)
}
func post(notification string: String) {
for box in notifications[string]?.allObjects ?? [] {
box.obj()
}
}
}
I have the following unit test to confirm this works, which uses an autoreleasepool to force the observer under test to deallocate:
func test_observerIsDeallocated() {
let center = NotificationCenter()
let notification = "myNotification"
var timesExecuted = 0
autoreleasepool {
let observer = UIView()
center.register({
timesExecuted += 1
}, notification: notification, forObserver: observer)
center.post(notification: notification)
XCTAssertEqual(timesExecuted, 1)
center.post(notification: notification)
XCTAssertEqual(timesExecuted, 2)
center.post(notification: "otherNotification")
XCTAssertEqual(timesExecuted, 2)
}
center.post(notification: notification)
XCTAssertEqual(timesExecuted, 2)
}
However, the last assertion fails. If I print the contents of the dictionaries at that point, it reveals this:
NSMapTable {
}
["myNotification": NSHashTable {
[9] <_TtGC16AppAttestTestsds3BoxFT_T__: 0x60000283ad20>
}
]
This shows that the observer was in fact deallocated, with its related "notification boxes" being evicted from the NSMapTable. But how come the box object is still being referenced by notifications's NSHashTable if it only holds weak references? I've tried multiple ways to write this, but the box never dies even though it isn't being retained by anything. Where are those 9 references coming from?