One of the iOS black arts is hunting down the cause of a crash report. At the best of times they’re vague and indicative of the final point of failure; rarely do they ever identify the actual root cause in any enlightening way.
So let’s take a look at a stack trace from one of our crash logs:
There isn’t much to go on, but two things stand out:
- SIGSEGV generally means something was over released, or since this is ARC, we left a dangling weak reference around.
- The bomb went off while a scroll view was animating; much sadness ensued.
From the method signature of the symbolicated log we can see that the scroll view was trying to message a delegate about an animation that was finishing. A quick search of our codebase revealed that none of our classes implement this method, so we’ve likely mishandled an object or two during the run of the app that lead to this crash.
Another clue is that all of the methods for UIScrollViewDelegate are optional, so there is likely a check to see if the delegate actually implements that selector like this:
But how can we be sure? Enter Hopper.
Hopper is a Mac App designed to poke around compiled binaries, in this case UIKit. Here’s what Hopper reveals about _delegateScrollViewAnimationEnded
As you can see there is indeed a call to respondsToSelector; Hopper can even approximate a pseudo code implementation for us:
So now we can see how a dangling weak reference could cause the crash. The delegate was deallocated without setting the scroll view’s delegate property to nil. Now we just need to find out which scroll view in our app could have caused this.
After a bit of sleuthing (we don’t actually have any scroll views, but lots of table views) I settled on the culprit likely being a tableview that is used in one of our views. To confirm this I wrote a simple test:
Sure enough that test failed, the table view controller was indeed leaving a dangling reference as both the dataSource and delegate of the table view. By simply setting both to nil in dealloc the test is satisfied.
Make Double Sure
Crashes like this are sometimes easy to reproduce live on a device or in the iOS simulator. However, I could not reproduce this on my own and thus settled for manipulating our unit tests to see if I could craft a test that crashed with the same stack trace:
By invoking the finish selector myself I found I could duplicate the call stack that of the original crash.
I don’t believe that is the right way to test this particular failure because _scrollViewAnimationEnded:finished: is private and its implementation could change in a future release. Calling it directly from the test introduces unnecessary variability in our test suite and makes the tests very brittle.
Instead our test should validate that the root cause has been addressed. Specifically it should validate that the view controller sets the delegate and dataSource properties to nil when it is deallocated. The only test we need to add for this particular crash is one we’ve already seen:
This small test validates that the root cause of this crash, as well as any others that may have been caused by the same issue, are now fixed.