Профилирование Swift кода: находим узкие места на практике

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Введение: почему профилирование 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                

Блоги

Рекомендаций по книгам