Продвинутый Debug

в 15:42, , рубрики: debug, expression, LLDB, swift, xcode, Тестирование IT-систем

Debug Area — полезная функция в работе iOS разработчика в Xcode. Как только мы начинаем осваивать разработку под iOS, и пытаемся отойти от привычного и любимого print метода, и найти более быстрые и удобные методы понимания состояния системы в определенный период мы начинаем изучать область дебага (Debug Area).

Скорее всего, в Debug панель ваш взгляд упадёт до того, как вы будете понимать, что именно там происходит. При первом падении приложения нижнее меню открывается автоматически, оно изначально может послужить помощью для понимания проблемы (Вспомним старую добрую “Fatal error: Index out of range”), в основном в самом начале вы не будете понимать, что от нас хочет Xcode и приметесь гуглить ошибки, но по ходу роста всё больше и больше информации станет понятной.

С самого начала программист старается оптимизировать свою работу. Для этого мы стремимся понять в какой момент наша программа перешла в некорректное состояние. И тут в зависимости от точки в которой находится эволюция программиста, методы могут разниться. Сначала как правильно Debug осуществляется методом “print()”, потом идёт расстановка Breakpoints и вызов методов “po”, далее ознакомление с Debug Variable Input (области рядом с консолью в Xcode), а далее приходит понимание и способов компиляции кода в процессе остановки на Breakpoint методов — “expression” (По крайней мере, такая была эволюция у меня).

Давайте попробуем разные способы которые нам помогут понять и изменить состояние нашего приложения. Самые простые вроде “print()”, и “po” рассматривать не будем, я думаю, вы и так понимаете их суть и умеете применять.

Создадим простое приложение с одним экраном в котором будем всего один тип ячеек (TableViewcell) c двумя элементами внутри: UIImageView и UILabel. В ячейках будем писать её порядковый номер, а в картинку ставить либо image1, либо image2.

Метод tableViewCellForRowAtIndexPath будет создавать для нас ячейки, проставлять данные и возвращать:

image

Данный метод будет генерировать такую таблицу:

image

Breakpoint

Давайте остановим нашу программу и допишем какой-нибудь текст в наш Label.

1. Ставим Breakpoint:

image

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

3. Пишем в консоли команду изменить текст ячейки:

image

4. Убираем наш Breakpoint и нажимаем кнопку «продолжить выполнения программы».

5. На экране нашего телефона видимо, что всё успешно получилось:

image

expression выполняет выражение и возвращает значение на текущем потоке.

Edited Breakpoint

Но, что если нам понадобиться изменить текст в большом количестве ячеек? Или мы уже в процессе выполнения программы поняли, что нам надо поменять?

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

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

  1. Создаем breakpoint.
  2. Левой кнопкой мыши по стрелочке breakpoint’a.
  3. Нажимаем Edit Breakpoint.
  4. Condition — условия при котором Breakpoint сработает, сейчас он нам не нужен.
  5. Ignore — сколько раз пропустить Breakpoint прежде чем он сработает (тоже не то).
  6. А вот Action — то, что надо, выбираем тип действий Debugger Command.
  7. Пишем выражение которое нам нужен выполнить:
  8. expression cell.desriptionTextOutlet.text = "(indexPath.item) mission complite”.
  9. Ставим галочку — Продолжить выполнение после успешного выполнения команды.

image

9. Пробуем.

image

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

Breakpoint function

Всегда бывают моменты когда в нашем приложении происходит что-то, что мы не можем объяснить, текст не меняется или меняется больше чем необходимо, казалось бы Breakpoint в таком случае ставить некуда. Но это не совсем так, если вы знаете Obj-C, и знаете какой метод выполняет компилятор который вы хотите отследить вы можете поставить на него Breakpoint и в следующий раз, когда метод вызовется, приложение остановиться в процессе выполнения Assembler кода.

1. В Breakpoint навигаторе выбираем Symbolic Breakpoint.

image

2. Мы хотим отследить метод установки текста в ячейке, пишем -[UILabel setText:].

image

3. Нулевого аргумента не существует, и счет начинается с первого. Первый пойманный метод не тот, что нам нужен (он устанавливаем текущее время в статус бар), а второй как раз наш:

4. Под “$arg1” храниться описание объекта.

5. Под “$arg2” храниться selector функции.

6. Под “$arg3” храниться текст получаемый методом.

Ок, с этим вроде бы понятно. Но иногда возникают ситуации, когда установкой одного текста в статус бар дело не ограничивается, и надо отследить выполнение метода в конкретном контроллере, что же делать? Можно включить Breakpoint подобный тому, что мы установили ранее, но установив его позицию в коде. Что это значит? Мы точно знаем, что наш view появится когда мы будем устанавливать текст в ячейку, значит самое то поставить его во viewDidLoad или после создания ячейки.

Для создания breakpoint мы устанавливаем его на линии, и в action прописываем следующий код:

breakpoint set --one-shot true --name "-[UILabel setText:]”

breakpoint set —one-shot true — создаем breakpoint
—name — имя символьного breakpoint
“-[UILabel setText:]” вызываемый метод

Вот что получилось:

image

Skip Lines

А что если мы заподозрили, что какая-то строка кода портит нам всю программу? В процессе выполнения кода можно избежать выполнения определенной строки кода так:

  1. Ставим breakpoint на строку, которую мы не хотели бы выполнять.
  2. Когда выполнение остановиться, перетаскиваем его в строку, с которой хотим продолжить выполнение программы (забавно, но это не всегда работает, ниже вариант без перетаскивания).

Так же есть другой вариант, который позволит оптимизировать пропускание строк, — это прописывание соответствующей команды в “edit breakpoint”. Команда является рискованной, так как суть таких скачков — это избавить нас от ребилда, но если вы пропустите инициализацию объекта и попытаетесь к нему обратиться программа упадёт.

image

Остановим нашу программу на инициализации картинки, и не будем вообще присваивать картинку ячейке, для этого нам надо пропустить пять строк кода и вернуть ячейку без картинки, для этого на текущем потоке мы пропускаем выполнение следующих пяти строк кода, и продолжаем выполнение программы:
<
img src=«hsto.org/webt/ud/1q/vi/ud1qviab0b7yahv-s8yydt5ouna.png» alt=«image»/>

Звучит довольно неплохо, но картинку всё же присвоить хочется, давайте добавим метод присвоения в breakpoint:

image

Удачная комбинация, теперь у нас в каждой ячейке только один тип картинки.

Watchpoint

Еще одна удобная функция в дебагере — это отслеживание значений в программе, watchpoints. Watchpoint чем то похожа на KVO, мы ставим breakpoint на изменение состояния объекта, и каждый раз, когда он меняет своё состояние, процесс выполнения программы останавливается, и мы можем посмотреть значение и места, откуда и кем было изменено значение. Например, я поставил watchpoint на ячейку, что бы узнать, что происходит в момент листания таблицы и иницилизации новой ячейки. Список команд получился очень большой, поэтому его я приводить не буду просто упомяну некоторые: выполнения layout view находящихся внутри ячейки и простановка constraint, анимация, простановка состояний для ячейки и многое-многое другое.

Для простановки watchpoint на значение необходимо остановить выполнение программы breakpoint в области видимости свойств, который вы хотите отслеживать, выбрать свойство в “debug variable” панели и выбрать watch “<параметр>”.

image

Для того, что бы снять watchpoint с переменной надо заглянуть в breakpoint navigator, там вместе с остальными breakpoint будет находиться и наш watchpoint.

Breakpoint UI Change

Иногда нам надо узнать больше об объекте, который мы пытаемся отдебажить. Самый простой вариант — это использовать “po”, для вывода информации об объекте, и там же посмотреть на расположение объекта в памяти. Но бывает, что мы не имеем прямой ссылки на объект, он не представлен в API view, на которой лежит или возможно скрыт библиотекой. Один из вариантов использовать View Hierarchy, но это не всегда удобно да и понять, что вы нашли нужный view не всегда сложно. Можно попробовать использовать команду:

expression self.view.recursiveDescription()

Она есть в Obj-C но в Swift её убрали из за особенностей работы языка выполнить мы её не можем, но так как на Debuger работает с Obj-C, в теории ему можно скормить эту команду, и он поймёт, что вы от него хотите. Для выполнения кода Obj-C в консоли необходимо ввести команду:

expression -l objc -O - - [`self.view` recursiveDescription] 

Что вы тут видите? Я вижу довольно не удобную конструкцию, к котором можно было бы привыкнуть со временем, но лучше мы не будем этого делать, а используем typealias для упрощения команды:

command alias poc expression -l objc -O —

Теперь наша команда сокращается и упрощается, но продолжает делать работу:

poc [`self.view` recursiveDescription]

Будет ли она работать после закрытия Xcode или в другом проекте? Увы, нет. Но это можно исправить! Создав файл .lldbinit и вписав туда наш alias. Если не знаете как, вот инструкция по пунктам:

1. Создаете файл .lldbinit (в качестве прототипа можете взять .gitignore, он относится к тому же типу текстовых невидимых файлов).

2. Напишите в этом файле ровно следующую команду:

 command alias poc expression -l objc -O - -	

3. Файл поместите в папку по адесу “MacintoshHD/Users/<Тут имя вашего юзера>”.

И так мы получили описание всех view, представленных на экране. Давайте попробуем посмотреть, что мы сможем сделать с адресом объектов в памяти. Для Swift тут имеется метод с недостатоком, надо всё время приводить тип объекта в памяти к определенному значению:

po unsafeBitCast(0x105508410, to: UIImageView.self)

Теперь мы видимо положение нашей картинки в ячейке, давайте её подвинем что бы она была по центу ячейки и имела отступ с боку 20 px.

image

Бывает не сразу заметно изменение, а необходимо снять с debug приложение что бы заметить изменение.

Но если мы хотим видеть нечто подобное в каждой ячейки, надо ускорить выполнение команд, можно написать на Python несколько скриптов которые будут работать на нас (как добавлять скрипты можно посмотреть здесь www.raywenderlich.com/612-custom-lldb-commands-in-practice), и если вы умеете обращаться с Python и хотите написать на нём для lldb то вам пригодиться.

Я же решил написать расширение для класса UIView, который просто будет двигать view в нужном направлении, мне показалось так будет меньше проблем с подключением новых скриптов к LLDB и не сложно для любого iOS программиста (иначе надо осваивать Python для LLDB).

Я не стал искать место объекта в памяти и приводить его в нужный класс, что бы потом взять frame, это так же займет слишком много времени. Вопрос решился написанием функции в расширении UIView:

image

К сожалению, она плохо работает с ячейками, скорее всего из за того, что в момент исполнения команды flush не все позиции ячейки просчитаны и она не появилась на экране (мы пока не вернули tableViewCell). С остальными статическими элементами оно работает отлично.

Зная положение view в иерархии, мы можем получить к нему доступ и менять его положение.

А теперь обратная ситуация, когда мы можем получить доступ к ViewHierarchy и хотим оттуда получить данные о view. В Xcode есть возможность просматривать иерархию view в процессе выполнения программы, так же там можно просмотреть цвета, расположение, типы и привязки к другим объектам в том числе. Давайте попробуем получить доступ к constraints нашего UIImageView.

Для получения данных о constraint:

1. Нажмите на Debug View Hierarchy.
2. Включите Clipped Content на панели внизу появившегося экрана.
3. Включите Constraints на той же панели.
4. Выберите Contraint.
5. В меню нажмите Edit -> Copy (Command + C).
6. Копируется привязка вот такого вида: ((NSLayoutConstraint *)0x2838a39d0).
7. И теперь, так же как мы меняем её через код так же можно поменять и в lldb:
expression [((NSLayoutConstraint *)0x2838a39d0) setConstant: 60]
8. После нажатия кнопки продолжить, элемент обновит своё положение на экране.

Таким же образом можно менять цвета, текст и многое другое:

expression [(UILabel *)0x102d0a260] setTextColor: UIColor.whiteColor]

Demo проект получился слишком простым (60 строк кода во ViewController), большую часть кода, который я написал, представлена в статье, так что сложности в воспроизведении тестового проекта не возникнет.

P.S.: Если есть вопросы или замечания пишите. Посматривайте WWDC и Дебажте как Pro.

Советую так же ознакомиться с материалами:

Вдохновлялся Advanced Debugger WWDC 18 Session
Команды Debugger
Добавление скриптов Python в LLDB Xcode

Автор: Анатолий Морозов

Источник


* - обязательные к заполнению поля