Демистификация аварийных журналов iOS

http://www.raywenderlich.com/23704/demystifying-ios-application-crash-logs
  • Перевод
  • Tutorial


Прежде чем отправить в AppStore ваше приложение, вы долго тестируете его, чтобы убедиться, что ваше приложение работает безупречно. Оно отлично работает на вашем устройстве, но после того, как приложение попало в App Store, некоторые пользователи сообщают, что оно «вылетает»!

Если вы похожи на меня, то вы хотите, чтобы ваше приложение было на пять с плюсом. Значит, вы возвращаетесь в свой код, чтобы исправить сбой… а куда надо смотреть?

Вот когда пригодятся аварийные журналы iOS. В большинстве случаев, вы получите очень подробную и полезную информацию о причинах аварии, это вроде обратной связи от хорошего учителя.

В этом уроке вы узнаете, как выглядят аварийные журналы, а также как получить аварийный журнал из iOS-устройства и iTunes Connect. Вы узнаете о символизации и о том, как вернуться от аварийного журнала назад, в код. Мы также займёмся отладкой приложения с ошибками, которые могут привести к сбою в определенных ситуациях.

Что это за аварийный журнал и где его взять?


Когда приложение «падает», то есть аварийно завершает свою работу на устройстве iOS, операционная система создает отчет о сбое или аварийный журнал. Этот журнал сохраняется на устройстве.

Вы можете найти много полезной информации в аварийном журнале, в том числе те причины, по которым приложение аварийно завершило работу. Как правило, там есть полная трассировка стека каждого исполняемого потока, так что вы сможете увидеть, что происходило в каждом потоке в момент аварии, а также определить поток, где произошла катастрофа.

Есть много способов, как получить аварийный журнал с устройства.
Устройство, которое синхронизируется с iTunes, хранит свои аварийные журналы на ПК. В зависимости от ОС, вот места, где их можно найти:

Mac OS X:
~/Library/Logs/CrashReporter/MobileDevice/<DEVICE_NAME>

Windows XP:
C:\Documents and Settings\\Application Data\Apple Computer\Logs\CrashReporter\MobileDevice\<DEVICE_NAME>


Windows Vista или 7:
C:\Users\\AppData\Roaming\Apple Computer\Logs\CrashReporter\MobileDevice\<DEVICE_NAME>

Если у пользователя ваша программа аварийно завершает работу, вы можете попросить его синхронизировать это устройство с iTunes, забрать в одном из вышеуказанных мест (в зависимости от ОС) журнал и отправить его вам по почте.

Вы заинтересованы в том, что бы иметь все аварийные журналы, которые генерируют пользователи. Чем больше журналов у вас есть, тем легче вам найти и диагностировать недостатки вашего приложения!
Кроме того, вы также можете получить аварийные журналы из вашего устройства, с помощью Xcode, если он у вас установлен. Для этого подключите iOS-устройство к компьютеру и откройте Xcode. В меню выберите Window\Organizer (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. Это там реализован контроллер представления лизания леденца. Взгляните на исходный код:

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];
}

И всё будет хорошо!

Что дальше?


Ура, вы прошли через все четыре сценария развития аварийных ситуаций! Ваше приложение работает намного лучше, и вы получили важные навыки отладки по ходу обучения.

Вы можете скачать улучшенный проект здесь.
Метки:
Поделиться публикацией
Похожие публикации
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама
Комментарии 16
  • 0
    Как вовремя! Я как раз смотрел на крэш-лог с первого iPad, думал как к нему подступиться. Теперь вижу (jettisoned), понимаю что в переводе на человеческий это Out of memory. Спасибо, автор!
    • +1
      Спасибо за перевод! )
      Вот перевел бы кто все статьи raywenderlich.com и собрал бы в одном месте…
      • +1
        Спасибо за перевод! На raywenderlich.com еще множество интересного материала
        • +1
          Пожалуйста! Обязуюсь и дальше выкладывать переводы с RayWenderlich.
        • +3
          Статья про крэшлоги и ни слова про dwarfdump? Нет пути!

          Применение этой замечательной команды:
          dwarfdump --lookup $[0x1000 + 899296] --arch armv7 YourApp.app.dSYM
          Где число 899296 надо брать в крэшдампе из конца строки (после плюса):
          7 YourApp 0x001d28e0 0xf7000 + 899296

          0x1000 — это смещение сегмента кода, его можно найти командой
          otool -arch armv7 -l YourApp.app/YourApp | grep '^ segname __TEXT' -A1
          Впрочем, похоже, оно всегда одинаковое — 0x1000.

          Приведу пример, почему dwarfdump стоит использовать, даже есть есть символизированный крешлог.

          Вот что мы имеем в обычном крешлоге:
          0 libsystem_c.dylib 0x38314b54 strlen + 28
          1 YourApp 0x0012fba3 length (char_traits.h:257)
          2 libdispatch.dylib 0x36852793 _dispatch_call_block_and_release + 11
          

          Не очень понятно, куда копать.

          А вот, что можно узнать при помощи dwarfdump (сильно сокращено и интерпретировано, в том числе при помощи утилиты c++filt):
          compile_unit: /Users/user/work/myapp/Classes/GCData.mm
          subprogram:
            decl_file: /Users/user/work/myapp/Classes/GCData.mm:
            decl_line: 534 (это начало блока в obj-c)
            ...
            inlined_subroutine: std::string::operator=(char const*)
              call_file: /Users/user/work/myapp/Classes/GCData.mm
              call_line: 550
                 в коде это  std::string name = [[player alias] UTF8String];
              
              inlined_subroutine: std::string::assign(char const*)
              ...
                inlined_subroutine: std::string::length(char const*)
                где оно и упало. Похоже, [player alias] оказался nil или что-то такое.
          

          Теперь понятно, что и где исправлять.
          • +1
            Также, думаю, стоит упомянуть, что существуют бесплатные сервисы, которые можно использовать, чтобы они собирали крэшлоги и передавали их вам. Достаточно подключить их фреймворк, написать в коде пару строчек, и вы автоматически получаете крешлоги от ваших пользователей, причём сами пользователи этого не замечают.
            Даже рискну назвать один из таких сервисов, которым мы давно и успешно пользуемся: crittercism.
            • 0
              Мы используем crashlytics
              • 0
                Очень интересно, можете ли рассказать подробнее?

                Это отражается на быстродействии программы?
                • 0
                  Не отражается. Программа ставит какой-то системный обработчик на случай креша, и на этом всё. Плюс ещё отправка данных на сервер, которая выполняется в отдельном треде.

                  У crittercism отправка данных о крэше выполняется при следующем запуске приложения — у такого решения есть и минусы, и плюсы. В других фреймворках это может быть сделано по-другому (в ранних версиях crittercism пытался отправить данные сразу после крэша).

                  Думаю, что наиболее подробно вы можете почитать на сайте разработчика.
              • +1
                > попросите прислать вам аварийные журналы по почте

                Ну или объясните, что Settings -> General -> Diagnostics & Usage -> Automatically Send — это как раз и есть та галочка, которая отвечает за появление логов у разработчика в iTunes Connect
                • 0
                  очень своевременный пост! только сейчас отправил первое свое приложение в AppStore
                  • +1
                    Пост отличный, материал полезный по сути.

                    Смущают только ошибки, которые приводятся в качестве примера. Ни один из этих крешей не уникален, т.е. он случается практически в 100% случаев и на всех моделях и на всех версиях iOS.
                    Поэтому вопрос, что это, блин, за контора такая, которая выпускает такие приложения? Почти все эти ошибки отлавливаются на этапе отладки, потому что воспроизводятся всегда. Синхронное обращение к сети при запуске вообще песня, ну хотя бы проверить есть ли вообще соединение, так, для начала.

                    Я бы понял если бы речь шла о начинающем инди разработчике-одиночке. Ну или если бы описывались ошибки, связанные с обратной совместимостью, мало ли что упустили из виду. А тут создается впечатление что наваяли код, или вообще аутсорсили его, увидели что запускается и в срочном порядке зафигачили в AppStore.

                    Какая-то антиреклама. Если я теперь увижу приложения от этой компании, я подумаю несколько раз прежде чем качать, ведь теперь я знаю что там не код а хрень какая-то.
                    • +1
                      Подозреваю, что это сделано намеренно для более наглядной иллюстрации.
                      Я например напротив, буду более уважительно относиться к продукции этой компании, коли у них так хорошо могут вылавливать ошибки и они так хорошо знают свою работу
                      • 0
                        Как-то наблюдали вылет своего приложения по таймауту в некоторых ситуациях. Нашлось синхронное обращение к сети. Появилось в очередной версии библиотеки google analytics для iOS. Было оно только в одной версии библиотеки и было исправлено быстро, но именно эта версия попала в релиз. Приятного было мало.
                      • 0
                        Познавательно, жаль, что по этим логам почти невозможно понять причину сбоев в чужом приложении, а зачастую очень интересно.
                        Да, зайдите сюда пожалуйста: tsya.ru
                        • 0
                          Виноват. Исправил 4 ошибки.

                        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.