2. ИСТОРИЯ 1
Дано: у клиента стабильно повторяющийся краш, а у нас,
конечно же, все работает.
Exception Type: EXC_CRASH (SIGABRT)
Exception Codes: 0x0000000000000000, 0x0000000000000000
Exception Note: EXC_CORPSE_NOTIFY
Triggered by Thread: 0
Last Exception Backtrace:
0 CoreFoundation 0x18ef211b8 __exceptionPreprocess + 124
1 libobjc.A.dylib 0x18d95855c objc_exception_throw + 56
2 CoreFoundation 0x18edfc71c -[__NSArrayM objectAtIndex:]
3 BeautifulApp 0x10005ce4c -[BAWebSerivce ...]
3. ИСТОРИЯ 1
Дано: у клиента стабильно повторяющийся краш, а у нас,
конечно же, все работает.
Источник горя по крашлогу:
(но у нас-то все нормально)
- (void) dataProvider:(id) dataProvider
didFinishLoadingTasks: (NSArray<TFTask*>*) tasks
activeTaskIndex: (NSInteger) activeIndex
{
//...
self.tasks = tasks;
self.activeTask = tasks[activeIndex];
//...
}
4. ИСТОРИЯ 1
Дано: у клиента стабильно повторяющийся краш, а у нас,
конечно же, все работает.
Тем временем, ПМ, поставивший версию с тестфлайта,
воспроизводит баг с завидной регулярностью.
Чем отличаются билд у разработчика на телефоне и билд у
клиента в тестфлайте?
5. ИСТОРИЯ 1
Дано: у клиента стабильно повторяющийся краш, а у нас,
конечно же, все работает.
Тем временем, ПМ, поставивший версию с тестфлайта,
воспроизводит баг с завидной регулярностью.
Build configuration: Debug vs. Release
6. ИСТОРИЯ 1
И действительно,
int index;
//...
if ([jsonData[kHasActiveSelection] boolValue]) {
index = [jsonData[kSelectedItem] integerValue];
}
7. ИСТОРИЯ 1
И действительно,
int index;
//...
if ([jsonData[kHasActiveSelection] boolValue]) {
index = [jsonData[kSelectedItem] integerValue];
}
int index;
8. ИСТОРИЯ 1
Сейчас таким свойством обладают только элементарные типы (и
компилятор по-прежнему не предупреждает, если только это не
SEL). То есть встретиться с этой проблемой можно только используя
непроинициализированные индексы или не-ARC указатели
(например, на CoreFoundation-объекты или просто void* буферы)
Но до появления ARC эта проблема касалась в том числе и любых
объектов, указатели на которые создавались на стеке, и очень легко
было пострадать:
NSError* error;
[_managedObjectContext save: &error];
if (error) {
NSLog(@"error: %@", error);
}
9. ИСТОРИЯ 1
А вообще различия между Debug и Release конфигурациями
таят в себе много незабываемых и веселых моментов
10. ИСТОРИЯ 1
А вообще различия между Debug и Release конфигурациями
таят в себе много незабываемых и веселых моментов:
NSAssert([[self managedObjectContext] save: nil],
@"Error saving context");
11. ОБЫЧНАЯ ИСТОРИЯ ДВА
ИСТОРИЯ ДВА
• Дано: приложение сохраняет данные в папку документов, но
при следующем запуске приложения сохраненные данные
куда-то исчезают.
12. ОБЫЧНАЯ ИСТОРИЯ ДВА
ИСТОРИЯ ДВА
• Дано: приложение сохраняет данные в папку документов, но
при следующем запуске приложения сохраненные данные
куда-то исчезают.
• Медиаданные хранятся в виде файлов, пути к файлам
хранятся где-то неважно где (пусть хоть в NSUserDefaults)
13. ОБЫЧНАЯ ИСТОРИЯ ДВА
ИСТОРИЯ ДВА
• Дано: приложение сохраняет данные в папку документов, но
при следующем запуске приложения сохраненные данные
куда-то исчезают.
• Медиаданные хранятся в виде файлов, пути к файлам
хранятся где-то неважно где (пусть хоть в NSUserDefaults)
• Ошибок при записи нет. Все корректно записывается.
Ошибка при чтении есть: файл не найден. Как так не найден-
то, когда вот только что он нормально записался и путь тот же
самый?
14. ОБЫЧНАЯ ИСТОРИЯ ДВА
ИСТОРИЯ ДВА
• file:///Users/iosdeveloper/Library/Developer/CoreSimulator/
Devices/CAD31984-03BC-474C-9C3B-AAED8DBC2EF0/data/
Containers/Data/Application/8AB0AAF5-483A-4A21-
B503-86CFCE78C034/Documents/
15. ОБЫЧНАЯ ИСТОРИЯ ДВА
ИСТОРИЯ ДВА
• file:///Users/iosdeveloper/Library/Developer/CoreSimulator/
Devices/CAD31984-03BC-474C-9C3B-AAED8DBC2EF0/data/
Containers/Data/Application/8AB0AAF5-483A-4A21-
B503-86CFCE78C034/Documents/
• Снаряжаем экспедицию
16. ОБЫЧНАЯ ИСТОРИЯ ДВА
ИСТОРИЯ ДВА
• file:///Users/iosdeveloper/Library/Developer/CoreSimulator/
Devices/CAD31984-03BC-474C-9C3B-AAED8DBC2EF0/data/
Containers/Data/Application/8AB0AAF5-483A-4A21-
B503-86CFCE78C034/Documents/
• Снаряжаем экспедицию
• file:///Users/iosdeveloper/Library/Developer/CoreSimulator/
Devices/CAD31984-03BC-474C-9C3B-AAED8DBC2EF0/data/
Containers/Data/Application/8AB0AAF5-483A-4A21-
B503-86CFCE78C034/Documents/
17. ПРОСТО ИСТОРИЯ ТРИ
ИСТОРИЯ ТРИ
• Дано: при клике на ячейку модальный вьюконтроллер
стабильно показывается со второго раза.
18. ПРОСТО ИСТОРИЯ ТРИ
ИСТОРИЯ ТРИ
• Дано: при клике на ячейку модальный вьюконтроллер
стабильно показывается со второго раза.
• Удаляем все лишнее. Проблема сохраняется даже в проекте
на 20 строчек.
19. ПРОСТО ИСТОРИЯ ТРИ
ИСТОРИЯ ТРИ
• Дано: при клике на ячейку модальный вьюконтроллер
стабильно показывается со второго раза.
• Удаляем все лишнее. Проблема сохраняется даже в проекте
на 20 строчек.
• Замечаем, что иногда вьюконтроллер показывается все-таки
с первого раза, но с большой задержкой. Или при попытке
повернуть телефон. Или потрясти.
20. ПРОСТО ИСТОРИЯ ТРИ
ИСТОРИЯ ТРИ
• Дано: при клике на ячейку модальный вьюконтроллер
стабильно показывается со второго раза.
• Удаляем все лишнее. Проблема сохраняется даже в проекте
на 20 строчек.
• Замечаем, что иногда вьюконтроллер показывается все-таки
с первого раза, но с большой задержкой. Или при попытке
повернуть телефон. Или потрясти.
21. ПРОСТО ИСТОРИЯ ТРИ
ИСТОРИЯ ТРИ
• Для тех, кто сомневается, что это происходит в мейн треде:
• когда вы в последний раз попадали в didSelectRowAtIndexPath
в бэкграунде?
- (void) tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath
*)indexPath {
UIViewController* newController = [[UIStoryboard storyboardWithName:@"Main"
bundle:nil] instantiateViewControllerWithIdentifier:@"Modal"];
[self presentViewController: newController animated:YES completion:nil];
}
22. ПРОСТО ИСТОРИЯ ТРИ
ИСТОРИЯ ТРИ
• Ладно, ставим брейкпоинт, запускаем дебаггер, видим, что
это действительно главный тред. Ой. С дебаггером все
стабильно работает.
- (void) tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath
*)indexPath {
UIViewController* newController = [[UIStoryboard storyboardWithName:@"Main"
bundle:nil] instantiateViewControllerWithIdentifier:@"Modal"];
[self presentViewController: newController animated:YES completion:nil];
}
23. ПРОСТО ИСТОРИЯ ТРИ
ИСТОРИЯ ТРИ
• Ок. Сделаем так. В таком варианте мы видим, что тред все-
таки главный, а приложение все-таки не работает.
• Кстати, вьюконтроллер показывается, когда переключается
минута на часах в статус баре. Или уменьшается процент
батарейки.
- (void) tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath
*)indexPath {
NSLog(@"Thread: %@", [NSThread currentThread]);
UIViewController* newController = [[UIStoryboard storyboardWithName:@"Main"
bundle:nil] instantiateViewControllerWithIdentifier:@"Modal"];
[self presentViewController: newController animated:YES completion:nil];
}
24. ПРОСТО ИСТОРИЯ ТРИ
ИСТОРИЯ ТРИ
• Вот так все работает стабильно. Задача выполнена, но чувство
удовлетворения не достигнуто.
- (void) tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath
*)indexPath {
UIViewController* newController = [[UIStoryboard storyboardWithName:@"Main"
bundle:nil] instantiateViewControllerWithIdentifier:@"Modal"];
dispatch_async(dispatch_get_main_queue(), ^{
[self presentViewController:newController animated:YES completion:nil];
});
}
25. ПРОСТО ИСТОРИЯ ТРИ
ИСТОРИЯ ТРИ
• Оказывается, это известный баг с версии чуть ли не 5.х,
который связан с тем, что ранлуп главного потока почему-то
засыпает, и просыпается только от внешнего события, либо от
явного пробуждения. Например, вот так:
CFRunLoopWakeUp(CFRunLoopGetCurrent());
26. ПРОСТО ИСТОРИЯ ТРИ
ИСТОРИЯ ТРИ
• Задача выполнена, чувство удовлетворения достигнуто.
- (void) tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath
*)indexPath {
UIViewController* newController = [[UIStoryboard storyboardWithName:@"Main"
bundle:nil] instantiateViewControllerWithIdentifier:@"Modal"];
[self presentViewController: newController animated:YES completion:nil];
CFRunLoopWakeUp(CFRunLoopGetCurrent());
}
28. КАК ТРИ, ТОЛЬКО ЧЕТЫРЕ
ИСТОРИЯ ЧЕТЫРЕ
• Error: Error Domain=AVFoundationErrorDomain Code=-11800
"The operation could not be completed"
UserInfo={NSUnderlyingError=0x60800004c210 {Error
Domain=NSOSStatusErrorDomain Code=-12780 "(null)"},
NSLocalizedFailureReason=An unknown error occurred (-12780),
NSLocalizedDescription=The operation could not be completed}
29. КАК ТРИ, ТОЛЬКО ЧЕТЫРЕ
ИСТОРИЯ ЧЕТЫРЕ
• Дано: есть AVPlayer. Есть AVPlayerItem, в который положена
композиция. В композиции несколько треков. Если при
первом проигрывании композиции в каком-то из треков нет
данных, то потом этот трек не будет проигрываться никогда.
30. КАК ТРИ, ТОЛЬКО ЧЕТЫРЕ
ИСТОРИЯ ЧЕТЫРЕ
• Может, надо добавить на неиспользуемый трек пустоты?
• Может, надо, чтобы все треки были одинаковой длины?
• Может, проблема в том, что у нас везде разные знаменатели у
CFTime? (зачем они тогда вообще нужны, если надо самим
следить за одинаковостью)
• Может, надо уже просто добавить ассет с тишиной и ставить
его в начало каждого трека просто потому что почему бы и
нет? (конечно нет)
31. КАК ТРИ, ТОЛЬКО ЧЕТЫРЕ
ИСТОРИЯ ЧЕТЫРЕ
• НЕТ
• НЕТ
• НЕТ
• Да, даже вставление маленького звука в начало трека
работает через раз.
• Ничего не работает. Ну то есть как, все то работает, то не
работает. Режем лишнее, готовимся к Support Request.
32. КАК ТРИ, ТОЛЬКО ЧЕТЫРЕ
ИСТОРИЯ ЧЕТЫРЕ
• А в мини-варианте все заработало.
33. КАК ТРИ, ТОЛЬКО ЧЕТЫРЕ
ИСТОРИЯ ЧЕТЫРЕ
• Давайте посмотрим ближе
AVPlayerItem* item = [[AVPlayerItem alloc] initWithAsset: self.composition];
AVPlayer* player = [[AVPlayer alloc] initWithPlayerItem:item];
[player play];
34. КАК ТРИ, ТОЛЬКО ЧЕТЫРЕ
ИСТОРИЯ ЧЕТЫРЕ
• Давайте посмотрим ближе
• Отличие в том, что в мини-приложении я сразу создаю и
AVPlayerItem, и AVPlayer, а в большом приложении я
пользуюсь одними и теми же. AVPlayer тут роли не играет, а
вот AVPlayerItem созданный на базе AVMutableComposition
надо пересоздавать каждый раз, когда композиция
изменяется.
AVPlayerItem* item = [[AVPlayerItem alloc] initWithAsset: self.composition];
AVPlayer* player = [[AVPlayer alloc] initWithPlayerItem:item];
[player play];
35. НЕ СОВСЕМ ИСТОРИЯ ДАЖЕ
ИСТОРИЯ ПЯТЬ
• Дано: чужой проект, в котором после небольшого, казалось
бы, изменения, все перестало работать.
• Пока конфигурирование оформления ячеек производилось
локально, все было в порядке. Как только данные о стиле
ячейки стали приходить с сервера, все пошло прахом.
36. НЕ СОВСЕМ ИСТОРИЯ ДАЖЕ
ИСТОРИЯ ПЯТЬ
• Дано: чужой проект, в котором после небольшого, казалось
бы, изменения, все перестало работать.
• Пока конфигурирование оформления ячеек производилось
локально, все было в порядке. Как только данные о стиле
ячейки стали приходить с сервера, все пошло прахом.
[self doSomeAutomaticEventStyleSetup];
self.eventStyle = jsonData[kEventStyleKey];
37. НЕ СОВСЕМ ИСТОРИЯ ДАЖЕ
ИСТОРИЯ ПЯТЬ
if (dataObject.eventStyle == kDefaultStyleKey) {
//...
} else if (dataObject.eventStyle == kClassicStyleKey) {
//...
} else if (dataObject.eventStyle == kHighlightedStyleKey) {
//...
} //...
38. НЕ СОВСЕМ ИСТОРИЯ ДАЖЕ
ИСТОРИЯ ПЯТЬ
NSString* literal = @"literal"; // 0x00001111
NSString* immutable1 = [NSString stringWithString: literal]; //0x00001111
NSString* immutable2 = [NSString stringWithString: immutable1]; //0x00001111
NSString* immutable3 = [NSString stringWithFormat: @"%@", literal]; // 0x00002222
NSString* immutable4 = [NSString stringWithFormat: @"%@", immutable3]; // 0x00002222
NSString* immutable5 = [NSString stringWithFormat: @"%@", immutable4]; // 0x00002222
NSString* mutable = [immutable5 mutableCopy]; //some random address
NSString* immutableAgain = [NSString stringWithString: mutable]; //0x00002222 again