Як скрэшить будь-який додаток на айфоні, і як цього не допустити

image

Одного разу ми, Surfingbird, знайшли дивну помилку, через яку додаток стабільно крэшилось. Пізніше виявилося, що майже будь-який додаток можна досить просто скрэшить (навіть програми, написані самою Apple). Про те, що ж це за помилка і як її обійти, ми розповімо у статті.

Відразу уточнимо, все описане вірно для iOS 7 і менше. Про те, що змінилося в iOS 8 — в кінці статті (нічого хорошого, насправді).
Почнемо з практики. Є 2 кнопки, кожна з них показує новий екран. Просто натисніть одночасно на обидві кнопки (потрібно трохи потренуватися) і потім 2 рази назад:

image

Для того, щоб впустити додаток, нам потрібен navigationController. Якщо в navigationController запушить viewController (з анімацією), потім, не чекаючи завершення анімації, запушить другий viewController та натиснути 2 рази кнопку «назад», тоді додаток скрэшится. Спочатку це звучить як маячня, адже ніхто не стане робити. Однак, не варто забувати, що в айфоне є мультитач і одночасно можна натиснути кілька кнопок. Власне, зовсім не складний код, який до цього призведе:

@interface ViewController ()
@property (strong, nonatomic) UIButton *buttonL;
@property (strong, nonatomic) UIButton *buttonR;
@end

@implementation ViewController

- (void)viewDidLoad {
[super viewDidLoad];

self.navigationItem.title = @"root";
self.view.backgroundColor = [UIColor whiteColor];

self.buttonL = [[UIButton alloc] initWithFrame:CGRectMake(0.0 f, 0.0 f, 1.0 f, 1.0 f)];
self.buttonL.backgroundColor = [UIColor blueColor];
[self.buttonL setTitle:@"push vc #1" forState:UIControlStateNormal];
[self.buttonL addTarget:self action:@selector(pushViewControllerOne) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:self.buttonL];

self.buttonR = [[UIButton alloc] initWithFrame:CGRectMake(0.0 f, 0.0 f, 1.0 f, 1.0 f)];
self.buttonR.backgroundColor = [UIColor redColor];
[self.buttonR setTitle:@"push vc #2" forState:UIControlStateNormal];
[self.buttonR addTarget:self action:@selector(pushViewControllerTwo) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:self.buttonR];
}

- (void) viewWillLayoutSubviews {
CGFloat width = self.view.bounds.size.width /2;
CGFloat height = self.view.bounds.size.height;

[self.buttonL setFrame:CGRectMake(0.0 f, 0.0 f, width, height)];
[self.buttonR setFrame:CGRectMake(width, 0.0 f, width, height)];
}

- (void) pushViewControllerOne {

UIViewController *vc1 = [UIViewController new];
vc1.navigationItem.title = @"#1";
vc1.view.backgroundColor = [UIColor whiteColor];
[self.navigationController pushViewController:vc1 animated:YES];
}

- (void) pushViewControllerTwo {

UIViewController *vc1 = [UIViewController new];
vc1.navigationItem.title = @"#2";
vc1.view.backgroundColor = [UIColor whiteColor];
[self.navigationController pushViewController:vc1 animated:YES];
}

@end

Якщо подивитися логи Xcode, можна побачити попередження про вкладеної анімації і можливих пошкодженнях навигейшен бару:
nested push animation can result in corrupted navigation bar
Finishing up a navigation transition in an unexpected state. Navigation Bar subview tree might get corrupted.
Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'can't add self as subview'
У мережі опис цієї помилки зустрічається дуже рідко, а рішення було знайдено лише одне, і воно не працює. Тому, ми вирішили стати Санта Клаусами і подарувати спільноті рішення проблеми, яку Apple ніяк не можуть вирішити.

Дозвіл проблеми досить очевидна: наследуемся від UINavigationController, всі пуши складаємо в чергу, потім виконуємо їх по черзі. Частину коду, необхідний для розуміння реалізації описана нижче:

//
// StackNavigationController.m
//

#import "StackNavigationController.h"

@interface StackNavigationController () <UINavigationControllerDelegate>
@property (nonatomic, assign) BOOL isTransitioning;
@property (nonatomic, strong) NSMutableArray *tasks;
@property (nonatomic, weak) id<UINavigationControllerDelegate> customDelegate;
@end

@implementation StackNavigationController

-(void)viewDidLoad {
[super viewDidLoad];

if (self.delegate) {
self.customDelegate = self.представник;
}
self.delegate = self;

self.tasks = [NSMutableArray new];
}

// we should save navController.представник to another property because we need delegate
// to prevent multiple push/pop bug
-(void)setDelegate:(id<UINavigationControllerDelegate>)delegate
{
if (delegate == self) {
[super setDelegate:delegate];
} else {
self.customDelegate = delegate;
}
}

- (void) pushViewController:(UIViewController *)viewController animated:(BOOL)animated {

@synchronized(self.tasks) {
if (self.isTransitioning) {

void (^task)(void) = ^{
[self pushViewController:viewController animated:animated];
};

[self.tasks addObject:task];
}
else {
self.isTransitioning = YES;
[super pushViewController:viewController animated:animated];
}
}
}

- (void) runNextTask {

@synchronized(self.tasks) {
if (self.tasks.count) {
void (^task)(void) = self.tasks[0];
[self.tasks removeObjectAtIndex:0];
task();
}
}
}

#pragma mark UINavigationControllerDelegate
-(void)navigationController:(UINavigationController *)navigationController didShowViewController:(UIViewController *)viewController animated:(BOOL)animated
{
self.isTransitioning = NO;

if ([self.customDelegate respondsToSelector:@selector(navigationController:didShowViewController:animated:)]) {
[self.customDelegate navigationController:navigationController didShowViewController:viewController animated:animated];
}

// black magic :)
// if one of push/pop will be without animation - we should place this code to the end of runLoop to prevent bad behavior
[self performSelector:@selector(runNextTask) withObject:nil afterDelay:0.0 f];
}

@end

Весь код можна знайти на гітхабі.

В останніх версіях iOS ситуація трохи покращилася. Якщо раніше в iOS 7 і менше, додаток крэшилось при одночасному натисканні на дві кнопки, то тепер в iOS 8 для цього знадобиться 3 кнопки. Але креш все одно неминучий.
Повторимося, застосовуючи цю практику можна скрэшить практично будь-який додаток. У нас, наприклад, стабільно виходить крэшить навіть App Store. Незрозуміло, чому Apple не вважає це проблемою і не займається її рішенням. А вам зустрічалася подібна проблема у ваших проектах, і як її вирішували?

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.