Облегчаем поддержку iOS приложения. Часть 1 — не отрываясь от Xcode

Добрый день. Я хотел бы рассказать о том, как можно облегчить поддержку iOS приложений.

  1. Облегчаем поддержку iOS приложения. Часть 1 — не отрываясь от Xcode
  2. Облегчаем поддержку iOS приложения. Часть 2 — локация и сеть
  3. Облегчаем поддержку iOS приложения. Часть 3 — падение и логи


Всем, кто создавал iOS приложение, и оно доходило хотя бы до открытого β-тестирования, скорее всего, знакома фраза: “Я тут поигрался с приложением и вот что получилось...”. И вот после этой фразы вы могли провести несколько часов, пытаясь понять, как же «это» получилось.

Если вам знакома эта ситуация, или хочется узнать о том, как спасти себя от такого в будущем — прошу под кат.


И так, мы создали приложение для себя или для заказчика и идет процесс тестирования. Тестировщики, как и заказчик, могут быть за 1000км от вас. Так что, давайте рассмотрим несколько ситуаций, при которых вам сообщают о странном баге, и вам либо трудно, либо вообще никак не воспроизвести его.

Хит парада — трудно воспроизвести или трудно дебажить.


То есть, либо до проблемы трудно добираться (нужно далеко забраться в приложение с очень особыми условиями, да хоть погода за окном должна быть именно -40, а локация ваша должна быть на экваторе), либо надо много раз повторять одно и то же действие. В последнем случае, то, что вы на каждой итерации попадаете в breakpoint, как-то раздражает, но именно на этой строке кода должно что-то произойти, нам очень надо остановиться именно на ней, когда нам повезет воспроизвести баг.

Все, кто разрабатывает приложения для iOS/MAC, обязательно ставили брейкпоинты и пытались понять, что же происходит перед тем, как приложение падает/ведет себя некорректно. К сожалению, зачастую разработчики используют малую часть функционала, который им доступен. Перерыл Хабр и нашел всего одну статью о полнофунциональном использовании breakpoint’ов. Про отладку приложения с помощью lldb вообще глухо (ну или мои поисковые способности не позволили мне найти нужную статью).

Брейкпоинты
Отличная статья, но я не увидел там описания Symbolic breakpoint. Так что, рекомендую ознакомится со статьей, а я расскажу вам о Symbolic breakpoint'ах.
И так, вы смогли получить тот самый запуск, который воспроизводит некорректное поведение. И, допустим, оно происходит, когда UIViewController только появился, но вот незадача, у вас нету имплементированного метода -[MyViewController viewDidAppear:] и нету ни делегата у UINavigationViewController, ни чего-то похожего в вашем коде, что помогло бы поставить breakpoint в нужный момент. Именно в этой ситуации вам и пригодятся Symbolic breakpoint.

Вам лишь надо вбить символ -[UIViewController viewDidAppear:]

и вы остановитесь в нужный момент.
Еще одно очень полезное применение Symbolic breakpoint — вам достался проект от другого разработчика и есть описание проблемы от тестировщика. И вы понимаете, как попасть туда в приложении, но как сопоставить это с кодом? База кода может быть колоссальной, и анимация намекает, что происходит -[UINavigationController pushViewController:animated:], но вот где это происходит — не ясно. Добавляем Symbolic breakpoint: -[UINavigationController pushViewController:animated:] и мы остановимся на всех вызовах этого метода.
Однако, при использовании Symbolic breakpoint для системных функций, у вас нету ни self, ни _cmd, ничего, что обычно доступно. Так что вам придется подбираться к конроллеру снаружи, и поможет вам lldb.

LLDB
Во время запуcка приложения нажмите на паузу — видите (lldb)? Это консоль дебагера. Есть много полезных команд, которые он умеет выполнять, и самая первая — это help. Теперь можете увидеть остальные полезные команды.
Самая популярная — это po(print object). Она распечатывает -[NSObject debugDescription], в отличие от NSLog, который выводит -[NSObject description]. Стоит иметь это ввиду, когда вы переопределяете description для более удобного логирования в приложении, не забудьте еще и debugDescription.
Продолжим с того места, где остановились: у вас есть консоль и вы в нужном месте приложения. Но, как минимум, надо получить полную иерархию UIView и неплохо бы все ivar'ы MyViewController?
Чтобы узнать указатель на UIWindow нашего приложения, нам надо набрать
po [[[UIApplication sharedApplication] delegate] window]

Мы получили указатель на UIWindow, давайте теперь получим указатель на rootViewController
po [(UIWindow *)<pointer> rootViewController]
Теперь у нас есть указатель на главный контроллер. Аналогичными манипуляциями мы можем дойти до нужного нам контроллера, идя по child'ам или же по property'ям.
И так, у нас есть указатель на наш MyViewController. Допустим, нам нужен его целочисленный _ivar, property для него никто не создавал (незачем нам лишние getter и setter).
Набираем
po ((MyViewController *)<pointer>)->_ivar
Теперь у нас есть все, что нужно.
Вывод информации об объектах — только часть функционала. Вы хотите, чтобы на девайсе анимация стала медленнее, как в симуляторе по нажатию Toggle Slow Animations? Вот вам вариант, как это можно сделать, не перезапуская приложение
expr -- ((CALayer *)[[[[UIApplication sharedApplication] delegate] window] layer]).speed = 0.2

Готово — на время этого запуска у вас медленные анимации и вы можете так делать именно в нужный момент времени и именно в этот запуск. А как надоест, верните значение 1.
Может, эти примеры наивны, но они дают понять базовый функционал. И да, помните, я описал невероятный расклад — -40 && экватор. Теперь вам никто не мешает подменить значения на лету, перед выполнением критичного куска кода, и все будет выглядеть, как будто вы на экваторе и за окном у вас -40.

chisel
Есть один большой минус в предыдущем описании — очень долго и дорого идти до нужного контроллера. Но есть проект, который вам поможет — chisel. Этот товарищ многое умеет, список команд можно найти тут. Если у вас непонятно как лежит вью, потому что она прозрачная, то можно подсветить границы (к примеру, командой mask). Можно, зная текст на кнопке, найти ее (команда pa11y). А если заранее подготовится и сделать так, что текст accessibilityLabel соответствует тексту элемента, то искать можно что угодно. После установки chisel, список его команд добавится в полный список того, что умеет lldb.
Для меня хит парада — это pviews и pvc. Эти команды выведут иерархию UIView и UIViewController. Если вдруг нужно понять, что за команда и как она работает — набираем help <имя команды>.
В общем, рекомендую ознакомиться с этим инструментом, или хотя бы поставить, потому что, когда прижмет и у вас все-таки воспроизведется проблема — остается только рвать волосы на голове, если нету такого помощника под рукой.
Прелестные функции lldb и chisel не заканчиваются вышеперечисленными. В них можно углубляться без конца. Но, на большинстве некрупных проектов, этого будет вполне достаточно.

Attach to Process
Предположим, что хороший задел для дебагинга у нас есть, мы поставили chisel, и теперь можем в нужный момент в lldb набрать help и, благодаря вшитым и доп. командам, понять, что же у нас не так, вот именно тут и вот именно на этом запуске. Все это мы можем сделать, пока наше приложение запущено из Xcode на девайсе или симуляторе. А что же делать, если мы словили баг просто в процессе использования приложения на девайсе? Можно успеть подойти к Xcode и попросить его присоединиться к приложению. Для этого вам нужно Xcode→Debug→Attach to Process. Можно по имени приложения, но у меня Xcode сам угадывает target.

Заключение
Сегодня мы рассмотрели случай, когда у вас есть приложение, идет β-тестирование, есть невероятное описание или же непонятный результат, и никаких идей, как же так получилось. Надеюсь, теперь у вас есть направление, чтобы начать расследование.

Similar posts

AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 7

    +1
    Опечатку CALAyer на CALayer исправляем и пробуем:
    expr -- ((CALayer *)[(UIWindow *)[(MyAppDelegate *)[UIApplication sharedApplication].delegate window] layer]).speed = 0.2

    получаем:
    error: property 'delegate' not found on object of type 'id'
    error: 1 errors parsing expression

    Причина? Перемудрили с кастами. Операция приведения типа имеет более высокий приоретет перед операцией точка. Поэтому вы результат [UIApplication sharedApplication] приводите к типу MyAppDelegate*. Вы же рассчитывали на такой приоретет: (MyAppDelegate *)([UIApplication sharedApplication].delegate). Да, Objective-C не хватает синтаксиса приведения типов из C++.
    Да и не нужен этот каст, вообще. И каст к UIWindow* тоже не нужен. Правильная команда, без ненужных кастов, выглядит так:
    expr -- ((CALayer *)[[[[UIApplication sharedApplication] delegate] window] layer]).speed = 0.2
      0
      Ошибку поправил. Больше спасибо, за комментарий.
      Я уже по привычке набираю приведение всех типов, где возвращается id, забыл что window часть UIApplicationDelegate.
        0
        Аналогично. Запускаем:
        po [(MyAppDelegate *)[UIApplication sharedApplication].delegate window]

        Получаем:
        error: no known method '+sharedApplication'; cast the message send to the method's return type
        error: 1 errors parsing expression


        Исправляем:
        po [[[UIApplication sharedApplication] delegate] window]

        Получаем ожидаемый результат:
        <UIWindow: 0x15d6f2f0; frame = (0 0; 320 480); gestureRecognizers = <NSArray: 0x15d6d8a0>; layer = <UIWindowLayer: 0x15d6d720>>


        Вы бы проверили каждую команду на работоспособность, прежде чем людям советовать.
          0
          Ну или же вот так
          po [((UIApplication *)[UIApplication sharedApplication]).delegate window]

          Я предпочитаю в коде использовать NSObject.property, нежели [NSObject getter]. Использование их в lldb приводит к излишним скобкам, но привычка.
            0
            Как только излечитесь от этой привычки, автоматически излечитесь от глюков с кастами. Я уже излечился. Как вы правильно заметили, не нужны будут лишние скобки в кастах. Да и каст не туда вписать будет значительно труднее.
      0
      Иерархия всех вьюх без аддонов:
      po [[UIWindow keyWindow] recursiveDescription]
      
        0
        Да, так и есть. Я использовал самый популярный набор задач. Но аналога для pvc нет. А если говорить о UIView, то нельзя вывести superview рекурсивно. Но с аддоном можно — pviews --up <_pointer_>. То есть ли Вы остановились где на breakpoint и хотите посмотреть, не всю иерархию UIView, а только относительно конкретной UIView(причем вврех), используйте приведенную команду.

      Only users with full accounts can post comments. Log in, please.