Привет! Меня зовут Владислав Даниелян, я iOS-разработчик в AGIMA. Предлагаю немного поговорить о принтах. Это одна из первых и наиболее используемых функций, с которой начинаются первые шаги в разработке у любого новичка:
print("Hello world")
Цель статьи — сэкономить время начинающих разработчиков, уберечь их от бесконечного потока непонятных сообщений в консоли и от нервов, потраченных на поиск «той самой» строки, которая всё объясняет. Мы разберем виды принтов и напишем свой небольшой логгер, который можно внедрить сразу, параллельно чтению.
Логирование — важный инструмент в арсенале разработчика. Оно помогает систематизировать наши сообщения (коих со временем может появиться огромное количество), дает возможности фильтрации и многое другое. В этой статье посмотрим, какие инструменты от Apple у нас в распоряжении. А начнем с Print.
Print — самая базовая функция, выводящая текст в консоль Xcode. Она часто используется для дебаггинга и неплохо справляется с задачей, когда решать приходится простые проблемы. Но многие сталкивались с ситуацией, когда сообщения в Print становятся слишком громоздкими или когда их становится слишком много по всему приложению. В этих случаях консоль превращается в сплошную стену сложночитаемого текста.
Поэтому ниже рассмотрим альтернативы Print, но для начала разберемся с разновидностями самой Print, посмотрим, чем отличаются друг от друга Print, DebugPrint и Dump, поговорим об их достоинствах и недостатках, а также узнаем, что такое Logger и OSLog и напишем их базовую имплементацию для вашего проекта.
Начнем с простого. Так выглядит наша функция:
func print(
_ items: Any...,
separator: String = " ",
terminator: String = "\n"
)
items — содержимое принта: то, что мы хотим напечатать.
separator — разделитель, вставляется между элементами.
terminator — будет вставлен за последним элементом из Items.
Print — это, пожалуй, самый легкий в использовании способ вывести сообщение. Правда, не очень подробное. Используется, как правило, для легкого дебага (с мелкими объектами). В релизном коде он оставаться не должен.
Задерживаться на нем долго не будем. Просто посмотрим на небольшой пример:
print(1, 2, separator: ", ", terminator: ".")
// 1, 2.
Так работает обычный принт. Зеленые цифры — текст, который мы получаем от функции.
DebugPrint
func debugPrint(
_ items: Any...,
separator: String = " ",
terminator: String = "\n"
)
Очень похож на обычный принт, но отличается тем, что предоставляет дополнительную информацию о печатаемых объектах:
print(1...5)
// 1...5
debugPrint(1...5)
// ClosedRange(1...5)
let world = "world"
print("hello", world)
// hello world
debugPrint("hello", world)
// "hello" "world"
DebugPrint целесообразно использовать в соответствии с его названием — для дебага. Он покажет больше полезной информации о том, с каким типом объектов мы имеем дело. Поэтому по большей части он может стать заменой обычной Print в работе. Но рекомендую перед релизом удалить Print и его альтернативы.
Dump
func dump<T>(
_ value: T,
name: String? = nil,
indent: Int = 0,
maxDepth: Int = .max,
maxItems: Int = .max
) -> T
Еще одна функция для распечатки сообщений в консоль, но на этот раз со значительными отличиями от предыдущих. Давайте разбираться!
Для примера будем использовать небольшой объект Movie:
struct Movie {
let name: String
let rating: Double
let actors: [String]
}
let movie = Movie(
name: "Звездные войны",
rating: 5.0,
actors: ["Лиам Нисон", "Натали Портман"]
)
value — объект, который мы хотим распечатать.
dump(movie)
// Output:
▿ StudyProject.Movie
- name: "Звездные войны"
- rating: 5.0
▿ actors: 2 elements
- "Лиам Нисон"
- "Натали Портман"
name — заголовок, с которым объект будет распечатан.
dump(movie, name: "Объект")
// Output:
▿ Объект: StudyProject.Movie
- name: "Звездные войны"
- rating: 5.0
▿ actors: 2 elements
- "Лиам Нисон"
- "Натали Портман"
indent — отступ: чем больше этот параметр, тем правее будет выведено сообщение.
dump(movie, indent: 10)
// Output:
▿ StudyProject.Movie
- name: "Звездные войны"
- rating: 5.0
▿ actors: 2 elements
- "Лиам Нисон"
- "Натали Портман"
maxDepth — глубина, отражает, насколько подробно будет напечатана информация об объекте.
dump(movie, maxDepth: 1)
// Output:
▿ StudyProject.Movie
- name: "Звездные войны"
- rating: 5.0
▹ actors: 2 elements
maxItems — максимальное количество элементов с «полным» описанием.
dump(movie, maxItems: 2)
// Output:
▿ StudyProject.Movie
- name: "Звездные войны"
(2 more children)
При работе с объектами и массивами объектов Dump показывает себя лучше, чем Print и DebugPrint. Мы получаем гораздо более наглядный результат, можем повлиять на то, в каком виде будет представлена информация, избавиться от лишнего «шума» в консоли.
OSLog
Наш самый главный инструмент для ведения логов — OSLog. Логи бывают нескольких уровней с говорящим названием:
default — используется, если не указан другой конкретный уровень;
info — полезная, но не критично важная информация, не отобразится в Console.app (об этом далее);
debug — сообщения для отладки также не отображается в Console.app;
error — для логирования ошибок, в консоли Xcode выделяется желтым цветом;
fault — также для логирования ошибок, но уже критичных, в Xcode выделяется красным.
Напишем простой логгер, которым прямо сейчас можно будет воспользоваться в своем проекте. В дальнейшем можно использовать данную информацию как фундамент и расширять его возможности (сохранять логи в файл, замерять скорость выполнения функции и многое другое).
// Используем публичную функцию для ведения логов, в зависимости от ваших потребностей, реализовать можно разными способами
public func log(_ items: Any...,
type: OSLogType = .default,
file: String = #file,
function: String = #function) {
formLog(items, type: type, file: file, function: function)
}
Для этой функции мы передаем тип, название файла и название функции. Кастомизировать это можно как угодно. Например, передавать номер строки или различные названия для subsystem, чтобы легче фильтровать логи по разным модулям. Обращу внимание, что в целях экономии ресурсов логи мы запускаем в дебаг-среде пользуясь #if DEBUG. Посмотрим, как функция реализована внутри:
private func formLog(_ items: [Any],
type: OSLogType,
file: String,
function: String) {
#if DEBUG
// Форматируем название файла(можно пропустить или сделать иначе)
let lastSlashIndex = (file.lastIndex(of: "/") ?? String.Index(utf16Offset: 0, in: file))
let nextIndex = file.index(after: lastSlashIndex)
let filename = file.suffix(from: nextIndex).replacingOccurrences(of: ".swift", with: "")
// subsystem, можно передать извне, чтобы отличать логи по модулям
let subsystemName = "TestModule"
// создаем лог, в качестве категории используем обработанное название файла
let log = OSLog(subsystem: subsystemName, category: filename)
// формируем сообщение
let items = items.map {"\($0)"}.joined(separator: ", ")
let formattedMessage = [function, items].joined(separator: " | ")
os_log("%{public}s", log: log, type: type, formattedMessage)
#endif
}
Итак, мы добавили логи в приложение, придали им читаемый вид. Что дальше? Конечно, нужно их прочитать. Логи мы можем видеть в консоли Xcode с разным выделением по цвету в зависимости от типа, но наибольшую пользу из них мы сможем извлечь с помощью нативного приложения для MacOS — Console.
Плюсы приложения по сравнению с обычной консолью:
приложение пока открыто,
сохраняет логи (в том числе и после перезапуска приложения),
в нем можно выставить удобные фильтры-шаблоны, чтобы быстро находить необходимые записи.
Запустим приложение на симуляторе. Откроем приложение и увидим следующую картину:
Выбираем наш девайс и нажимаем «Начать потоковую передачу».
Сразу же видим тонну не слишком информативных логов. Чтобы найти необходимые нам, воспользуемся фильтрацией в верхнем правом углу. Вписываем название нашего приложения, жмем Enter, выбираем «Библиотека». Теперь мы увидим собственные логи.
Поиск можно кастомизировать: добавить поиск по Subsystem, чтобы посмотреть на сообщения из определенного модуля, отфильтровать результаты по типу и т. д. Также поиск можно сохранить, а отображаемые поля настроить, например уберем «процесс» и добавим «подсистему».
Сохраняем поиск, настраиваем отображение полей и видим такой результат:
Все наши логи теперь на экране. Можем их просматривать, фильтровать, делиться в текстовом виде. Еще один плюс логирования перед принтами: логи не очищаются между перезагрузками вашего симулятора, поэтому можете сравнивать результаты и отслеживать изменения последовательно, в реальном времени
Logger
Более свежей альтернативой OSLog является Logger, доступный с iOS 14.
Logger можно использовать как альтернативу OSLog. С помощью расширений (Extension) можно создать несколько логгеров, отвечающих за логирование разного функционала.
import OSLog
extension Logger {
private static var subsystem = “com.agima.example”
/// Логи network слоя
static let network = Logger(subsystem: subsystem, category: "network")
/// Логи вашего сервиса
static let yourService = Logger(subsystem: subsystem, category: "your_service")
}
Logger от OSLog отличается в деталях. Это и разные уровни логирования, и возможности настройки логов. Разберем по порядку.
Уровни логирования:
notice;
info;
debug;
trace;
warning;
error;
fault;
critical.
Как мы видим, уровней здесь больше чем в OSLog, и некоторые из них отличаются по смыслу. В консоли Xcode, нажав на иконку правее «глаза» (на скрине ниже), можно отобразить метаданные сообщений. Таким образом наши логи будут выглядеть так:
В приложении Console они читаются идентично примеру из OSLog, поэтому перестраиваться не придется.
Еще одна отличительная особенность — возможность настроить приватность сообщений. Например:
Logger.network.info("Пользователь \(username, privacy: .private) добавил товар")
Из-за параметра Privacy Username будет скрыт, однако при использовании симулятора вы этого не увидите.
Что в итоге использовать
В заключение отмечу, что не стоит злоупотреблять логами в принципе. Чтобы избежать негативного влияния на производительность, лучше включать их в Debug-среде или по тогглу для конкретного пользователя, если возникает такая необходимость. Также стоит избирательно подходить к их добавлению и не логировать без надобности, чтобы избежать лишнего «шума» и облегчить процесс дебаггинга.
Но если необходимость всё-таки есть, то к выбору инструментов нужно подходить с учетом стоящих перед вами задач:
Print и его разновидности — для дебага в сочетании с Breakpoints. Не оставляем его в релизном коде. Вид принта (Dump, Debug и т. д.) выбираем в зависимости от того, что именно дебажим.
OSLog/Logger. Фреймворк для ведения логов выбираем исходя из минимальной версии iOS на проекте. Logger доступен начиная с iOS 14. Стараемся логировать только по необходимости и не забываем им пользоваться только при дебаге.
Вот и всё. Мы рассмотрели виды принта с их достоинствами и недостатками и вспомнили виды логирования. А еще с минимумом усилий сделали процесс отладки работы приложения более легким и понятным.
Эту статью можно считать стартовой точкой. Для дальнейшего изучения оставлю документацию:
Когда ознакомитесь, попробуйте поэкспериментировать самостоятельно. Это обширная тема, и она достойна более глубокого изучения.
Если у вас остались вопросы — задавайте в комментариях. А если вас интересуют новости мобильной разработки — подписывайтесь на телеграм-канал моего коллеги Саши Ворожищева.
FreeNickname
Спасибо, совсем забыл про dump :)
Добавлю, что Apple не рекомендует делать свои функции-обёртки вокруг OSLog / Logger, т.к., во-первых, они оптимизированы для того, чтобы лениво формировать сообщения в фоне (Вы в методе
formLog
создаётеformattedMessage
сами), а во-вторых, позволяют прямо из сообщения в логе перейти в то место в коде, где оно было залогировано. Если вокруг системных функций сторонняя обёртка, кидать будет всегда в эту обёртку.Vdanielyan Автор
Спасибо, хорошее замечание по поводу обертки! С учетом того, что мы используем логи только в дебаг-режиме, это не влияет на производительность критичным образом, однако, вызов функции стоит действительно вынести и воспользоваться советом Apple :)
FreeNickname
Там, кстати, на самом деле вообще всё интересно :)
Для реализации нативного Swift-логирования хаки зашиты прямо в компиляторе. Я уже не помню подробностей того, что я хотел сделать, но, вроде бы, я хотел иметь либо свою обёртку вокруг системного логгера, которая прокидывает ему все параметры вроде точки вызова и т.д. (ведь наверняка это реализовано через неявные параметры, думал я), либо сделать протокол, повторяющий интерфейс системного логгера, "подписать" системный логгер под этот протокол через extension, и таким образом иметь возможно работать везде с протоколом с простой возможностью подменить реализацию во время выполнения. Но оказалось, насколько я помню, что невозможно перебросить параметры в системный логгер, даже если у нас функция объявлена точно так же, потому что для системного логгера выполняется какая-то особая компиляторная магия.
В итоге я удовольствовался typealias. У меня есть свой логгер, который повторяет интерфейс системного, и я повсюду пользуюсь alias-ом. Когда нужно, alias указывает на системный логгер, когда нужно – на кастомный. Но в динамике, понятное дело, уже не переключишь.
Vdanielyan Автор
typealias звучит как хороший выход из ситуации)