[Из песочницы] Продвинутый 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 будет создавать для нас ячейки, проставлять данные и возвращать:
Данный метод будет генерировать такую таблицу:
Breakpoint
Давайте остановим нашу программу и допишем какой-нибудь текст в наш Label.
Ставим Breakpoint: 1.
Программа остановила выполнение на 55 строке, сразу после присваивания текста. 2. Так как мы находимся на строке, расположенной в зоне видимости ячейки, мы можем взаимодействовать с нашей ячейкой.
Пишем в консоли команду изменить текст ячейки: 3.
Убираем наш Breakpoint и нажимаем кнопку «продолжить выполнения программы». 4.
На экране нашего телефона видимо, что всё успешно получилось: 5.
expression выполняет выражение и возвращает значение на текущем потоке.
Edited Breakpoint
Но, что если нам понадобиться изменить текст в большом количестве ячеек? Или мы уже в процессе выполнения программы поняли, что нам надо поменять?
Мы можем оптимизировать выполнение этой операции и немного ускорить работу, сделать изменение текста в ячейки тогда, когда он доходит до Breakpoint и продолжить выполнять программу, это сократит много времени и позволит не печатать одно и тоже для каждой ячейки.
Для этого нам понадобиться немного модифицировать наш Breakpoint, прописать туда дополнительно код, который будет в зоне видимости нашей ячейки менять её текст и продолжать работу программы.
- Создаем breakpoint.
- Левой кнопкой мыши по стрелочке breakpoint’a.
- Нажимаем Edit Breakpoint.
- Condition — условия при котором Breakpoint сработает, сейчас он нам не нужен.
- Ignore — сколько раз пропустить Breakpoint прежде чем он сработает (тоже не то).
- А вот Action — то, что надо, выбираем тип действий Debugger Command.
- Пишем выражение которое нам нужен выполнить:
- expression cell.desriptionTextOutlet.text = "\(indexPath.item) mission complite”.
- Ставим галочку — Продолжить выполнение после успешного выполнения команды.
Пробуем. 9.
Это успех, получилось изменить текст для каждой ячейки во время формирования таблицы, и нам не пришлось жертвовать временем и прописывать операции для каждой.
Breakpoint function
Всегда бывают моменты когда в нашем приложении происходит что-то, что мы не можем объяснить, текст не меняется или меняется больше чем необходимо, казалось бы Breakpoint в таком случае ставить некуда. Но это не совсем так, если вы знаете Obj-C, и знаете какой метод выполняет компилятор который вы хотите отследить вы можете поставить на него Breakpoint и в следующий раз, когда метод вызовется, приложение остановиться в процессе выполнения Assembler кода.
В Breakpoint навигаторе выбираем Symbolic Breakpoint. 1.
Мы хотим отследить метод установки текста в ячейке, пишем -[UILabel setText:]. 2.
Нулевого аргумента не существует, и счет начинается с первого. 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:]”
вызываемый метод
Вот что получилось:
Skip Lines
В процессе выполнения кода можно избежать выполнения определенной строки кода так: А что если мы заподозрили, что какая-то строка кода портит нам всю программу?
- Ставим breakpoint на строку, которую мы не хотели бы выполнять.
- Когда выполнение остановиться, перетаскиваем его в строку, с которой хотим продолжить выполнение программы (забавно, но это не всегда работает, ниже вариант без перетаскивания).
Так же есть другой вариант, который позволит оптимизировать пропускание строк, — это прописывание соответствующей команды в “edit breakpoint”. Команда является рискованной, так как суть таких скачков — это избавить нас от ребилда, но если вы пропустите инициализацию объекта и попытаетесь к нему обратиться программа упадёт.
Остановим нашу программу на инициализации картинки, и не будем вообще присваивать картинку ячейке, для этого нам надо пропустить пять строк кода и вернуть ячейку без картинки, для этого на текущем потоке мы пропускаем выполнение следующих пяти строк кода, и продолжаем выполнение программы:
Звучит довольно неплохо, но картинку всё же присвоить хочется, давайте добавим метод присвоения в breakpoint:
Удачная комбинация, теперь у нас в каждой ячейке только один тип картинки.
Watchpoint
Еще одна удобная функция в дебагере — это отслеживание значений в программе, watchpoints. Watchpoint чем то похожа на KVO, мы ставим breakpoint на изменение состояния объекта, и каждый раз, когда он меняет своё состояние, процесс выполнения программы останавливается, и мы можем посмотреть значение и места, откуда и кем было изменено значение. Например, я поставил watchpoint на ячейку, что бы узнать, что происходит в момент листания таблицы и иницилизации новой ячейки. Список команд получился очень большой, поэтому его я приводить не буду просто упомяну некоторые: выполнения layout view находящихся внутри ячейки и простановка constraint, анимация, простановка состояний для ячейки и многое-многое другое.
Для простановки watchpoint на значение необходимо остановить выполнение программы breakpoint в области видимости свойств, который вы хотите отслеживать, выбрать свойство в “debug variable” панели и выбрать watch “<параметр>”.
Для того, что бы снять 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.
Бывает не сразу заметно изменение, а необходимо снять с debug приложение что бы заметить изменение.
Но если мы хотим видеть нечто подобное в каждой ячейки, надо ускорить выполнение команд, можно написать на Python несколько скриптов которые будут работать на нас (как добавлять скрипты можно посмотреть здесь www.raywenderlich.com/612-custom-lldb-commands-in-practice), и если вы умеете обращаться с Python и хотите написать на нём для lldb то вам пригодиться.
Я же решил написать расширение для класса UIView, который просто будет двигать view в нужном направлении, мне показалось так будет меньше проблем с подключением новых скриптов к LLDB и не сложно для любого iOS программиста (иначе надо осваивать Python для LLDB).
Вопрос решился написанием функции в расширении UIView: Я не стал искать место объекта в памяти и приводить его в нужный класс, что бы потом взять frame, это так же займет слишком много времени.
С остальными статическими элементами оно работает отлично. К сожалению, она плохо работает с ячейками, скорее всего из за того, что в момент исполнения команды 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