В преддверии старта базового курса «iOS-разработчик» подготовили для вас еще один интересный перевод.





Примечание: Эта статья был написана незадолго до WWDC 2020, но вам следует читать ее только после просмотра конференции. Все, о чем здесь пойдет речь, уже доступно — вам не нужно скачивать бета-версию или ждать до осени, чтобы использовать это. Также, я собираюсь обновить статью, если что-нибудь относящееся к ней зарелизят на WWDC

Большинство из вас, вероятно, уже работали или в настоящее время работают над приложением, для которого значительная часть функциональности зависит от связи с сервером посредством HTTP. Когда что-то работает не так, как ожидалось, или вы просто хотите понять область кода, с которой вы еще не знакомы, часто бывает полезно посмотреть HTTP-запросы, идущие между приложением и сервером. Какие запросы были сделаны? Что именно отправляет сервер? Для этого вы, вероятно, используете такие инструменты, как Charles Proxy или Wireshark.

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

В качестве подготовки к WWDC на следующей неделе1, я (пере)смотрел пару выступлений из предыдущих WWDC. Так или иначе, я полностью упустил, что ядро инструментов было переписано, чтобы унифицировать их, и сделать его намного проще для создания пользовательских инструментов для Xcode 10. Кроме того, WWDC 2019 оказался отличным введением в инструменты, чего мне не хватало все эти годы.

Круто, теперь вы можете писать собственные инструменты для измерения того, что инструменты обычно не измеряют. Но что вы можете измерить и насколько это легко? Я бы сказал: «почти все» и «не то чтобы очень сложно, достаточно быстро». Как правило, все, что вам нужно — написать XML-файл, в котором будет указано, как преобразовывать signpost-указатели из вашего кода в данные для отображения в инструментах, а XML-код, необходимый для этого, не особо замысловат. Основным препятствием является то, что «код», который вы пишете, вероятно, будет сильно отличаться от того, к чему вы привыкли, примеров очень мало, документация дает только общий обзор того, как это нужно делать, и хотя Xcode на самом деле довольно строго проверяет XML-файлы, автодополнения нет и мало что может упростить вам поиск ошибок. Но, потратив немного времени, можно найти нужные вам элементы, а если у вас есть пример для адаптации кода, вы сможете добиться результата довольно быстро. Здесь я как раз и собираюсь привести такой пример и постараться перечислить все полезные ссылки.

Но давайте начнем с самого начала: я хочу, чтобы каждый из вас, кто раньше использовал Charles или Wireshark для отладки своего приложения, или разрабатывал приложение, которое выполняет множество HTTP-запросов, смог создать инструмент трассировки HTTP, настроенный под ваше приложение или, по крайней мере, фреймворк. Он будет выглядеть так:



У меня ушел приблизительно один день на то, чтобы создать и отладить этот прототип (после просмотра соответствующих видео WWDC). Если вас не интересует ничего кроме кода, вы можете посмотреть его здесь.

Обзор


Самый простой способ создать пользовательский инструмент — это использовать os_signpost, собственно что мы и собираемся здесь делать. Вы используете его для логирования signpost-указателей .event или .begin и .end. Затем вы настраиваете пользовательский инструмент для анализа этих os_signpost интервалов и извлечения дополнительных значений, которые вы логировали в него, настраиваете, как отобразить их на графике, как сгруппировать их, какие отфильтровать и как представлять структуры списков или деревьев/блок-схем в панели подробностей инструмента.

Мы хотим создать инструмент, который отображает все HTTP-запросы, которые проходят через нашу сетевую библиотеку, как интервалы (начало + конец), чтобы мы могли видеть, сколько времени они занимают, и сопоставлять их с другими событиями, происходящими в нашем приложении. В этой статье я использую Alamofire в качестве сетевой библиотеки инструмента и Wordpress в качестве приложения для профилирования, просто потому что у них открытый исходный код. Но вы легко сможете адаптировать весь код к вашей сетевой библиотеке.

Шаг 0: Ознакомьтесь с Instruments App


  1. Начало работы с инструментами (Сессия 411 из WWDC 2019) — это очень хороший обзор инструментов. Посмотрите хотя бы раздел «Orientation», чтобы ознакомиться с терминологией, такой как инструменты (instruments), треки (tracks), полосы (lanes), трейсы (traces), шаблоны (templates), подробный вид (detail view) и т. д.
  2. Посмотрите Создание пользовательских инструментов (Сессия 410 из WWDC 2018), чтобы получить представление о том, что мы здесь делаем. Если вы слишком нетерпеливы, достаточно посмотреть разделы «Architecture» (для получения дополнительной информации о том, как работают инструменты, и что вы на самом деле настраиваете) и «Intermediate». Не ожидайте, что поймете все тонкости во время просмотра, там показано слишком много всего для одного сеанса, и они не объясняют каждую деталь из-за временных ограничений. Мне приходилось смотреть это несколько раз и искать дополнительную документацию, прежде чем я действительно смог заставить свой инструмент работать так, как мне было нужно. Тем не менее, я попытаюсь заполнить некоторые пробелы ниже.


Шаг 1: Логирование нужных вам данных в signpost-указатели


Мы хотим построить наш инструмент на signpost-указателях, то есть мы будем логировать наши данные через signpost. Alamofire отправляет Notification каждый раз, когда запрос начинается или завершается, поэтому все, что нам нужно, это что-то вроде этого2:

NotificationCenter.default.addObserver(forName: Notification.Name.Task.DidResume, object: nil, queue: nil) { (notification) in
    guard let task = notification.userInfo?[Notification.Key.Task] as? URLSessionTask,
        let request = task.originalRequest,
        let url = request.url else {
            return
    }
    let signpostId = OSSignpostID(log: networking, object: task)
    os_signpost(.begin, log: SignpostLog.networking, name: "Request", signpostID: signpostId, "Request Method %{public}@ to host: %{public}@, path: %@, parameters: %@", request.httpMethod ?? "", url.host ?? "Unknown", url.path, url.query ?? "")
}
NotificationCenter.default.addObserver(forName: Notification.Name.Task.DidComplete, object: nil, queue: nil) { (notification) in
    guard let task = notification.userInfo?[Notification.Key.Task] as? URLSessionTask else { return }
    let signpostId = OSSignpostID(log: networking, object: task)
    let statusCode = (task.response as? HTTPURLResponse)?.statusCode ?? 0
    os_signpost(.end, log: SignpostLog.networking, name: "Request", signpostID: signpostId, "Status: %@, Bytes Received: %llu, error: %d, statusCode: %d", "Completed", task.countOfBytesReceived, task.error == nil ? 0 : 1, statusCode)
}


Когда запрос начинается, мы логируем signpost .begin, когда он завершается, мы добавляем signpost .end. Для сопоставления завершения вызова с соответствующим началом вызова используется signpostId, чтобы убедиться, что мы закрываем правильный интервал, если несколько запросов происходят параллельно. В идеале мы должны хранить signpostId в объекте запроса, чтобы быть уверенными, что мы используем один и тот же для .begin и .end. Однако я не хотел править тип Request в Alamofire, поэтому решил использовать OSSignpostID(log:, object:) и передавать ему объект-идентификатор. Мы используем базовый объект URLSessionTask, поскольку в обоих случаях он будет одинаковым, а это позволяет OSSignpostID(log:, object:) возвращать нам один и тот же идентификатор при его многократном вызове.

Мы логируем данные посредством format string. Вам, вероятно, следует всегда разделяете два аргумента некоторой четко определенной строкой, чтобы упростить анализ на стороне инструмента, а также облегчить парсинг. Обратите внимание, что вам не нужно логировать данные в .end вызове, если вы уже логировали их в .begin. Они будут объединены в один интервал и у вас будет к ним доступ.

Шаг 2: Создайте новый проект пользовательского инструмента в Xcode.


Следуйте по шагам из Создания пользовательских инструментов (Сессия 410 из WWDC 2018) или справки Instruments App — Создание проект пакета инструментов, чтобы создать новый проект пакета инструментов в Xcode. В результате вы получите базовый проект Xcode с .instrpkg файлом. Все детали мы укажем там.

Шаг 3: Сделайте все остальное


В основном вы будете следовать шагам, описанным в справке Instruments App — Создание инструмента на основе данных signpost-указателей. Несмотря на то, что описания всех шагов здесь корректны, им все же не хватает множества деталей, поэтому лучше иметь перед собой пример реального пользовательского инструмента. Вы можете посмотреть на мой здесь. В основном вам понадобятся следующие части:

Схема

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

<os-signpost-interval-schema>
	<id>org-alamofire-networking-schema</id>
	<title>Alamofire Networking Schema</title>

	<subsystem>"org.alamofire"</subsystem>
	<category>"networking"</category>
	<name>"Request"</name>

	<start-pattern>
	    <message>"Request Method " ?http-method " to host: " ?host ", path: " ?url-path ", parameters: " ?query-parameters</message>
	</start-pattern>
	<end-pattern>
	    <message>"Status: " ?completion-status ", Bytes Received: " ?bytes-received ", error: " ?errored ", statusCode: " ?http-status-code</message>
	</end-pattern>

	<column>
	    <mnemonic>column-http-method</mnemonic>
	    <title>HTTP Method</title>
	    <type>string</type>
	    <expression>?http-method</expression>
	</column>
	<!-- и в том же духе -->
</os-signpost-interval-schema>


mnemonic является идентификатором того, что вы собираетесь ссылаться на этот столбец позже. Мне почему-то было немного странно называть столбцы так же, как переменные, поэтому я поставил перед ними префикс column. Но, насколько я знаю, необходимости это делать нет.

Инструмент

Инструмент состоит из базового определения:

<instrument>
    <id>org.alamofire.networking.instrument</id>
    <title>Alamofire</title>
    <category>Behavior</category>
    <purpose>Trace HTTP calls made via Alamofire, grouped by method, host, path, etc.</purpose>
    <icon>Network</icon>
    
    <create-table>
        <id>alamofire-requests</id>
        <schema-ref>org-alamofire-networking-schema</schema-ref>
    </create-table>

    <!-- Остальная часть определения инструмента -->
</instrument>


Все довольно просто. Большинство из этих полей представляют собой текст произвольной формы или относятся к материалам, которые вы определили ранее (schema-ref). Но category и icon могут иметь только небольшой набор значений, определенных здесь и здесь.

График внутри инструмента

График (graph) определяет графическую часть пользовательского интерфейса инструмента, визуальное представление, которое вы видите в области трека. Это выглядит примерно так:

<instrument>
    <!-- Базовое определение инструмента -->
    <graph>
        <title>HTTP Requests</title>
        <lane>
            <title>the Requests</title>
            <table-ref>alamofire-requests</table-ref>
            
            <plot-template>
                <instance-by>column-host</instance-by>
                <label-format>%s</label-format>
                <value-from>column-url-path</value-from>
                <color-from>column-response</color-from>
                <label-from>column-url-path</label-from>
            </plot-template>
        </lane>
    </graph>
    <!-- остальные части инструмента --> 
</instrument>


У вас могут быть разные полосы (lane), и вы можете использовать шаблон графика (plot-template), чтобы реализовать динамическое число графиков в полосе. Мой пример содержит пример простого графика. Я не совсем уверен, почему у graph и lane есть заголовки. В дополнение к этому каждый график в plot-template также получает метку от label-format.

Список, агрегация или что там еще для подробного представления

С одним лишь графиком инструменты выглядели бы несколько неполными. Вы также хотели бы отобразить что-нибудь в подробном представлении (Detail View). Вы можете сделать это с помощью list, aggregation или narrative. Может быть даже больше вариантов, которые я еще не встречал. Агрегация выглядит примерно так:

<instrument>
    <!-- Базовое определение инструмента -->
    <aggregation>
        <title>Summary: Completed Requests</title>
        <table-ref>alamofire-requests</table-ref>
        <slice>
                <column>column-completion-status</column>
                <equals><string>Completed</string></equals>
        </slice>
        <hierarchy>
            <level>
                <column>column-host</column>
            </level>
            <level>
                <column>column-url-path</column>
            </level>
        </hierarchy>
        
        <column><count/></column>
        <column><average>duration</average></column>
        <column><max>duration</max></column>
        <column><sum>column-size</sum></column>
        <column><average>column-size</average></column>
    </aggregation>
    <!-- остальные части инструмента --> 
</instrument>


список выглядит следующим образом:

<instrument>
    <!-- Базовое определение инструмента -->
    <list>
        <title>List: Requests</title>
        <table-ref>alamofire-requests</table-ref>
        <column>start</column>
        <column>duration</column>
        <column>column-host</column>
        <!-- Остальные столбцы ->
    </list>
    <!-- остальные части инструмента -->
</instrument>


Бонусный материал


На этом, по сути, все. Тем не менее, вы сделали не намного больше, чем описано в видео WWDC, а я обещал заполнить некоторые пробелы.

Мой пример инструмента содержит еще пару приятных вещей.

Небольшое CLIPS выражение, чтобы интервал окрашивался в зависимости от того, был ли запрос успешным или нет. Вы можете найти цветовые значения в Instruments Engineering Type Reference.
С помощью шаблона графика вы можете отобразить несколько графиков на одной полосе или, например, иметь по графику на хост, как в моем примере. Тем не менее, вы можете иметь более одного уровня иерархии и позволить пользователю разворачивать или сворачивать детали. Для этого вам нужно будет использовать элемент <engineering-type-track>, чтобы определить свою иерархию, а затем добавить (augmentation) для разных уровней иерархии, чтобы добавить графики и подробные представления. Кроме того, не забудьте активировать дополнения внутри соответствующего инструмента.

Дальнейшие действия


Если вы еще не набрели на нее по предыдущим ссылкам, на самом деле есть полная справка на все, что вы можете поместить в .instrpkg файл. Например, она расскажет вам, какие элементы валидны внутри элемента <instrument> или какие иконки(icon) вы можете выбрать для вашего инструмента. Один важный момент: порядок имеет значение. Так, например, в <instrument>, <title> должен появиться раньше чем <category>, иначе описание будет невалидно.

Пересмотрите создание пользовательских инструментов (Сессия 410 из WWDC 2018) еще раз, чтобы отметить детали, которые вам могут понадобиться. Есть также пример кода из сессии WWDC 2019, где я нашел пример использования <engineering-type-track>.

CLIPS — это язык, используемый для написания пользовательских моделеров (modelers — здесь мы это не рассматриваем), но он также может использоваться для коротких выражений во время объявления столбцов. Документация о языке гораздо обширнее, чем вам нужно. Главное, что вам, вероятно, нужно знать, чтобы написать простое выражение: CLIPS использует префиксную нотацию, поэтому вместо ?a + ?b вам придется писать (+ ?a ?b).

Другие статьи о пользовательских инструментах


Игорь о создании пользовательских пакетов инструментов в Xcode

Отладка


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

Что я еще не выяснил


  • Как использовать значения, которые Instruments предоставляет вам по умолчанию и отображает в пользовательском интерфейсе (например, продолжительность) в выражениях для определений столбцов (например, чтобы создать столбец скорости передачи данных путем деления полученных байтов на продолжительность).
  • Как отобразить что-либо в области дополнительной детализации. Такое ощущение, что она только для стека вызовов. Я хотел бы отобразить, например, тело JSON выбранного запроса, но не нашел ни одного примера, который бы прояснил это.


На что способен этот инструмент


Работа еще в процессе

Загрузите его и посмотрите сами.

Сноски


  1. Ладно, на самом деле были и другие причины.
  2. Полный код для логирования в моем примере находится в файле Logger.swift. Он предполагается для Alamofire 4.8, потому что это то, что использует текущая версия приложения Wordpress для iOS, хотя на момент написания Alamofire 5 уже был зарелижен. Из-за уведомлений этот код логирования легко добавить без изменения самого Alamofire, однако, если у вас есть настраиваемая сетевая библиотека, может быть проще добавить запись в саму библиотеку, чтобы получить доступ к более подробной информации.



Быстрый старт в iOS-разработку (бесплатный вебинар)



Читать ещё