31

Image of Crashlytics back trace from users in the field

We use Crashlytics, 30+ users have seen this crash. this crash log is from users in the field. we have never been able to reproduce it. This is running on iOS7. No clue what is causing this, as you can see there is nothing in the call stack relating to our app. Anyone else see this or solved the issue? thanks! Please don't ask me to post code (see comments above).

Shaik Riyaz
  • 11,204
  • 7
  • 53
  • 70
Richie Hyatt
  • 2,244
  • 2
  • 21
  • 14
  • Do you use `UIControlEventAllTouchEvents` anywhere near the crash? – Mick MacCallum Nov 19 '13 at 22:29
  • I have a few cases of users getting the exact same crash. I too have no idea how it's caused. It only seems to happen on iPhones and not iPads. – rmaddy Nov 19 '13 at 23:45
  • I have the same crash. The only code I could find that seemed to add self as subview was SVSegmentedControl. Has anyone made any progress on this? – SAHM Nov 21 '13 at 22:14
  • We do not use UIControlEventAllTouchEvents anywhere. thx. -rrh – Richie Hyatt Jan 07 '14 at 17:43
  • I've just posted the detail answer here: http://stackoverflow.com/a/21875395/721460. Hope it helps. – Arnol Feb 19 '14 at 09:28
  • possible duplicate of [iOS app error - Can't add self as subview](http://stackoverflow.com/questions/19560198/ios-app-error-cant-add-self-as-subview) – Rivera Apr 28 '14 at 05:59
  • @Rivera - this isn't an exact duplicate - the accepted answer for the linked question makes clear that the OP made a mistake in their viewDidLoad that caused this crash report. However, lots of people, me included were seeing this same crash report even though we were using the API correctly. There may be an argument for merging the two questions containing ALL the possible answers that generate the same crash report - misusing the API, or using the API correctly but triggering the crash because of iOS 7's changed implementation. See my answer below for why this can happen on iOS 7. – Rob Glassey Apr 28 '14 at 18:32

5 Answers5

34

tl;dr - see the Conclusion at the bottom.

I too have been getting occasional crash reports through from users regarding this, and then today suffered the indignity of it for myself. This was caused when selecting a row on my table view attempted to push a new nav controller, and then I pressed the back button. Strangely the nav controller I pushed contained no data. After pressing back, the view under the nav bar went black and the app crashed.

Looking around, all I could find was other users (here, here and here) suggesting this was down to calling a segue or pushing a view controller twice in rapid succession. However, I call pushViewController: from within my tableView:didSelectRowAtIndexPath: only once. Short of the user double tapping on a cell, I don't see why this could happen to me. Testing by double tapping a table view cell failed to reproduce the observed crash.

However, what if the user had double tapped because the main thread happened to be blocked at the time? (I know, I know, never ever block the main thread!)

To test this, I created some (rather horrific, sorry, quickly cut and paste from other code) class methods (in the fictional class MyDebugUtilities) to repeatedly block and unblock the main thread, launching it just before I opened the view controller containing the table view that caused the crash. Here's the code for anyone who wants to quickly check this for themselves (a call to [MyDebugUtilities repeatedlyBlockTheMainThread]; will block the main thread for 3 seconds using a semaphore, then queue up another call to repeatedlyBlockTheMainThread 3 seconds later, ad infinitum. BTW, you don't want to launch this method repeatedly or it'll end up just blocking all the time):

+ (void)lowCostSemaphoreWait:(NSTimeInterval)seconds
{
    // Use a semaphore to set up a low cost (non-polling) delay on whatever thread we are currently running
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
    dispatch_time_t delayTime = dispatch_time(DISPATCH_TIME_NOW, seconds * NSEC_PER_SEC);

    dispatch_after(delayTime, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        dispatch_semaphore_signal(semaphore);
    });

    NSLog(@"DELAYING...");
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    NSLog(@"END of delay\n\n");
}


+ (void)repeatedlyBlockTheMainThread
{    
    dispatch_async(dispatch_get_main_queue(), ^{
        // Block the main thread temporarily using a semaphore
        [MyDebugUtilities lowCostSemaphoreWait:3.0];

        // Queue up another blocking attempt to happen shortly
        dispatch_time_t delayTime = dispatch_time(DISPATCH_TIME_NOW, 3.0 * NSEC_PER_SEC);
        dispatch_after(delayTime, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            [MyDebugUtilities repeatedlyBlockTheMainThread];
        });
    });
}

Putting a NSLog call in my tableView:didSelectRowAtIndexPath:, and then attempting to double tap in one of the time periods when the main thread was blocked did indeed reproduce the bug as observed above - pressing back crashed the app, and from the NSLog it was obvious tableView:didSelectRowAtIndexPath: was getting called twice. I think here what is happening is that the touches are getting queued up while the main thread is blocked, and then delivered all-together as soon as it is freed up. Double-tapping when the main thread is not blocked does not generate two calls to tableView:didSelectRowAtIndexPath:, presumably because it's already processed the first touch and handled the push.

Because this requires the main thread to be blocked temporarily, and in a well crafted app this should happen rarely (if at all), this would explain the fact that this is really hard to reproduce - I've had crash reports for this from a tiny percentage of users, and because the crash reports didn't even indicate which of my view controllers had triggered the scenario, I had no idea of where to start looking until I experienced it for myself.

To solve this, the really simple solution is to create a BOOL property (selectionAlreadyChosen say), which you set to NO in viewWillAppear: (so when you go back to this screen it gets reset), and then implement tableView:willSelectRowAtIndexPath: to do something like:

- (NSIndexPath *)tableView:(UITableView *)tableView willSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
    if (self.selectionAlreadyChosen) {
        NSLog(@"BLOCKING duplicate calls to tableView:didSelectRowAtIndexPath:");
        return nil;
    }

    self.selectionAlreadyChosen = YES;
    return indexPath;
}

Not forgetting to remove any NSLogs and the call to repeatedlyBlockTheMainThead when you're done.

This exact scenario might not be the problem everyone else is experiencing, but I think there's a problem in the way Apple handles simultaneous multiple table view selections - it isn't just double-taps - testing the above solution by wildly tapping on the same cell when the main thread was blocked, generated a stream of BLOCKING duplicate calls to tableView:didSelectRowAtIndexPath: messages.

Actually, testing this on iOS 6 - the multiple calls to tableView:didSelectRowAtIndexPath: were not prevented, but these were handled differently (this being the result of calling pushViewController: repeatedly of course!) - on iOS 6 multiple taps while the main thread was blocked would give you the message:

Warning: Attempt to dismiss from view controller while a presentation or dismiss is in progress!

just before being unceremoniously dumped out of that UINavigationController. So it's maybe not so much that this is an iOS 7 bug, but that the handling has changed under the covers, and a problem that was always there is now manifesting as a crash.

Update:

Going through my project looking for other tableView's that exhibit this problem, I've found one that doesn't. The only differences I can see are that this working one is editable (so supports switching in and out of edit mode, moving rows, and swipe to delete), and is closer to the root of my view hierarchy. The one that had been failing above was not editable, and is way down the view hierarchy - appearing after a number of modal segues. I can't see any other major differences between them - they both share a common superclass - but one attempts to pushViewController: repeatedly without the above fix, and the other doesn't.

Interestingly, the one that doesn't exhibit the problem also calls performSegueWithIdentifier: instead of pushViewController: when it is in editing mode, and when this is the case, it DOES repeatedly call tableView:didSelectRowAtIndexPath: (and hence performSegueWithIdentifier:) - but something about calling pushViewController: seems to cancel the repeated calls to tableView:didSelectRowAtIndexPath:.

Confused? I know I am. Trying to break the 'working' view controller by making it non-editable does nothing. Replacing the non-edit mode pushViewController: with one of the performSegueWithIdentifier: caused the segue to be called multiple times, so it isn't anything to do with being in edit mode in the tableView.

Replacing the working view controller in the hierarchy with the one that doesn't (so that it was linked by rootViewController relationships rather than modal segues) FIXED THE PREVIOUSLY BROKEN VIEW CONTROLLER (with the previous fix taken out of it).

Conclusion - there's something about how your view hierarchy is wired up, and maybe down to using Storyboards, or modal segues, or maybe I've a broken view hierarchy that causes this - that causes multiple taps on a table view cell to be sent all at once if your main thread happens to be blocked at the instant the user double taps. If you are calling pushViewController: within your tableView:didSelectRowAtIndexPath: it will end up being called multiple times. On iOS 6 this will dump you out of the offending view controller. On iOS 7 this will cause a crash.

Community
  • 1
  • 1
Rob Glassey
  • 2,237
  • 20
  • 21
  • 3
    Thanks for doing all this research. One alternative might be for a view controller to check if `self == self.navigationController.viewControllers.lastObject`. (IE, if one push already happened, this test will fail.) This seems easier to maintain than littering your app with BOOLs in every table controller. Do you know if this approach would work? – Aaron Brager Jan 21 '14 at 20:56
  • Any method that prevents duplicate presses from being processed should work - whatever works best for you. For me, the BOOL check is simple, and only adds a couple of lines of code per VC. What I've described above is simply the failure scenario I encountered. I've went into some detail in case there is something in there that gives someone else that lightbulb moment - it is entirely possible there are other scenarios that I've not come across yet. This one does require a modal push to have happened prior, so in theory you only need to apply the fix to VC's that can appear after a modal push. – Rob Glassey Jan 21 '14 at 21:27
  • Had a similar issue because we weren't disabling presses on a collection view while awaiting on the next screen's data to load. Refactored so now the new screen pushes on and that controller manages its own data load, rather than letting the previous screen pre-populate it. – Matt S. May 08 '14 at 18:46
  • I had those crash reports in HockeyApp however with blocking main thread I'm still unable to reproduce them. Tapping rapidly on a row which will open new VC while main thread is blocked did not caused multiple instances added on top of each other. I tested with iPhone 4s, iOS 7.1.2. Also dispatch_release raises warning under ARC. – hris.to Apr 22 '15 at 12:30
  • @hris.to dispatch_release on semaphores was still required on earlier versions of iOS even with ARC, but no longer, so I'll remove that. Unsure why you were unable to reproduce, maybe you're seeing something slightly different? There are a couple of different problems that can lead to this crash report other than multiple VC's being pushed while the main thread is blocked, this is just the 'really weird, unexplained' one that cropped up unexpectedly on iOS 7 due to the underlying changes in behaviour. – Rob Glassey Apr 22 '15 at 15:40
  • Yep, that is definitely weird crash. I'm just using VC with tableView inside NavController. I'm pushing new VC in didSelectRow. When I block main thread and double tap, after the thread is unblocked it acts like a single tap(hence only one execute to didSelectRow). Indeed I observe the same crash report for my users in HockeyApp. I'm experiencing increasing of this crash with my latest versions, where I made a lot of improvements to make open-closing of views faster. That makes me think my problem is exactly the stated above, however still unable to reproduce :( – hris.to Apr 23 '15 at 06:47
2

I had the same crash caused by calling pushViewController/popViewController on iOS7 while in a viewDidLoad or viewWillAppear method.

It appears to be related to calling your #5 above with YES for animateTransition when an existing call to that same _block_invoke thing is running. Worked for me, your milage may vary.

My stack looked very similar, but not exactly the same. Not sure if that is due to differences in our code or what. I solved my issue by refactoring the loading sequence, and when I could not, controlling the value for "animated:" so it was only YES on one concurrent event.

Shaik Riyaz
  • 11,204
  • 7
  • 53
  • 70
Cory Trese
  • 1,148
  • 10
  • 23
  • thanks for the information. I checked our code and I don't believe pushViewController or popViewController is called in the viewDidLoad or viewWillAppear. I'll keep looking. thx. -rrh – Richie Hyatt Jan 07 '14 at 17:55
1

We had the same problem in our project.

The scenario in ours was: ViewController A had 2 buttons. Button 1 was pushing ViewController B and Button 2, was pushing ViewController C, to a navigationController.

If the user was pressing Button 1 and 2 the same time and after that was trying to get back to ViewController A (via popViewController), the app was crashing with the logs you posted. Strangely, this happens on iOS 7 only.

Our temporary solution was to override pushViewController of the navigationController to have a custom behavior so that such monkey scenarios are avoided. Hope this helps.

marius bardan
  • 4,962
  • 4
  • 29
  • 32
1

If you push (or pop) a view controller with animation(animated:YES) it doesn't complete right away, and bad things happen if you do another push or pop before the animation completes.

To reproduce this bug, try pushing or popping two view controllers at the same time. Example:

- (void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];
    UIViewController *vc = [[UIViewController alloc] init];
    [self.navigationController pushViewController:vc animated:YES];
}

You will receive this error:

2014-07-03 11:54:25.051 Demo[2840:60b] nested push animation can result in corrupted navigation bar 2014-07-03 11:54:25.406 Demo[2840:60b] Finishing up a navigation transition in an unexpected state. Navigation Bar subview tree might get corrupted.

There's a perfect solution: https://github.com/macjam/SafeTransition

Just add the code files into your project and makes your navigation controller as a subclass of APBaseNavigationController, and you'll be good to do.

Joywek
  • 67
  • 1
  • 5
0

It looks like nested/overlapping pushes are causing this. I made a simple wrapper pod for UINavigationController that prevents nested pops and pushes ( https://github.com/Itheme/SmartPopNavigationController — should be used for every navigation controller in app to work properly) Can't reproduce this crash after I've used it on all navigation controllers. Not 100% sure that it's not a coincidence though.

user1232690
  • 481
  • 5
  • 16