Что делать с Apple Сrash Logs
Давненько не писал в блог, а материала готового пока нет, поэтому сегодня будет скучная, но полезная статья. По новой работе столкнулся с нетривиальной проблемой. Смысл: после локального тестирования и убеждения себя в том, что все вроде бы ОК, собираю AdHoc версию приложения, отправляю тестировщику ipa. Тот инсталлирует приложение себе на девайс, запускает и начинает тестировать. Спустя некоторое время получаю от него репорт, мол вот тут-то упало. Пытаюсь воспроизвести — не выходит, у меня на двух девайсах все работает при тех же исходных. Что делать? Идеальным решением было бы как-то удаленно запустить под дебаггером приложение на девайсе тестировщика по интернету, и даже вроде я встречал упоминания о таких сервисах, но, во-первых как-то боязно доверять третим лицам свое приложение, а во-вторых, времени не было. В итоге попросил у тестировщика Crash Log — журнал аварийного завершения, который формируется внутри iOS каждый раз, когда какое-либо приложение аварийно завершается. Взять то взял, но что с ним делать?
Вот пример такого лога
Incident Identifier: 4186E31D-AAE4-4683-A70C-3DE9686BC1DC CrashReporter Key: 2e9f789a246d42d4a5ac38a7df65edf30b5b2a91 Hardware Model: iPod4,1 Process: MyApp [15863] Path: /var/mobile/Applications/2DA57CD3-D8E7-435C-8E4A-17335F6ECE19/MyApp.app/MyApp Identifier: MyApp Version: ??? (???) Code Type: ARM (Native) Parent Process: launchd [1] Date/Time: 2011-12-02 17:10:58.569 +0000 OS Version: iPhone OS 4.3.3 (8J2) Report Version: 104 Exception Type: EXC_CRASH (SIGABRT) Exception Codes: 0x00000000, 0x00000000 Crashed Thread: 0 Thread 0 name: Dispatch queue: com.apple.main-thread Thread 0 Crashed: 0 CoreFoundation.dylib 0x0144c5a9 0x321db000 + 185 1 libobjc.A.dylib 0x015a0313 0x36333000 + 44 2 libsystem_c.dylib 0x3635ebf8 0x36333000 + 179192 3 libstdc++.6.dylib 0x30d0ca64 0x30cc8000 + 281188 4 libobjc.A.dylib 0x306e306c 0x306dd000 + 24684 5 libstdc++.6.dylib 0x30d0ae36 0x30cc8000 + 273974 6 libstdc++.6.dylib 0x30d0ae8a 0x30cc8000 + 274058 7 libstdc++.6.dylib 0x30d0af5a 0x30cc8000 + 274266 8 libobjc.A.dylib 0x306e1c84 0x306dd000 + 19588 9 CoreFoundation 0x361c348a 0x36125000 + 648330 10 CoreFoundation 0x361c34c4 0x36125000 + 648388 11 CoreFoundation 0x361359d0 0x36125000 + 68048 12 MyApp 0x0000e998 0x1000 + 55704 13 MyApp 0x0000d972 0x1000 + 51570 14 CoreFoundation 0x3613356a 0x36125000 + 58730 15 UIKit 0x32edfec2 0x32ec3000 + 118466 16 UIKit 0x32edfe62 0x32ec3000 + 118370 17 UIKit 0x32edfe34 0x32ec3000 + 118324 18 UIKit 0x32edfb86 0x32ec3000 + 117638 19 UIKit 0x32ee041c 0x32ec3000 + 119836 20 UIKit 0x32ec552e 0x32ec3000 + 9518 21 UIKit 0x32ec4bfa 0x32ec3000 + 7162 22 CoreFoundation 0x3619aa2e 0x36125000 + 481838 23 CoreFoundation 0x3619c45e 0x36125000 + 488542 24 CoreFoundation 0x3619d754 0x36125000 + 493396 25 CoreFoundation 0x3612debc 0x36125000 + 36540 26 CoreFoundation 0x3612ddc4 0x36125000 + 36292 27 GraphicsServices 0x30f22418 0x30f1e000 + 17432 28 GraphicsServices 0x30f224c4 0x30f1e000 + 17604 29 UIKit 0x32ef1d62 0x32ec3000 + 191842 30 UIKit 0x32eef800 0x32ec3000 + 182272 31 MyApp 0x0003094c 0x1000 + 194892 32 MyApp 0x00002e64 0x1000 + 7780
Что можно из этого всего вынести?
Видно, что произошло исключение EXC_CRASH (SIGABRT) в главной нити с индексом 0. Далее идет стек вызовов — путь, по которому приложение пришло к краху. Но почему при запуске под дебаггером тот же самый стек выглядит по-другому, и дебаггер даже показывает какой-то текст ошибки?
Dec 2 22:43:30 unknown MyApp[8532] : strEqual [] [ ] Dec 2 22:43:30 unknown MyApp[8532] : -[NSExternalRefCountedData isEqualToString:]: unrecognized selector sent to instance 0x1d19e0 Dec 2 22:43:30 unknown MyApp[8532] : *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[NSExternalRefCountedData isEqualToString:]: unrecognized selector sent to instance 0x1d19e0' -[NSCFString sdfs]: unrecognized selector sent to instance 0x566b3b0 2011-12-03 00:05:40.682 MyApp[13462:c803] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[NSCFString sdfs]: unrecognized selector sent to instance 0x566b3b0' *** Call stack at first throw: ( 0 CoreFoundation 0x0144c5a9 __exceptionPreprocess + 185 1 libobjc.A.dylib 0x015a0313 objc_exception_throw + 44 2 CoreFoundation 0x0144e0bb -[NSObject(NSObject) doesNotRecognizeSelector:] + 187 3 CoreFoundation 0x013bd966 ___forwarding___ + 966 4 CoreFoundation 0x013bd522 _CF_forwarding_prep_0 + 50 5 MyApp 0x00015b01 -[ProfileViewController isExtraChanged] + 65 6 MyApp 0x000188eb -[ProfileViewController onBackClick:] + 267 7 UIKit 0x007654fd -[UIApplication sendAction:to:from:forEvent:] + 119 8 UIKit 0x007f5799 -[UIControl sendAction:to:forEvent:] + 67 9 UIKit 0x007f7c2b -[UIControl(Internal) _sendActionsForEvents:withEvent:] + 527 10 UIKit 0x007f67d8 -[UIControl touchesEnded:withEvent:] + 458 11 UIKit 0x00789ded -[UIWindow _sendTouchesForEvent:] + 567 12 UIKit 0x0076ac37 -[UIApplication sendEvent:] + 447 13 UIKit 0x0076ff2e _UIApplicationHandleEvent + 7576 14 GraphicsServices 0x02fee992 PurpleEventCallback + 1550 15 CoreFoundation 0x0142d944 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__ + 52 16 CoreFoundation 0x0138dcf7 __CFRunLoopDoSource1 + 215 17 CoreFoundation 0x0138af83 __CFRunLoopRun + 979 18 CoreFoundation 0x0138a840 CFRunLoopRunSpecific + 208 19 CoreFoundation 0x0138a761 CFRunLoopRunInMode + 97 20 GraphicsServices 0x02fed1c4 GSEventRunModal + 217 21 GraphicsServices 0x02fed289 GSEventRun + 115 22 UIKit 0x00773c93 UIApplicationMain + 1160 23 MyApp 0x00057fc9 main + 121 24 MyApp 0x00001ff5 start + 53 }
Дело в том, что, когда приложение запускается под отладчиком, у отладчика имеется карта (или таблица) символов (symbol map), которая представляет собой справочник идентификаторов программы (названий функций, переменных) и их адресов в памяти. Поэтому он может восстановить стэк вызовов используя привычные разработчику имена. Когда приложение на устройстве, у него может и не быть этой информации, поэтому стэк вызовов крэш-репорте он формирует в виде списка абсолютных адресов. Как же с этим быть? Давайте разберем, что из себя представляет запись стэка вызовов, например, вот эти:
//Crash log from Device 0 CoreFoundation.dylib 0x0144c5a9 0x321db000 + 185 1 libobjc.A.dylib 0x015a0313 0x36333000 + 44 ... //Crash log from Simulator 0 CoreFoundation 0x0144c5a9 __exceptionPreprocess + 185 1 libobjc.A.dylib 0x015a0313 objc_exception_throw + 44 ...
Запись состоит из пяти колонок.
В первой колонке содержится порядковый номер записи. Важно знать, в каком направлении следует стек вызовов. Следует от от самого большого порядкового номера к нулю. То есть, вызов с порядковым номером 0 и есть причина падения, перед ней выполнялся вызов с номером 1, и т.д.
Во второй колонке содержится название фреймворка или системной библиотеки. Видимо, это таблица импорта присутствует всегда на девайсе, поэтому тут нет расхождения между логом девайса и симулятора. Далее следует абсолютный адрес точки входа функции (функция = вызов). Тут тоже пока сходится.
В третьем столбце находится адрес функции.
В четвертом находится адрес какого-то сегмента, вероятно, точка входа во фреймворк или библиотеку, а может быть адрес экземпляра класса. В логе симулятора на этом месте название функции.
Пятый столбец вероятно содержит смешение в байтах от начала фрейворка до машинной инструкции, которая вызвала исключение. Если сложить четвертый и пятый получим третий.
Задача — восстановить из абсолютных адресов название класса и метода, вызвавших крах.
Cуществуют отдельные утилиты с графическим интерфейсом, но мне не охота было искать, и я попробовал стандартную утилиту командной строки atos (Address to Symbol). На вход ей дается имя бинарного файла и интересующий адрес, а на выходе получаем название класса, метода и номер строки в исходном коде.
Что для этого понадобится? Понадобится бандл приложения. Где его взять? Можно тупо выдрать из IPA файла, который ни что иное, как ZIP. Есть и другие способы, но о них долго рассказывать. И так, распаковываем из IPA папку MyApp.app и заходим в нее в терминале (Terminal.app). Внутри можно увидеть папку MyApp.app.dSYM — в ней то как раз и содержится таблица символов (преобразование адреса в название). У компилатора GCC есть опция Generate Debug Symbols — это в Project Build Settings секция Code Generation. Вообще, много любопытных вещей.
Если Generate Debug Symbols установить в NO, то дебаг инфо по таблице символов генерироваться не будет. Разумно так поступать на уровне Release-конфигурации.
Ситаксис использования прост:
# atos -arch armv6 -o MyApp 0x00004ee2
где
# — символ приглашения командной строки
-arch armv6 — указание таргет-платформы. тут важно понимать, под какую платформу собирался проект — это также можно узнать в Build Settings. Если указать неправильную платформу, можно получить не корректный результат.
-o MyApp — название исполняемого файла. Он находится там же, где и .dSYM файл, то есть в папке бандла приложения (MyApp.app)
-0x00004ee2 — собственно, виновник торжества — адрес крэш инструкции.
Ответ не заставляет себя долго ждать:
-[ProfileViewController requestUserRegisterOrUpdate] (in MyApp) (ProfileViewController.m:454)
Ну вот теперь стало гораздо понятнее.
Таким образом, можно отдельно сделать билд приложения без отладочной информации, а таблицу символов хранить отдельно. Когда пользователь пришлет крэш отчет, можно легко восстановить название упавшего вызова. К слову. Ровно в таком же виде Apple ожидает отчеты об ошибках неожиданного завершения той или иной программы, который пользователю предлагается отправить. И опять к слову, в таком же виде присылается отчет из iTunes Connect после reject’a приложения, если в процессе Review это самое приложение упало.