Введение: почему профилирование Swift кода — это навык №1 для iOS/macOS разработчика
Современные iOS и macOS приложения становятся всё сложнее, а пользователи — требовательнее. Никто не будет ждать, пока экран загрузится 5 секунд, или терпеть дёрганую анимацию. Оптимизация производительности — не роскошь, а необходимость. Но как понять, какой именно участок кода тормозит всё приложение? Ответ — профилирование Swift кода.
Профилирование — это процесс сбора данных о работе программы: время выполнения функций, использование памяти, частота вызовов. Без него любые попытки оптимизации превращаются в гадание на кофейной гуще. В этой статье мы разберём практические инструменты и техники, которые помогут вам находить узкие места в Swift коде для iOS и macOS. Вы научитесь использовать Instruments, os_signpost и встроенные средства Xcode, а также увидите реальные примеры кода.
1. Инструменты профилирования Swift в Xcode
Xcode предоставляет мощный набор инструментов для профилирования, объединённых в приложение Instruments. Вот основные из них, которые пригодятся каждому разработчику:
- Time Profiler — показывает, сколько процессорного времени тратится на каждую функцию. Идеален для поиска «горячих точек» (hot spots).
- Allocations — отслеживает выделение и освобождение памяти. Помогает находить утечки и избыточное потребление.
- Leaks — автоматически ищет утечки памяти (retain cycles).
- Core Animation — анализирует производительность графики и анимаций.
- os_signpost — API для ручного добавления меток в код, которые затем видны в Instruments.
Запустить профилирование просто: выберите схему (Scheme) в Xcode, нажмите Cmd + I (Product → Profile), и выберите нужный шаблон в Instruments.
1.1. Первый запуск: Time Profiler
Допустим, у вас есть приложение, которое медленно обрабатывает список из 10 000 элементов. Запустите Time Profiler и выполните сценарий. Через несколько секунд вы увидите стек вызовов, отсортированный по времени. Вот как это выглядит в коде:
func processLargeArray() {
let data = (0..<10000).map { $0 }
let result = data.filter { $0 % 2 == 0 }.map { $0 * 2 }
print(result.count)
}
Time Profiler покажет, что filter и map занимают большую часть времени. Но это ожидаемо. А что, если проблема в другом? Например, в частом вызове дорогостоящей функции внутри цикла.
2. Практический пример: оптимизация работы с Core Data
Одна из частых проблем в iOS — медленные запросы к Core Data. Рассмотрим пример: загрузка 5000 объектов с фильтрацией по имени.
func fetchUsers(with name: String) -> [User] {
let request = User.fetchRequest()
request.predicate = NSPredicate(format: "name == %@", name)
request.sortDescriptors = [NSSortDescriptor(key: "name", ascending: true)]
return (try? context.fetch(request)) ?? []
}
Запустив Time Profiler, вы можете увидеть, что fetch занимает 200 мс — много. Но почему? Возможно, индекс по полю name не создан. В Xcode добавьте индекс в модели данных:
- Откройте .xcdatamodeld файл.
- Выберите сущность
User. - В инспекторе данных (Data Model Inspector) добавьте
nameв секцию Indexes.
После этого повторный профиль покажет снижение времени до 20–30 мс. Разница в 10 раз!
2.1. Использование os_signpost для ручной разметки
Иногда стандартные инструменты не дают нужной детализации. Тогда на помощь приходит os_signpost. Он позволяет вручную отмечать начало и конец интересующего вас участка кода, а затем видеть эти метки в Instruments.
import os
let log = OSLog(subsystem: "com.example.app", category: "Database")
func fetchUsersWithSignpost(name: String) -> [User] {
os_signpost(.begin, log: log, name: "Fetch Users", "Fetching users with name: %@", name)
let result = fetchUsers(with: name)
os_signpost(.end, log: log, name: "Fetch Users", "Fetched %d users", result.count)
return result
}
Теперь в Instruments (в шаблоне os_signpost) вы увидите временную шкалу с точными интервалами выполнения. Это незаменимо при профилировании асинхронных операций.
3. Профилирование памяти: находим утечки и избыточное потребление
Утечки памяти (retain cycles) — бич Swift приложений. Instruments с шаблоном Leaks автоматически находит такие проблемы. Но есть и ручной способ: использовать Allocations и анализировать, какие объекты не освобождаются.
Пример классической утечки через замыкание:
class ViewController: UIViewController {
var onButtonTap: (() -> Void)?
override func viewDidLoad() {
super.viewDidLoad()
onButtonTap = { [weak self] in
self?.doSomething()
}
}
func doSomething() {
print("Tapped")
}
}
Если забыть [weak self], контроллер никогда не будет освобождён. Запустите Leaks и нажмите кнопку — утечка будет найдена мгновенно.
3.1. Оптимизация работы с изображениями
Ещё одна распространённая проблема — загрузка изображений без кэширования. Рассмотрим код:
func loadImage(from url: URL) -> UIImage? {
guard let data = try? Data(contentsOf: url) else { return nil }
return UIImage(data: data)
}
Каждый вызов загружает изображение заново. Используйте NSCache или URLCache:
let imageCache = NSCache<NSURL, UIImage>()
func cachedImage(from url: URL) -> UIImage? {
if let cached = imageCache.object(forKey: url as NSURL) {
return cached
}
guard let image = loadImage(from: url) else { return nil }
imageCache.setObject(image, for