Back to Home

The killer bunch of NSCache and UINib

ios · objective-c · nscache · crash

The killer bunch of NSCache and UINib

    I want to share another crash that I figured out a couple of months ago. Now, over time, this type of crash report is no longer observed in HockeyApp, but used to be one of the most popular. Actually, the problem has been observed for quite some time, but then our application still used TestFlight and there was no information for analysis. Crash was characterized approximately by such stack:

    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.

    Read Next