The killer bunch of NSCache and UINib
Thread 0 Crashed:
0 libobjc.A.dylib 0x39abcf42 objc_msgSend + 2
1 CoreFoundation 0x2bfe0c61 __CFNOTIFICATIONCENTER_IS_CALLING_OUT_TO_AN_OBSERVER__ + 10
2 CoreFoundation 0x2bf3c6d5 _CFXNotificationPost + 1782
3 Foundation 0x2cc6e129 -[NSNotificationCenter postNotificationName:object:userInfo:] + 70
4 Foundation 0x2cc72c8f -[NSNotificationCenter postNotificationName:object:] + 28
5 UIKit 0x2f750883 -[UIApplication _performMemoryWarning] + 132
6 libdispatch.dylib 0x3a0107a7 _dispatch_client_callout + 20
7 libdispatch.dylib 0x3a021253 _dispatch_source_latch_and_call + 624
8 libdispatch.dylib 0x3a0122ed _dispatch_source_invoke + 210
9 libdispatch.dylib 0x3a013e1f _dispatch_main_queue_callback_4CF + 328
10 CoreFoundation 0x2bfee3b1 __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__ + 6
11 CoreFoundation 0x2bfecab1 __CFRunLoopRun + 1510
12 CoreFoundation 0x2bf3a3c1 CFRunLoopRunSpecific + 474
13 CoreFoundation 0x2bf3a1d3 CFRunLoopRunInMode + 104
14 GraphicsServices 0x332cf0a9 GSEventRunModal + 134
15 UIKit 0x2f5487b1 UIApplicationMain + 1438
16 xxx 0x0015bb81 main (main.m:18)
17 libdyld.dylib 0x3a030aaf start + 0
By calling - [UIApplication _performMemoryWarning] it is clear that the problem occurred while processing the memory warning. Apparently, some object subscribed to UIApplicationDidReceiveMemoryWarningNotification and forgot to unsubscribe before its destruction. But a check on the project code did not reveal suspicious situations - everyone who used this notification was either singleton or more or less correctly unsubscribed. At that time, this was limited to this, there were no ideas for fix so far.
Then, when Apple bought TestFlight, we switched to HockeyApp. They use a cool cracker (PLCrashReporter), and in general, working with crashes there was much better (you can also attach your logs / info when sending a report from the device). But returning to the problem, in addition to the stack above, the following lines also appeared:
Application Specific Information:
objc_msgSend() selector name: setArchiveData:
Now we know which selector was sent to the deceased object. There were no such methods / properties in our code, which confirmed the previous analysis. Accordingly, the task is to find a class that has such a selector. The obj-c functions of the runtime objc_getClassList (gives a list of registered classes) and class_copyMethodList (allows you to get instance methods and the class itself) help with this. After going through all the classes and checking all their selectors, I got the only option - UINibStorage. This is a private class, and using the swizzling of its methods, we see that it is created and held by UINibs. Then, again, using swizzling and disassembling, we find out that UINib subscribes to UIApplicationDidReceiveMemoryWarningNotification, and when it is received, clears the contents of its UINibStorage (including calls setArchiveData) - this call falls into the crashlog. Unsubscribing from notification takes place in the UINib dealloc. How did it happen that UINib died, but received a notification?
The problem appeared to be due to the fact that we used NSCache to cache any. If there is not enough memory, NSCache clears its contents in the background thread, i.e. essentially asynchronous with memory warning in the main thread. T.O. - in the background thread - [UINib dealloc] is called, in which it is unsubscribed from notifications, and in the main they are processed. This is the wrong and dangerous approach to using NSNotificationCenter. Generally speaking, during the work on the project, we have managed to fix a lot of bugs related to asynchrony, because many asynchronous operations are performed there. One of the common mistakes that I have encountered is the cancellation or unsubscribing from something in deallok. This is too late. the object is actually dying, and if the asynchronous operation is trying to work with it at the same time, then it will end badly. Unfortunately, the harsh reality is that there is not always a good place where you could unsubscribe. In the case of UINib, it is clear that there is no such convenient place, so it’s difficult to blame it (rather, it’s better to blame the infrastructure or NSNotificationCenter).
As a solution to the problem, I wrote a trivial cache for storing nibs. In general, this is not the first crash with NSCache. Previously, I already managed to fix the crash associated with storing NSCache in NSCache - this is also not worth doing. But I also can’t call NSCache explicitly guilty, because he should not think that you cannot send a release to an object in any background thread because this release may be the last and dealloc does more than nothing. Perhaps this situation is one of those when clear and excusable solutions give a negative result.