Как ускорить Swift: профилирование и оптимизация кода

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

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

Начать курс

Почему Swift тормозит? Разбираемся с производительностью

Вы когда-нибудь запускали своё Swift-приложение и чувствовали, что оно работает… ну, как улитка в час пик? Экран зависает, анимации дёргаются, а пользователи уже готовы поставить единицу в App Store. Знакомая боль? Добро пожаловать в клуб!

Swift — язык быстрый. Apple это обещала, и в 90% случаев это правда. Но есть нюанс: даже самый мощный язык можно «задушить» плохой архитектурой, утечками памяти или неэффективными алгоритмами. В этой статье мы не будем читать скучные лекции. Вместо этого я покажу вам реальные инструменты и приёмы, которые превратят ваше приложение из черепахи в гепарда.

Готовы? Поехали!

Инструменты профилирования: ваш рентген для кода

Прежде чем что-то оптимизировать, нужно понять, что именно тормозит. Гадать на кофейной гуще — плохая стратегия. Тут на помощь приходит профилирование Swift-кода.

Xcode Instruments — ваш главный друг

В Xcode есть встроенный набор инструментов — Instruments. Это как МРТ для вашего приложения: показывает каждый чих, каждый вызов функции и каждый байт памяти. Запускается просто: Product > Profile (или Cmd + I).

Вот три самых полезных шаблона:

  • Time Profiler — показывает, какие функции съедают больше всего процессорного времени.
  • Allocations — отслеживает выделение и освобождение памяти.
  • Leaks — ловит утечки памяти (да-да, в Swift они тоже бывают!).

Пример: вы запускаете Time Profiler, скроллите таблицу — и видите, что 70% времени уходит на tableView(_:cellForRowAt:). Это сигнал: пора оптимизировать ячейки.

Signposts и os_log: профилирование без Instruments

Instruments — штука мощная, но не всегда под рукой. Иногда нужно быстро замерить производительность прямо в коде. Тут помогут Signposts (iOS 12+):

import os.signpost

let log = OSLog(subsystem: "com.yourapp.networking", category: .pointsOfInterest)
let signpostID = OSSignpostID(log: log)

os_signpost(.begin, log: log, name: "fetchData", signpostID: signpostID)
// ваш код загрузки данных
os_signpost(.end, log: log, name: "fetchData", signpostID: signpostID)

Потом открываете Instruments, выбираете os_signpost — и видите временные метки. Красота, правда?

Оптимизация Swift-кода: от простого к сложному

Теперь, когда мы знаем, где узкие места, давайте их чинить. Я разбил советы на три уровня: для новичков, для опытных и для профи.

1. Базовый уровень: что может сделать каждый

Начнём с очевидного, но часто забываемого.

Используйте value types (struct) вместо reference types (class)

Это классика Swift. Struct хранится в стеке, class — в куче. Стек — быстрый, куча — медленная (нужно выделять память, считать ссылки). Если ваш объект небольшой и не требует наследования — смело делайте его структурой.

// Плохо (class)
class User {
    var name: String
    var age: Int
    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
}

// Хорошо (struct)
struct User {
    var name: String
    var age: Int
}

Ленивая инициализация (lazy)

Зачем создавать объект, если он может не понадобиться? Используйте lazy var:

class DataManager {
    lazy var networkService = NetworkService() // создастся только при первом обращении
}

Избегайте лишних копирований

Каждый раз, когда вы передаёте массив в функцию, Swift может его скопировать (если это Array — value type). Используйте inout или UnsafeMutablePointer для больших данных.

2. Средний уровень: алгоритмы и память

Тут уже нужно думать головой.

Выбирайте правильные коллекции

Array — быстро по индексу, медленно на вставку в начало. Set — молниеносный поиск (O(1)). Dictionary — король для пар ключ-значение. Не используйте массив там, где нужен сет!

// Плохо: поиск в массиве — O(n)
let users = ["Alice", "Bob", "Charlie"]
if users.contains("Bob") { ... }

// Хорошо: поиск в сете — O(1)
let userSet: Set<String> = ["Alice", "Bob", "Charlie"]
if userSet.contains("Bob") { ... }

Работа с памятью: weak и unowned

Замыкания — частая причина утечек памяти. Если внутри замыкания вы используете self, и замыкание живёт дольше объекта — получаете retain cycle. Спасаемся:

class ViewController {
    var onTap: (() -> Void)?
    
    func setup() {
        onTap = { [weak self] in
            guard let self = self else { return }
            self.doSomething()
        }
    }
}

String vs NSString

Swift String — мощный, но для тяжёлых текстовых операций (парсинг, regex) лучше использовать NSString из Foundation. Он работает быстрее за счёт C-шного бэкенда.

3. Продвинутый уровень: GCD, Metal и Unsafe

Для тех, кто хочет выжать максимум.

Правильная работа с многопоточностью

DispatchQueue — хорошо, но не злоупотребляйте. Каждый переключение между потоками — накладные расходы. Используйте DispatchQueue.global(qos: .userInitiated) для задач, которые должны выполняться быстро, и .background для тяжёлой работы.

DispatchQueue.global(qos: .userInitiated).async {
    let result = self.heavyCalculation()
    DispatchQueue.main.async {
        self.updateUI(with: result)
    }
}

Для iOS 13+ есть Task и async/await — они ещё удобнее.

Unsafe Swift: когда нужно быстро

Если вы работаете с сырыми данными (аудио, видео, графика), стандартный Swift может быть медленным. Используйте UnsafeMutablePointer или ContiguousArray:

let count = 100_000
let buffer = UnsafeMutablePointer<Int>.allocate(capacity: count)
for i in 0..<count {
    buffer[i] = i * 2
}
buffer.deallocate()

Это работает быстрее, чем Array, но требует ручного управления

Блоги

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