📜 ⬆️ ⬇️

The killer bundle of NSCache and UINib

I want to share one more kresham with whom I understood a couple of months ago. Now, after the passage of time, crashreports of this type are no longer observed in HockeyApp, and earlier they were among the most popular. Actually, the problem has been observed for quite some time, but then our application still used TestFlight and the information was not enough for analysis. Kresh was characterized by something like this:

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 

On call - [UIApplication _performMemoryWarning] it is clear that the problem occurred while processing memory warning. Apparently, some object subscribed to UIApplicationDidReceiveMemoryWarningNotification and forgot to unsubscribe before its destruction. But checking by project code did not reveal suspicious situations - everyone who used this notification was either singleton or more or less correctly unsubscribed. At that time, the matter was limited to this; there were no ideas for fixing yet.

Then, when Apple bought TestFlight, we switched to HockeyApp. They use a cool kresreportilku (PLCrashReporter), and in general work with kreshami there was much better (you can also attach your own logs / info when sending a report from the device). But, returning to the problem, in addition to the stack above, these lines also appeared:
')
 Application Specific Information: objc_msgSend() selector name: setArchiveData: 

Now we know which selector was sent to the deceased object. In our code, such methods / properties were not present, which confirmed the previous analysis. Accordingly, the task is to find a class that has such a selector. The obj-c functions of the objc_getClassList runtime (returns the list of registered classes) and class_copyMethodList (allows you to get the methods of the instances and the class itself) help with this. Walking through all the classes and checking all their selectors, I got the only option - UINibStorage. This is a private class, and using swizzling its methods we see that it is created and held by UINibs. Next, again using swizzling and disassembling, we find out that UINib subscribes to UIApplicationDidReceiveMemoryWarningNotification, and when it is received, it clears the contents of its UINibStorage (including calls setArchiveData) - this call falls in the crash log. Cancellation of notification occurs in the UINib deallock. How did it happen that UINib died, but received a notification?

The problem seems to have arisen from the fact that we used NSCache to cache nibs. When memory is low, NSCache clears its contents in the background thread, i.e. essentially asynchronously with memory warning in the main thread. So in the background thread, it is called - [UINib dealloc], in which it unsubscribes from notifications, and the main thing is their processing. This is the wrong and dangerous approach to using NSNotificationCenter. Generally speaking, during the work on the project we had to fix a lot of bugs related to asynchrony, because Many asynchronous operations are performed there. One of the common mistakes that have been encountered is the cancellation or unsubscribing from anything in the deallock. This is too late, because the object is actually dying, and if an asynchronous operation at the same time tries to work with it, then it will end badly. Unfortunately, the harsh reality is that there is not always a good place to unsubscribe. In the case of UINib, it is clear that there is no such convenient place, therefore it is difficult to blame for this (rather, then it is worth blaming 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. I used to have to fix the crash associated with NSCache storage in NSCache - this is also not worth doing. But I also cannot call NSCache obviously guilty, since he should not think that it is impossible to send a release to an object in any background thread due to the fact that this release may be the last, and the dealloc does more than nothing. Perhaps this situation is one of those when clear and forgivable solutions give a negative result.

Source: https://habr.com/ru/post/254969/


All Articles