Почему 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, но требует ручного управления