Хабрахабр

[Из песочницы] Продвинутый Debug

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

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

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

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

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

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

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

image

Breakpoint

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

Ставим Breakpoint: 1.

image

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

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

image

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

На экране нашего телефона видимо, что всё успешно получилось: 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 кода.

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

image

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

image

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

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

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

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

Но иногда возникают ситуации, когда установкой одного текста в статус бар дело не ограничивается, и надо отследить выполнение метода в конкретном контроллере, что же делать? Ок, с этим вроде бы понятно. Что это значит? Можно включить 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

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

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. Если не знаете как, вот инструкция по пунктам:

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

Напишите в этом файле ровно следующую команду: 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).

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

image

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

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

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

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

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

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

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

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

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

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

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

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть