Прежде чем отправить в AppStore ваше приложение, вы долго тестируете его, чтобы убедиться, что ваше приложение работает безупречно. Оно отлично работает на вашем устройстве, но после того, как приложение попало в App Store, некоторые пользователи сообщают, что оно «вылетает»!
Если вы похожи на меня, то вы хотите, чтобы ваше приложение было на пять с плюсом. Значит, вы возвращаетесь в свой код, чтобы исправить сбой… а куда надо смотреть?
Вот когда пригодятся аварийные журналы iOS. В большинстве случаев, вы получите очень подробную и полезную информацию о причинах аварии, это вроде обратной связи от хорошего учителя.
В этом уроке вы узнаете, как выглядят аварийные журналы, а также как получить аварийный журнал из iOS-устройства и iTunes Connect. Вы узнаете о симболизации и о том, как вернуться от аварийного журнала назад, в код. Мы также займёмся отладкой приложения с ошибками, которые могут привести к сбою в определенных ситуациях.
Что это за аварийный журнал и где его взять?
Когда приложение «падает», то есть аварийно завершает свою работу на устройстве iOS, операционная система создает отчет о сбое или аварийный журнал. Этот журнал сохраняется на устройстве.
Вы можете найти много полезной информации в аварийном журнале, в том числе те причины, по которым приложение аварийно завершило работу. Как правило, там есть полная трассировка стека каждого исполняемого потока, так что вы сможете увидеть, что происходило в каждом потоке в момент аварии, а также определить поток, где произошла катастрофа.
Есть много способов, как получить аварийный журнал с устройства.
Устройство, которое синхронизируется с iTunes, хранит свои аварийные журналы на ПК. В зависимости от ОС, вот места, где их можно найти:
Mac OS X:
~/Library/Logs/CrashReporter/MobileDevice/<DEVICE_NAME>
Windows XP:
C:Documents and Settings<USERNAME>Application DataApple ComputerLogsCrashReporterMobileDevice<DEVICE_NAME>
Windows Vista или 7:
C:Users<USERNAME>AppDataRoamingApple ComputerLogsCrashReporterMobileDevice<DEVICE_NAME>
Если у пользователя ваша программа аварийно завершает работу, вы можете попросить его синхронизировать это устройство с iTunes, забрать в одном из вышеуказанных мест (в зависимости от ОС) журнал и отправить его вам по почте.
Вы заинтересованы в том, что бы иметь все аварийные журналы, которые генерируют пользователи. Чем больше журналов у вас есть, тем легче вам найти и диагностировать недостатки вашего приложения!
Кроме того, вы также можете получить аварийные журналы из вашего устройства, с помощью Xcode, если он у вас установлен. Для этого подключите iOS-устройство к компьютеру и откройте Xcode. В меню выберите WindowOrganizer (Shift-CMD-2).
В окне Organizer перейдите на вкладку Devices. В левой навигационной панели найдите пункт Device Logs, как показано на изображении ниже:
Как вы видите на скриншоте, на панели слева есть несколько элементов Device Logs. Пункт Device Logs в разделе LIBRARY содержит все аварийные журналы от всех ваших устройств (точнее, тех, которые вы подключали к Xcode). В разделе DEVICES пункт Device Logs под названием конкретного устройства содержит журналы для каждого конкретного устройства.
Как только ваше приложение опубликовано, вы также можете получить аварийные журналы от ваших пользователей с помощью iTunes Connect. Чтобы сделать это, просто войдите в свою учетную запись iTunes Connect, перейдите в раздел Manage Your Applications, выберите приложение, от которого вы хотите получить аварийные журналы, нажмите на кнопку View Details ниже иконки приложения и нажмите на ссылку Crash Reports в разделе Links в панели справа.
Если там нет аварийных журналов, попробуйте нажать на кнопку Refresh. Если Вы продали еще недостаточно много копий вашего приложения или приложение выложено недавно, то возможно, никаких аварийных журналов доступно вам не будет.
Если же у вас они есть в iTunes Connect, то вы увидите что-то вроде этого:
Но иногда тут ничего нет, даже если ваши пользователи сообщают об аварийных остановах. В этом случае, попросите прислать вам аварийные журналы по почте.
Кто создаёт аварийные журналы?
Существуют две основные ситуации, которые могут привести возникновению аварийного журнала:
- Ваше приложение нарушает политики ОС.
- В вашем приложении есть ошибки.
Нарушение политик ОС включают в себя такие вещи, как:
- таймаут контрольного таймера во время запуска, приостановки работы, продолжения работы и выхода вашего приложения;
- принудительное закрытие приложения пользователем;
- прекращение работы программы из-за нехватки памяти.
Давайте рассмотрим эти случаи более подробно.
Таймаут контрольного таймера
Как вы, наверное, знаете, начиная с iOS 4.x, в большинстве случаев, когда вы выходите из iOS-приложения, приложение не прекращает свою работу — вместо этого оно переходит в фоновый режим.
Однако, иногда, операционная система прекращает работу вашего приложения и создаёт аварийный журнал, если приложение не отвечает на запросы ОС достаточно быстро. Речь идёт о следующих методах UIApplicationDelegate:
- application:didFinishLaunchingWithOptions:
- applicationWillResignActive:
- applicationDidEnterBackground:
- applicationWillEnterForeground:
- applicationDidBecomeActive:
- applicationWillTerminate:
На выполнению любого из перечисленных выше метода приложение получает ограниченное количество времени. Если обработка метода приложением занимает слишком много времени, операционная система аварийно завершит приложение.
Примечание: Это может легко произойти, если не выполнять длительные операции (например, доступ к сети) в фоновом потоке. Чтобы узнать о том, как это избежать, читайте наши учебники по Grand Central Dispatch и NSOperations.
Принудительное закрытие приложения пользователем
iOS, начиная с версии 4.x, поддерживает многозадачность. Если приложение блокирует пользовательский интерфейс и перестает отвечать на запросы, пользователь может дважды нажать главную кнопку iOS-устройства и завершить приложение. В этом случае операционная система создаёт аварийный журнал.
Примечание: Возможно, Вы заметили, что когда вы дважды нажмёте кнопку Домой, вы видите список всех приложений, которые вы раньше запускали. Эти программы не обязательно работают, но и они не обязательно приостановлены.
Обычно приложение получает около 10 минут на работу в фоновом режиме, начиная с того момента, как пользователь нажмёт на кнопку Домой, а затем приложение будет автоматически завершено операционной системой. Таким образом, список приложений, который вы видите, дважды нажав кнопку Домой – это только список приложений, которые раньше работали. Удаление значков для этих приложений не генерирует никаких аварийных журналов.
Прекращение работы программы из-за нехватки памяти
При наследовании UIViewController, вы, возможно, замечаете существование метода didReceiveMemoryWarning.
Любое приложение, которое вы видите работающим на экране, имеет самый высокий приоритет в плане доступа и использования памяти. Тем не менее, это не означает, что приложение получает всю доступную устройству память — каждое приложение получает только часть доступной памяти.
Когда потребление памяти доходит до определённого уровня, ОС посылает сообщение UIApplicationDidReceiveMemoryWarningNotification. И тогда же, приложение получает вызов метода didReceiveMemoryWarning.
В этот момент ваше приложение продолжает ещё нормально работать, а операционная система уже начинает закрыть программы, находящиеся в фоновом режиме, чтобы освободить память. После того, как все фоновые программы завершены, а вашему приложению всё ещё требуется память, операционная система прекращает ваше приложение и генерирует аварийный журнал.
ОС не создает аварийный журнал для фонового приложения, которое было прекращено при попытке освободить память вашему приложению.
Примечание: Документация Apple говорит, что Xcode не создаёт автоматически журнал при нехватке памяти. Вы должны получить их вручную, как описано выше. Однако, по моему личному опыту использования Xcode 4.5.2, журналы нехватки памяти импортируются автоматически с неизвестными значениями в полях «Процесс» и «Тип».
Стоит также отметить, что выделение большого куска памяти в очень короткий период времени увеличивает нагрузку на системную память — даже если это доля секунды. Это тоже может привести к сообщению о нехватке памяти.
Ошибки в приложении
Как вы догадываетесь, ошибки — это причина большинства сбоев приложения, и, следовательно, причина создания основной массы аварийных журналов. Ошибки бывают бесконечных разновидностей.
Ниже мы рассмотрим несколько примеров аварийных журналов от приложений с ошибками, и используем свои дедуктивные способности, чтобы найти виновных и исправить ошибку!
Пример аварийного журнала
Давайте начнем с того, что посмотрим на пример аварийного журнала, чтобы у нас было представление о том, чего ожидать, когда мы будем работать с реальными журналами.
Не мудрствуя лукаво, встречайте нового друга:
// 1: Информация о процессе
Incident Identifier: 30E46451-53FD-4965-896A-457FC11AD05F
CrashReporter Key: 5a56599d836c4f867f6eec76afee451bf9ae5f31
Hardware Model: iPhone4,1
Process: Rage Masters [4155]
Path: /var/mobile/Applications/A5635B22-F5EF-4CEB-94B6-FE158D885014/Rage Masters.app/Rage Masters
Identifier: Rage Masters
Version: ??? (???)
Code Type: ARM (Native)
Parent Process: launchd [1]
// 2: Основная информация
Date/Time: 2012-10-17 21:39:06.967 -0400
OS Version: iOS 6.0 (10A403)
Report Version: 104
// 3: Исключение
Exception Type: 00000020
Exception Codes: 0x000000008badf00d
Highlighted Thread: 0
// 4: Трассировка потоков
Thread 0 name: Dispatch queue: com.apple.main-thread
Thread 0:
0 libsystem_kernel.dylib 0x327f2eb4 mach_msg_trap + 20
1 libsystem_kernel.dylib 0x327f3048 mach_msg + 36
2 CoreFoundation 0x36bd4040 __CFRunLoopServiceMachPort + 124
3 CoreFoundation 0x36bd2d9e __CFRunLoopRun + 878
4 CoreFoundation 0x36b45eb8 CFRunLoopRunSpecific + 352
5 CoreFoundation 0x36b45d44 CFRunLoopRunInMode + 100
6 CFNetwork 0x32ac343e CFURLConnectionSendSynchronousRequest + 330
7 Foundation 0x346e69ba +[NSURLConnection sendSynchronousRequest:returningResponse:error:] + 242
8 Rage Masters 0x000d4046 0xd2000 + 8262
Thread 1:
0 libsystem_kernel.dylib 0x32803d98 __workq_kernreturn + 8
1 libsystem_c.dylib 0x3a987cf6 _pthread_workq_return + 14
2 libsystem_c.dylib 0x3a987a12 _pthread_wqthread + 362
3 libsystem_c.dylib 0x3a9878a0 start_wqthread + 4
// 5: Состояние потока
Thread 0 crashed with ARM Thread State (32-bit):
r0: 0x00000000 r1: 0x00000000 r2: 0x00000001 r3: 0x39529fc8
r4: 0xffffffff r5: 0x2fd7d301 r6: 0x2fd7d300 r7: 0x2fd7d9d0
r8: 0x2fd7d330 r9: 0x3adbf8a8 r10: 0x2fd7d308 r11: 0x00000032
ip: 0x00000025 sp: 0x2fd7d2ec lr: 0x001bdb25 pc: 0x30301838
cpsr: 0x00000010
// 6: Дамп памяти
Binary Images:
0xd2000 - 0xd7fff +Rage Masters armv7 <f37ee6d2c7b334868972e0e9c54f7062> /var/mobile/Applications/A5635B22-F5EF-4CEB-94B6-FE158D885014/Rage Masters.app/Rage Masters
0x2fe41000 - 0x2fe61fff dyld armv7 <75594988728831d98e1f7c4c7b7ca29d> /usr/lib/dyld
0x327f2000 - 0x32808fff libsystem_kernel.dylib armv7 <f167dacec44b3a86a8eee73400ff7a83> /usr/lib/system/libsystem_kernel.dylib
0x328a8000 - 0x328bdfff libresolv.9.dylib armv7 <e79b59a3406f34d9b37f8085955115ce> /usr/lib/libresolv.9.dylib
0x32a70000 - 0x32b35fff CFNetwork armv7 <3e973794a4d13428bb974edcb2027139> /System/Library/Frameworks/CFNetwork.framework/CFNetwork
0x32b7a000 - 0x32cc3fff libicucore.A.dylib armv7 <0253932c1b9038a0849ef73c38e076ca> /usr/lib/libicucore.A.dylib
0x32cc4000 - 0x32cc5fff CoreSurface armv7 <b3f9d4e8dd803a48b88c58a0663d92a3> /System/Library/PrivateFrameworks/CoreSurface.framework/CoreSurface
0x32f65000 - 0x32f8afff OpenCL armv7 <f7706501012430fc94ed99006419fba9> /System/Library/PrivateFrameworks/OpenCL.framework/OpenCL
В этом журнале куча таинственных вещей. Давайте пройдемся по его разделам:
(1) Информация о процессе
Первый раздел дает нам информацию о том процессе, который был аварийно остановлен.
- Incident Identifier — уникальный идентификатор журнала сбоя.
- CrashReporter Key — тоже уникальный ключ, который связан с идентификатором устройства. Хотя он анонимизирован, он дает вам очень полезную информацию: если вы видите, что все ваши 100 аварийных журналов имеют один и тот же параметр CrashReporter Key (или только два разных), это означает, что эта проблема — не широко распространенная проблема, а ограничена только одним или несколькими устройствами.
- Hardware Model — тип устройства. Если вы получаете много аварийных журналов от одной и той же модели устройства, это может означать, что ваше приложение не работает должным образом с конкретной моделью. В нашем примере это iPhone 4S.
- Process — имя приложения. Число в квадратных скобках – это идентификатор процесса приложения в момент сбоя.
- Следующие несколько строк должны быть понятны и так.
(2) Основная информация
Этот раздел дает вам некоторую базовую информацию о дате/времени сбоя и версии iOS, запущенного на устройстве. Если у вас много журналов сбоев от iOS 6.0, это может означать, что эта проблема специфична для iOS 6.
(3) Исключение
В этом разделе вы видите тип исключения, который был получен в момент сбоя. Вы также видите код исключения и поток, который допустил исключение. В зависимости от типа аварийного отчета, в этом разделе может быть некая дополнительная информация.
(4) Трассировка потоков
Этот раздел содержит трассировку для всех потоков приложения. Трассировка представляет собой список всех активных фреймов в момент сбоя. Мы видим, какие функции вызывались, когда произошёл сбой. Рассмотрим следующую строку:
2 XYZLib 0x34648e88 0x83000 + 8740
Тут четыре колонки:
- Номер фрейма — в данном случае, 2.
- Название модуля — в этом случае, XYZLib.
- Адрес функции, которая была вызвана — в данном случае, 0x34648e88.
- В четвертой колонке две части, базовый адрес и смещение. Тут это 0×83000 + 8740, где первое число указывает на файл, а вторая — на строку кода в файле.
(5) Состояние потока
Этот раздел содержит значения в регистрах на момент сбоя. Обычно этот раздел не нужен, потому что трассировка потоков уже дала вам необходимую информацию, чтобы решить вашу проблему.
(6) Дамп памяти
В этом разделе перечислены все модули, которые были загружены в память на момент сбоя.
Демистификация с помощью симболизации
Первый взгляд на трассировку потоков говорит о том, там нет смысла. Ты привык работать с именами функций и номерами строк, а не загадочными письменами, вроде этого:
6 Rage Masters 0x0001625c 0x2a000 + 30034
Процесс преобразования этих шестнадцатеричных адресов исполняемого кода в имена методов и номера строк называется симболизация (symbolification).
Когда вы получаете аварийный журнал от устройства используя Органайзер Xcode, то симболизация автоматически происходит через несколько секунд. Строчка из журнала сбоя выше после симболизации выглядит так:
6 Rage Masters 0x0001625c -[RMAppDelegate application:didFinishLaunchingWithOptions:] (RMAppDelegate.m:35)
Чтобы Xcode смог симболизировать аварийный журнал, он должен иметь доступ к файлу приложения, который был загружено в App Store, и DSYM-файлу, который был создан при компиляции приложения. Тут должно быть точное соответствие версий, в противном случае аварийный журнал не может быть полностью симболизирован.
То есть, важно сохранять каждую сборку, которую вы распространяете среди пользователей. Когда вы архивируете ваше приложение перед отправкой, Xcode сохраняет откомпилированный файл. Вы можете найти все архивы вашего приложения в Xcode Organizer, вкладка Archives.
Примечание: Вам нужно хранить и откомпилированный файл приложения и dSYM-файл, чтобы иметь возможность в полной мере симболизировать отчеты о сбоях. Вы должны архивировать эти файлы для каждой сборки, которую вы выкладываете в iTunes Connect.
DSYM-файл и откомпилированный файл связаны друг с другом на уровне сборки, и последующая версия, даже из тех же самых исходных файлов, не будет работать с файлами из других сборок.
Если вы используете пункт меню Build and Archive, файлы будут сохранены в нужном месте автоматически.
Сбои, вызванные нехваткой памяти
Журнал сбоя, вызванного нехваткой памяти немного отличается от обычных аварийных журналов и мы рассмотрим его отдельно.
При нехватке памяти, система виртуальной памяти посылает сообщение об этом приложениям с просьбой освободить память. Такие сообщения направляются всем запущенным приложениям и процессам.
Если памяти всё равно не хватает, система может завершить фоновые процессы, чтобы уменьшить нагрузку на память. Если достаточный объем памяти был освобожден, ваше приложение будет продолжать работать и отчёт о нехватке памяти не будет сгенерирован. В противном случае, ваше приложение будет аварийно завершено iOS, и будет создан аварийный журнал.
В аварийных журналах, созданных при нехватке памяти, нет раздела с трассировкой потоков приложения. Вместо этого, есть отчёт об использовании памяти каждым процессом с указанием количества страниц памяти. (На момент написания документа – одна страница равняется 4 Кб.)
Пометка (jettisoned) рядом с названием процесса говорит о том, что процесс был завершён iOS, чтобы освободить память. Если такая пометка около вашего приложения, это означает, что ваше приложение было аварийно остановлено, так как использовало слишком много памяти.
Журнал сбоя, вызванного нехваткой памяти выглядит примерно так:
Incident Identifier: 30E46451-53FD-4965-896A-457FC11AD05F
CrashReporter Key: 5a56599d836c4f867f6eec76afee451bf9ae5f31
OS Version: iPhone OS 3.1.3 (7E18)
Date/Time: 2012-10-17 21:39:06.967 -0400
Free pages: 96
Wired pages: 10558
Purgeable pages: 0
Largest process: Rage Masters
Processes
Name UUID Count resident pages
Rage Masters <cc527ca9b51937c5adbe035fe27a7b12> 9320 (jettisoned) (active)
mediaserverd <3d3800d6acfff050e4d0ed91cbe2467e> 255
dataaccessd <13d80b2e707acc91f9aa3ec4c715b9cc> 505
syslogd <8eddddc00294d5615afded36ee3f1b62> 71
apsd <32070d91b216d806973c8f1b1d8077a4> 171
securityd <b9e51062610d27f727c5119b8f80dcdf> 243
notifyd <591dd4dd804b4b8741f52335ea1fa4ab> 2027
CommCenter <b4b87526ae086bb62c982f1078f43f81> 189
SpringBoard <324939a437d1cca1fa4af72d9f5d0eba> 2158 (active)
accessoryd <8f21c8b376d16e2ccb95ed6d21d8317a> 91
configd <85efd41aceac34ccc0019df76623c7a9> 371
fairplayd <a2eaf736b3e07c7c9a2c82e9eb893555> 93
mDNSResponder <df1cd275e4ad434e0575990e8e1da4cb> 292
lockdownd <80d2bd44c0bcca273d48ce52010f7e65> 1204
launchd <a5988245aade809bf77576f1d9de42c5> 72
Когда ваше приложение прекращает работу из-за нехватки памяти, вам нужно исследовать то, как ваше приложение использует память и то как оно реагирует на предупреждения о нехватке памяти. Вы можете использовать утилиту Instruments, профили Allocations и Leaks, чтобы обнаружить утечки памяти. Если вы не знаете, как использовать эти инструменты, почитайте это руководство для начала.
И не забывайте о виртуальной памяти! Профили Leaks и Allocations утилиты Instruments не отслеживают графическую память. Вам необходимо при запуске профиля Allocations просмотреть данные профиля VM Tracker, чтобы узнать об использовании графической памяти.
Профиль VM Tracker по-умолчанию отключен. Для профилирования вашего приложения с VM Tracker, выберите строку с названием профиля VM Tracker в запущенной с профилем Allocations утилите Instrument, там поставьте флаг Automatic Snapshotting или просто нажмите кнопку Snapshot Now.
Как исследовать журнал аварийного останова, вызванного нехваткой памяти, мы рассмотрим ниже.
Коды исключений
Перед тем, как погрузиться в некоторые реальные аварийные журналы, поговорим ещё немного об интересной стороне аварийного журнала – о смешных кодах исключений.
Код исключения указывается в разделе № 3 (Исключение), см. приведенный выше пример. Есть несколько кодов исключений, которые могут возникнуть чаще, чем остальные.
Как правило, код исключения начинается с текста, потом одна или несколько шестнадцатеричных значений, которые являются процессор-специфичными кодами и которые могут дать вам информацию о характере сбоя. Эти коды могут сказать вам почему приложение аварийно остановлено, то ли из-за ошибки программирования, то ли из-за неверного доступа к памяти, то ли по какой-то другой причине.
Вот некоторые из наиболее распространенных кодов исключения:
- 0xbaaaaaad: Читается как «плоооохой». Код говорит о том, что это не аварийный журнал, а это stackshot – журнал, содержащий состояние стека системы в данный момент. Чтобы получить stackshot, нажмите одновременно на кнопку Home и любую клавишу регулировки громкости. Часто эти журналы создаются пользователями случайно и не указывают на ошибку.
- 0xc00010ff: Читается как «cool off» (остынь). Код говорит о том, что приложение было принудительно закрыто операционной системой в ответ на тепловое событие (стало горячо или холодно). Это может быть вызвано проблемой с конкретным устройством, или состоянием окружающей среды.
- 0x8badf00d: Читается как «ate bad food» (ели плохую еду). Этот код значит, что приложение было прекращено iOS по таймауту. Обычно это происходит из-за того, что время, которое потратило ваше приложение на запуск, останов или отклик на системные события было больше, чем нужно.
- 0xbad22222: Этот код означает, что ваше VoIP-приложение была прекращено iOS из-за слишком частых обращений.
- 0xdead10cc: Читается как «deadlock» (тупик). Код означает, что ваше приложение, находясь в фоновом режиме, занимало какие-нибудь системные ресурсы (вроде базы данных адресной книги).
- 0xdeadfa11: Читается как «deadfall» (западня). Код значит, что приложение было принудительно закрыто пользователем. Принудительное закрытие происходит когда пользователь удерживает на устройстве кнопку включения/выключения до тех пор, пока не появиться слайдер «Выключить», а затем удерживает главную кнопку. Согласно документации Apple, принудительный выход является причиной исключения с кодом 0xdeadfa11, потому, что приложение к тому моменту перестало отвечать.
Примечание: Помните, что принудительное прекращение приложения, находящегося в фоне, путём удаления его из списка задач не вызывает создания аварийного журнала. После того, как приложение было приостановлено, оно может быть прекращено операционной системой в любое время. И аварийный журнал создан не будет.
В омут головой!
Теперь у вас есть вся основная справочная информация, чтобы нырнуть в омут аварийных журналов, а также исправить некоторые неприятные ошибки! Теперь основной сценарий развития событий:
Вы только что начали работу в Rage-O-Rage LLC. У компании есть хорошо продаваемое в App Store приложение — Rage Masters.
Ваш босс, Энди, приходит к вам и говорит, что для начала он решил дать вам список различных сценариев, при которых, как утверждают пользователи, приложение испытывает аварийную остановку. Ваша задача — проработать эти сценарии, симболизировать аварийные журналы, предоставленные пользователями, найти ошибки в приложении и исправить их.
Вы можете скачать исходный код приложения отсюда.
Примечание: Если вы хотите иметь на своём устройстве аварийные журналы, созданное этим приложением, то выполните следующие действия:
- Загрузите исходный код и откройте проект в Xcode.
- Подключите авторизированное iOS-устройство с корректным профилем (Provisioning Profile).
- Выберите iOS-устройство, а не Simulator в качестве цели на панели инструментов Xcode и запустите приложение.
- Как только на устройстве вы увидите экран приложения по-умолчанию (полноэкранное изображение приложения), нажмите кнопку остановки в Xcode.
- Закройте Xcode.
- Запустите приложение непосредственно на устройстве.
- Протестируйте описанные ниже сценарии, затем подключите устройство к ПК и получить аварийные журналы из Xcode.
Сценарий 1. Плохой код на завтрак
Из писем пользователей вашего приложения: «Мужик, твоя программа — отстой! Я скачал её на свой iPod Touch, iPhone и iPod Touch моего сына. На всех устройствах, она упала сразу после запуска...»
Другое письмо: «Я загрузила вашу программу, но она не запускается. Я в печали...»
Следующее письмо более конкретное: «Я не могу запустить вашу программу. Я скачал её на все мои устройства и все устройства моей жены. На всех устройствах программа вываливается при запуске..."
Да ладно, не принимайте близко к сердцу! Как любой из этих комментариев даст вам подсказку? Взгляните лучше в аварийный журнал:
Incident Identifier: 85833DBA-3DF7-43EE-AF80-4E5C51091F42
CrashReporter Key: 5a56599d836c4f867f6eec76afee451bf9ae5f31
Hardware Model: iPhone4,1
Process: Rage Masters [20067]
Path: /var/mobile/Applications/B2121A89-3D1F-4E61-BB18-5511E1DC150F/Rage Masters.app/Rage Masters
Identifier: Rage Masters
Version: ??? (???)
Code Type: ARM (Native)
Parent Process: launchd [1]
Date/Time: 2012-11-03 13:37:31.148 -0400
OS Version: iOS 6.0 (10A403)
Report Version: 104
Exception Type: 00000020
Exception Codes: 0x000000008badf00d
Highlighted Thread: 0
Application Specific Information:
Soheil-Azarpour.Rage-Masters failed to launch in time
Elapsed total CPU time (seconds): 8.030 (user 8.030, system 0.000), 20% CPU
Elapsed application CPU time (seconds): 3.840, 10% CPU
Thread 0 name: Dispatch queue: com.apple.main-thread
Thread 0:
0 libsystem_kernel.dylib 0x327f2eb4 mach_msg_trap + 20
1 libsystem_kernel.dylib 0x327f3048 mach_msg + 36
2 CoreFoundation 0x36bd4040 __CFRunLoopServiceMachPort + 124
3 CoreFoundation 0x36bd2d9e __CFRunLoopRun + 878
4 CoreFoundation 0x36b45eb8 CFRunLoopRunSpecific + 352
5 CoreFoundation 0x36b45d44 CFRunLoopRunInMode + 100
6 CFNetwork 0x32ac343e CFURLConnectionSendSynchronousRequest + 330
7 Foundation 0x346e69ba +[NSURLConnection sendSynchronousRequest:returningResponse:error:] + 242
8 Rage Masters 0x000ea1c4 -[RMAppDelegate application:didFinishLaunchingWithOptions:] (RMAppDelegate.m:36)
9 UIKit 0x37f30ad4 -[UIApplication _handleDelegateCallbacksWithOptions:isSuspended:restoreState:] + 248
10 UIKit 0x37f3065e -[UIApplication _callInitializationDelegatesForURL:payload:suspended:] + 1186
11 UIKit 0x37f28846 -[UIApplication _runWithURL:payload:launchOrientation:statusBarStyle:statusBarHidden:] + 694
12 UIKit 0x37ed0c3c -[UIApplication handleEvent:withNewEvent:] + 1000
13 UIKit 0x37ed06d0 -[UIApplication sendEvent:] + 68
14 UIKit 0x37ed011e _UIApplicationHandleEvent + 6150
15 GraphicsServices 0x370835a0 _PurpleEventCallback + 588
16 GraphicsServices 0x370831ce PurpleEventCallback + 30
17 CoreFoundation 0x36bd4170 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__ + 32
18 CoreFoundation 0x36bd4112 __CFRunLoopDoSource1 + 134
19 CoreFoundation 0x36bd2f94 __CFRunLoopRun + 1380
20 CoreFoundation 0x36b45eb8 CFRunLoopRunSpecific + 352
21 CoreFoundation 0x36b45d44 CFRunLoopRunInMode + 100
22 UIKit 0x37f27480 -[UIApplication _run] + 664
23 UIKit 0x37f242fc UIApplicationMain + 1116
24 Rage Masters 0x000ea004 main (main.m:16)
25 libdyld.dylib 0x3b630b1c start + 0
Нашли в чем проблема? Код исключения — 0x000000008badf00d, и сразу после него, в журнале сказано:
Application Specific Information:
Soheil-Azarpour.Rage-Masters failed to launch in time
Elapsed total CPU time (seconds): 8.030 (user 8.030, system 0.000), 20% CPU
Elapsed application CPU time (seconds): 3.840, 10% CPU
Это значит, что приложению не удалось уложиться при запуске в отведённое для этого время и сторожевой таймер операционной системы прекратил работу приложения. Круто! Вы нашли причину, но почему (и что ещё более важно, где) это происходит?
Смотрим дальше журнал. Трассировку потоков принято читать в обратном порядке, снизу вверх. Самый последний фрейм (25 фрейм: libdyld.dylib) – это первый вызов, затем фрейм 24, Rage Masters, main (main.m:16) и так далее.
Нам интересны фреймы, которые связаны с кодом вашего приложения. Так что игнорируйте системные библиотеки и фреймворки. Вот эта строчка нам интересна:
8 Rage Masters 0x0009f244 -[RMAppDelegate application:didFinishLaunchingWithOptions:] (RMAppDelegate.m:35)
Приложение получило сбой в методе application:didFinishLaunchingWithOptions:, в 35-ой строке файла RMAppDelegate.m (RMAppDelegate.m: 35). Откройте Xcode и посмотрите на эту строку:
NSData *directoryData = [NSURLConnection sendSynchronousRequest:request returningResponse:nil error:nil];
Да, вот оно! Синхронный вызов веб-сервиса? В главном потоке? В application:didFinishLaunchingWithOptions:?! Кто писал этот код?
Во всяком случае, теперь это ваша работа — исправить это. Этот вызов должен быть асинхронным, а еще лучше, должен быть выполнен в другой части приложения, после того как application:didFinishLaunchingWithOptions: вернёт YES.
Чтобы перенести вызов в другое место может потребоваться значительные изменения, поэтому, в данный момент, просто сделаем минимум изменений, просто, чтобы приложение аварийно не останавливалось при запуске. Вы всегда можете вернуться к этому вопросу и сделать это совсем правильно. Замените строку «плохого» кода (и три строки после неё) на асинхронную версию:
[NSURLConnection sendAsynchronousRequest:request
queue:[NSOperationQueue mainQueue]
completionHandler:^(NSURLResponse *response, NSData *data, NSError *error)
{
NSURL *cacheDirectory = [[[NSFileManager defaultManager] URLsForDirectory:NSUserDirectory inDomains:NSCachesDirectory] lastObject];
NSURL *filePath = [NSURL URLWithString:kDirectoryFile relativeToURL:cacheDirectory];
[data writeToFile:[filePath absoluteString] atomically:YES];
}];
Сценарий 2. Где эта кнопка?
Пользователь пишет: «Я не могу отметить моего любимого персонажа закладкой. Когда я пытаюсь это сделать, приложение падает… ».
Другой пользователь: «Закладки не работают… Я вхожу в Подробную информацию, нажимаю на кнопку «Закладка» и БА-БАХ!»
Эти жалобы о многом не говорят, и существует куча причин, почему программа так себя ведёт. Посмотрим в аварийный журнал:
Incident Identifier: 3AAA63CC-3088-41CC-84D9-82FE03F9F354
CrashReporter Key: 5a56599d836c4f867f6eec76afee451bf9ae5f31
Hardware Model: iPhone4,1
Process: Rage Masters [20090]
Path: /var/mobile/Applications/B2121A89-3D1F-4E61-BB18-5511E1DC150F/Rage Masters.app/Rage Masters
Identifier: Rage Masters
Version: ??? (???)
Code Type: ARM (Native)
Parent Process: launchd [1]
Date/Time: 2012-11-03 13:39:00.081 -0400
OS Version: iOS 6.0 (10A403)
Report Version: 104
Exception Type: EXC_CRASH (SIGABRT)
Exception Codes: 0x0000000000000000, 0x0000000000000000
Crashed Thread: 0
Last Exception Backtrace:
0 CoreFoundation 0x36bff29e __exceptionPreprocess + 158
1 libobjc.A.dylib 0x34f0f97a objc_exception_throw + 26
2 CoreFoundation 0x36c02e02 -[NSObject(NSObject) doesNotRecognizeSelector:] + 166
3 CoreFoundation 0x36c0152c ___forwarding___ + 388
4 CoreFoundation 0x36b58f64 _CF_forwarding_prep_0 + 20
5 UIKit 0x37fbb0a8 -[UIApplication sendAction:to:from:forEvent:] + 68
6 UIKit 0x37fbb05a -[UIApplication sendAction:toTarget:fromSender:forEvent:] + 26
7 UIKit 0x37fbb038 -[UIControl sendAction:to:forEvent:] + 40
8 UIKit 0x37fba8ee -[UIControl(Internal) _sendActionsForEvents:withEvent:] + 498
9 UIKit 0x37fbade4 -[UIControl touchesEnded:withEvent:] + 484
10 UIKit 0x37ee35f4 -[UIWindow _sendTouchesForEvent:] + 520
11 UIKit 0x37ed0804 -[UIApplication sendEvent:] + 376
12 UIKit 0x37ed011e _UIApplicationHandleEvent + 6150
13 GraphicsServices 0x3708359e _PurpleEventCallback + 586
14 GraphicsServices 0x370831ce PurpleEventCallback + 30
15 CoreFoundation 0x36bd416e __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__ + 30
16 CoreFoundation 0x36bd4112 __CFRunLoopDoSource1 + 134
17 CoreFoundation 0x36bd2f94 __CFRunLoopRun + 1380
18 CoreFoundation 0x36b45eb8 CFRunLoopRunSpecific + 352
19 CoreFoundation 0x36b45d44 CFRunLoopRunInMode + 100
20 GraphicsServices 0x370822e6 GSEventRunModal + 70
21 UIKit 0x37f242fc UIApplicationMain + 1116
22 Rage Masters 0x000ca004 main (main.m:16)
23 libdyld.dylib 0x3b630b1c start + 0
Код исключения — SIGABRT. Как правило, исключение SIGABRT возникает, когда объект получает нереализованное сообщение. Или, проще говоря, когда есть вызов несуществующего метода объекта.
Обычно этого не происходит, так как если вы вызываете метод «foo» объекта «bar», то компилятор выдаст ошибку, что метод «foo» не существует. Но когда вы косвенно вызывать метод используя селектор, компилятор не сможет определить, существует или нет метод у объекта.
Вернёмся к аварийному журналу. Он говорит, что аварийный останов произошёл в потоке №0. Это значит, что у нас, скорей всего, ситуация, когда метод был вызван объектом главного потока, там где объект не реализует метод.
Если вы продолжите читать журнал трассировки, вы видите, что единственный вызов, связанный с вашим кодом – это фрейм 22, main.m: 16. Это не особенно помогло.
Посмотрим на вызовы фреймворков, и видим это:
2 CoreFoundation 0x36c02e02 -[NSObject(NSObject) doesNotRecognizeSelector:] + 166
Это не ваш код. Но по крайней мере, есть подтверждение, что был вызов нереализованного метода объекта.
Идём в RMDetailViewController.m, где реализована кнопка закладок. Найдём код, который делает закладку:
-(IBAction)bookmarkButtonPressed {
self.master.isBookmarked = !self.master.isBookmarked;
// Update shared bookmarks
if (self.master.isBookmarked)
[[RMBookmarks sharedBookmarks] bookmarkMaster:self.master];
else
[[RMBookmarks sharedBookmarks] unbookmarkMaster:self.master];
// Update UI
[self updateBookmarkImage];
}
Тут всё выглядит нормально, так что проверим сториборд (XIB файл) и убедимся, что кнопки подключены правильно.
Вот оно! В MainStoryboard.storyboard, кнопка связана с bookmarkButtonPressed: вместо bookmarkButtonPressed (обратите внимание на двоеточие в конце, которое говорит о том, что у метода есть параметр). Чтобы это исправить, замените название метода на такой:
-(IBAction)bookmarkButtonPressed:(id)sender {
// Тут всё как и было раньше...
}
Конечно, вы можете просто удалить связь с неправильным методом в XIB-файле и связать событие к правильным методом. В любом случае сработает.
И вот ещё одна причина аварийного останова устранена.
Сценарий 3. Закладкой больше, закладкой меньше...
Ещё одна жалоба от пользователя: «Я не могу удалить закладку на персонажа из окна закладок...». И ещё одно письмо о том же: «Если я пытаюсь удалить персонажа из закладок, приложение падает...»
К этому моменту, вы уже привыкли, что письма от пользователей не бывают полезными. К аварийным журналам!
Incident Identifier: 5B62D681-D8FE-41FE-8D52-AB7E6D6B2AC7
CrashReporter Key: 5a56599d836c4f867f6eec76afee451bf9ae5f31
Hardware Model: iPhone4,1
Process: Rage Masters [20088]
Path: /var/mobile/Applications/B2121A89-3D1F-4E61-BB18-5511E1DC150F/Rage Masters.app/Rage Masters
Identifier: Rage Masters
Version: ??? (???)
Code Type: ARM (Native)
Parent Process: launchd [1]
Date/Time: 2012-11-03 13:38:45.762 -0400
OS Version: iOS 6.0 (10A403)
Report Version: 104
Exception Type: EXC_CRASH (SIGABRT)
Exception Codes: 0x0000000000000000, 0x0000000000000000
Crashed Thread: 0
Last Exception Backtrace:
0 CoreFoundation 0x36bff29e __exceptionPreprocess + 158
1 libobjc.A.dylib 0x34f0f97a objc_exception_throw + 26
2 CoreFoundation 0x36bff158 +[NSException raise:format:arguments:] + 96
3 Foundation 0x346812aa -[NSAssertionHandler handleFailureInMethod:object:file:lineNumber:description:] + 86
4 UIKit 0x37f04b7e -[UITableView(_UITableViewPrivate) _endCellAnimationsWithContext:] + 7690
5 UIKit 0x3803a4a2 -[UITableView deleteRowsAtIndexPaths:withRowAnimation:] + 22
6 Rage Masters 0x000fd9ca -[RMBookmarksViewController tableView:commitEditingStyle:forRowAtIndexPath:] (RMBookmarksViewController.m:68)
7 UIKit 0x3809a5d4 -[UITableView(UITableViewInternal) animateDeletionOfRowWithCell:] + 80
8 UIKit 0x37fbb0a8 -[UIApplication sendAction:to:from:forEvent:] + 68
9 UIKit 0x37fbb05a -[UIApplication sendAction:toTarget:fromSender:forEvent:] + 26
10 UIKit 0x37fbb038 -[UIControl sendAction:to:forEvent:] + 40
11 UIKit 0x37fba8ee -[UIControl(Internal) _sendActionsForEvents:withEvent:] + 498
12 UIKit 0x37fbb0a8 -[UIApplication sendAction:to:from:forEvent:] + 68
13 UIKit 0x37fbb05a -[UIApplication sendAction:toTarget:fromSender:forEvent:] + 26
14 UIKit 0x37fbb038 -[UIControl sendAction:to:forEvent:] + 40
15 UIKit 0x37fba8ee -[UIControl(Internal) _sendActionsForEvents:withEvent:] + 498
16 UIKit 0x37fbade4 -[UIControl touchesEnded:withEvent:] + 484
17 UIKit 0x37ee35f4 -[UIWindow _sendTouchesForEvent:] + 520
18 UIKit 0x37ed0804 -[UIApplication sendEvent:] + 376
19 UIKit 0x37ed011e _UIApplicationHandleEvent + 6150
20 GraphicsServices 0x3708359e _PurpleEventCallback + 586
21 GraphicsServices 0x370831ce PurpleEventCallback + 30
22 CoreFoundation 0x36bd416e __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__ + 30
23 CoreFoundation 0x36bd4112 __CFRunLoopDoSource1 + 134
24 CoreFoundation 0x36bd2f94 __CFRunLoopRun + 1380
25 CoreFoundation 0x36b45eb8 CFRunLoopRunSpecific + 352
26 CoreFoundation 0x36b45d44 CFRunLoopRunInMode + 100
27 GraphicsServices 0x370822e6 GSEventRunModal + 70
28 UIKit 0x37f242fc UIApplicationMain + 1116
29 Rage Masters 0x000fb004 main (main.m:16)
30 libdyld.dylib 0x3b630b1c start + 0
Этот журнал очень похож на предыдущий аварийный журнал. Тут тоже SIGABRT исключение. Может тут та же причина: отправка сообщения объекту, у которого не реализован метод?
Давайте посмотрим трассировку, какие методы вызывались. Начните с нижней части. Последний вызов на ваш код в Rage Masters был в фрейме №6:
6 Rage Masters 0x00088c66 -[RMBookmarksViewController tableView:commitEditingStyle:forRowAtIndexPath:] (RMBookmarksViewController.m:68)
Это вызов метода UITableViewDataSource. И что? Если вы не уверены, что компания Apple реализовала этот метод — можете переписать его, но не похоже, что это так. Кроме того, это дополнительный, не обязательный метод делегата. Так что проблема не в вызове нереализованного метода.
Посмотрим на фреймы дальше:
3 Foundation 0x346812aa -[NSAssertionHandler handleFailureInMethod:object:file:lineNumber:description:] + 86
4 UIKit 0x37f04b7e -[UITableView(_UITableViewPrivate) _endCellAnimationsWithContext:] + 7690
5 UIKit 0x3803a4a2 -[UITableView deleteRowsAtIndexPaths:withRowAnimation:] + 22
В фрейме №5, UITableView вызывает другой собственный метод, deleteRowsAtIndexPaths:withRowAnimation: а потом вызывается _endCellAnimationsWithContext:, который выглядит как внутренний метод Apple. Затем, происходит исключение фреймворка Foundation, handleFailureInMethod:object:file:lineNumber:description:.
Если собрать это вместе с жалобами пользователей, то это выглядит так, как будто вы имеете дело с ошибкой в процедуре удаления UITableView. Идём в Xcode. Вы знаете, куда идти? Может ли это сказать нам аварийный журнал? Смотрим строку №68 в RMBookmarksViewController.m:
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {
[self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
}
Нашли, где проблема? Я буду ждать, пока вы не найдёте.
Кто-то забыл про источник данных! Код удаляет строку в представлении, но не меняет источник данных. Чтобы это исправить, замените код на следующий:
-(void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {
RMMaster *masterToDelete = [bookmarks objectAtIndex:indexPath.row];
[bookmarks removeObject:masterToDelete];
[[RMBookmarks sharedBookmarks] unbookmarkMaster:masterToDelete];
[self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
}
Вот так будет с каждой ошибкой! Бац! Бах! Бум!
Сценарий 4. Леденец
Письмо: «Мое приложение падает, когда персонаж лижет леденец...». Другой пользователь: «Я нажимаю кнопку «Лизнуть леденец» несколько раз, а затем приложение вылетает!»
Вот аварийный журнал:
Incident Identifier: 081E58F5-95A8-404D-947B-5E104B6BC1B1 CrashReporter Key: 5a56599d836c4f867f6eec76afee451bf9ae5f31 Hardware Model: iPhone4,1 OS Version: iPhone OS 6.0 (10A403) Kernel Version: Darwin Kernel Version 13.0.0: Sun Aug 19 00:28:05 PDT 2012; root:xnu-2107.2.33~4/RELEASE_ARM_S5L8940X Date: 2012-11-03 13:39:59 -0400 Time since snapshot: 4353 ms Free pages: 968 Active pages: 7778 Inactive pages: 4005 Throttled pages: 92319 Purgeable pages: 0 Wired pages: 23347 Largest process: Rage Masters Processes Name <UUID> rpages [reason] (state) lsd <6a9f5b5f36b23fc78f87b6d8f1f49a9d> 331 [vm] (daemon) (idle) afcd <b0aff2e7952e34a9882fec81a8dcdbb2> 141 [vm] (daemon) (idle) itunesstored <4e0cd9f873de3435b4119c48b2d6d13d> 1761 [vm] (daemon) (idle) softwareupdatese <2bc4b5ae016431c98d3b34f81027d0ae> 311 [vm] (daemon) (idle) Amazon <4600481f07ec3e59a925319b7f67ba14> 2951 [vm] (suspended) accountsd <ac0fce15c1a2350d951efc498d521ac7> 519 [vm] (daemon) (idle) coresymbolicatio <edba67001f76313b992056c712153b4b> 126 [vm] (daemon) (idle) Skype <504cf2fe60cb3cdea8273e74df09836b> 3187 [vm] (background) MobileMail <bff817c61ce33c85a43ea9a6c98c29f5> 14927 [vm] (continuous) MobileSMS <46778de076363d67aeea207464cfc581> 2134 [vm] (background) MobilePhone <3fca241f2a193d0fb8264218d296ea41> 2689 [vm] (continuous) librariand <c9a9be81aa9632f0a913ce79b911f27e> 317 [vm] (daemon) kbd <3e7136ddcefc3d77a01499db593466cd> 616 [vm] (daemon) tccd <eb5ddcf533663f8d987d67cae6a4c4ea> 224 [vm] (daemon) Rage Masters <90b45d6281e934209c5b06cf7dc4d492> 28591 [vm] (frontmost) (resume) ptpd <04a56fce67053c57a7979aeea8e5a7ea> 879 (daemon) iaptransportd <f784f30dc09d32078d87b450e8113ef6> 230 (daemon) locationd <892cd1c9ffa43c99a82dba197be5f09e> 1641 (daemon) syslogd <cbef142fa0a839f0885afb693fb169c3> 237 (daemon) mediaserverd <80657170daca32c9b8f3a6b1faac43a2> 4869 (daemon) dataaccessd <2a3f6a518f3f3646bf35eddd36f25005> 1786 (daemon) aosnotifyd <d4d14f2914c3343796e447cfef3e6542> 549 (daemon) wifid <9472b090746237998cdbb9b34f090d0c> 455 (daemon) SpringBoard <27372aae101f3bbc87804edc10314af3> 18749 backboardd <5037235f295b33eda98eb5c72c098858> 5801 (daemon) UserEventAgent <6edfd8d8dba23187b05772dcdfc94f90> 601 (daemon) mediaremoted <4ff39c50c684302492e396ace813cb25> 293 (daemon) pasteboardd <8a4279b78e4a321f84a076a711dc1c51> 176 (daemon) springboardservi <ff6f64b3a21a39c9a1793321eefa5304> 0 (daemon) syslog_relay <45e9844605d737a08368b5215bb54426> 0 (daemon) DTMobileIS <23303ca402aa3705870b01a9047854ea> 0 (daemon) notification_pro <845b7beebc8538ca9ceef731031983b7> 169 (daemon) syslog_relay <45e9844605d737a08368b5215bb54426> 0 (daemon) ubd <74dc476d1785300e9fcda555fcb8d774> 976 (daemon) twitterd <4b4946378a9c397d8250965d17055b8e> 730 (daemon) configd <4245d73a9e96360399452cf6b8671844> 809 (daemon) absinthed.N94 <7f4164c844fa340caa940b863c901aa9> 99 (daemon) filecoordination <fbab576f37a63b56a1039153fc1aa7d8> 226 (daemon) distnoted <a89af76ec8633ac2bbe99bc2b7964bb0> 137 (daemon) apsd <94d8051dd5f5362f82d775bc279ae608> 373 (daemon) networkd <0032f46009f53a6c80973fe153d1a588> 219 (daemon) aggregated <8c3c991dc4153bc38aee1e841864d088> 112 (daemon) BTServer <c92fbd7488e63be99ec9dbd05824f5e5> 522 (daemon) fairplayd.N94 <7bd896bd00783a48906090d05cf1c86a> 210 (daemon) fseventsd <996cc4ca03793184aea8d781b55bce08> 384 (daemon) imagent <1e68080947be352590ce96b7a1d07b2f> 586 (daemon) mDNSResponder <3e557693f3073697a58da6d27a827d97> 295 (daemon) lockdownd <ba1358c7a8003f1b91af7d5f58dd5bbe> 389 (daemon) powerd <2d2ffed5e69638aeba1b92ef124ed861> 174 (daemon) CommCenter <1f425e1e897d32e8864fdd8eeaa803a8> 2212 (daemon) notifyd <51c0e03da8a93ac8a595442fcaac531f> 211 (daemon) ReportCrash <8c32f231b2ed360bb151b2563bcaa363> 337
Этот журнал очень отличается от тех, что мы видели до сих пор!
Это аварийный журнал нехватки памяти iOS 6. Как уже говорилось ранее, аварийный журнал нехватки памяти отличается от других аварийных журналов, потому что он не указывает на конкретный файл или строку кода. Вместо этого, он рисует картину, сложившуюся в памяти устройства в момент ситуации, которая привела к аварийному останову.
Заголовок, впрочем, похож на заголовок обычного аварийного журнала: те же поля Incident Identifier, CrashReporter Key, Hardware Model, OS Version и другие.
Вот следующий раздел специфичен только для аварийный журнал нехватки памяти:
- Free pages – количество свободной памяти в страницах. Каждая страница соотвествтвует примерно 4КБ, поэтому наш журнал говорит о свободной памяти в размере около 3 872 КБ (или 3,9 МБ).
- Purgeable pages – часть памяти, которая может быть очищена от предыдущего использования и снова использована. В нашем журнале её 0 КБ.
- Largest process – имя приложения, которое использовало самое большое количество памяти на момент аварийного останова, и там написано наше приложение!
- Processes — список процессов, а также как они использовали память во время аварии. Тут имя процесса (первый столбец), уникальный идентификатор процесса (второй столбец) и количество страниц, используемых в процессе (третий столбец). В последнем столбце (State), вы видите состояние каждого приложения. Как правило, приложения, которые привели к аварийному останову находятся в состоянии frontmost. И тут наш Rage Masters, который использует 28591 страниц (или 114 364 МБ) — это много памяти!
Как правило, самый большой процесс и процесс в состоянии frontmost – это один и тот же процесс, а также это тот процесс, который привел к аварийному останову из-за нехватки памяти. Но вы можете увидеть некоторые случаи, когда крупнейшие процессы и процессы в состоянии frontmost – это не то же самое. Например, если вы видите, что больше всего памяти потребляет процесс SpringBoard, можете игнорировать его, потому что SpringBoard — это главный процесс, отвечающий за главный экран в Apple iOS. С него запускаются и загружаются все установленные приложения. Он всегда активен.
При нехватке памяти случиться, iOS посылает предупреждение о низком уровне памяти активному приложению и завершает фоновые процессы. Если активное приложение продолжает увеличивать использование памяти, iOS завершает его.
Чтобы найти причину проблем с нехваткой памяти, необходимо профилировать приложение, используя утилиту Instruments. Если вы не знаете, как это делать, есть учебник. Вместо этого, мы решим проблему «в лоб», просто обработаем в нашей программе событие о нехватке памяти.
Перейдите в Xcode в RMLollipopLicker.m. Это там реализован контроллер представления лизания леденца. Взгляните на исходный код:
#import "RMLollipopLicker.h"
#define COUNT 20
@interface RMLollipopLicker ()
@property (weak, nonatomic) IBOutlet UIProgressView *progressView;
@property (weak, nonatomic) IBOutlet UILabel *label;
@property (weak, nonatomic) IBOutlet UILabel *lickedTimeLabel;
@end
@implementation RMLollipopLicker {
NSOperationQueue *queue;
NSMutableArray *lollipops;
}
#pragma mark - Life cycle
- (void)viewDidLoad {
[super viewDidLoad];
self.progressView.progress = 0.0;
self.label.text = [NSString stringWithFormat:@"Tap on run and I'll lick a lollipop %d times!", COUNT];
self.lickedTimeLabel.text = @"";
lollipops = [[NSMutableArray alloc] init];
queue = [[NSOperationQueue alloc] init];
}
- (void)lickLollipop {
NSURL *fileURL = [[NSBundle mainBundle] URLForResource:@"Lollipop" withExtension:@"plist"];
NSDictionary *dictionary = [NSDictionary dictionaryWithContentsOfURL:fileURL];
NSString *lollipop = [dictionary objectForKey:@"Lollipop"];
[lollipops addObject:lollipop];
}
#pragma mark - IBActions
- (IBAction)doneButtonPressed:(id)sender {
[self dismissViewControllerAnimated:YES completion:nil];
}
- (IBAction)runButtonPressed:(id)sender {
[sender setEnabled:NO];
[queue addOperationWithBlock:^{
for (NSInteger i = 0 ; i <= COUNT ; i++) {
[self lickLollipop];
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
self.label.text = [NSString stringWithFormat:@"Licked a strawberry lollipop %d time(s)!", i];
self.lickedTimeLabel.text = [NSString stringWithFormat:@"Licked the same lollipop %d time(s)!", lollipops.count-1];
self.progressView.progress = (float)(i/COUNT);
if (i >= COUNT) {
self.label.text = [NSString stringWithFormat:@"Tap on run and I'll lick a lollipop %d times!", COUNT];
self.progressView.progress = 0.0;
[sender setEnabled:YES];
}
}];
}
}];
}
@end
Когда пользователь нажимает кнопку запуска, приложение запускает в фоновом режиме процедуру lickLollipop несколько раз, а затем обновляет на экране количество лизаний. lickLollipop читает большую строку NSString из PLIST-файла и добавляет её в массив. Эти данные не критичны, и могут быть воссозданы, не влияя на работу пользователей.
Заведите хорошую привычку — пользоваться любой ситуации, когда можно очистить данные, которые можно потом восстановить без ущерба для пользователей. Так вы освобождаете память, что делает вероятность появления предупреждений о нехватке памяти менее вероятной.
Итак, как можно улучшить код в нашем случае? Реализовать didReceiveMemoryWarning и избавиться от данных в массиве lollipops:
-(void)didReceiveMemoryWarning {
[lollipops removeAllObjects];
[super didReceiveMemoryWarning];
}
И всё будет хорошо!
Что дальше?
Ура, вы прошли через все четыре сценария развития аварийных ситуаций! Ваше приложение работает намного лучше, и вы получили важные навыки отладки по ходу обучения.
Вы можете скачать улучшенный проект здесь.
Автор: wildfish