Ответ
Для обнаружения утечек памяти и retain cycles в iOS-разработке применяется комбинация инструментов Xcode и методик написания кода.
1. Debug Memory Graph (Визуализатор памяти)
Самый быстрый и наглядный инструмент, встроенный в Xcode.
Как использовать:
- Запустите приложение в режиме отладки.
- Выполните действия, которые могут вызывать утечку.
- Нажмите кнопку Debug Memory Graph (значок из трех кругов) на панели отладки.
- Xcode отобразит граф всех живых объектов в памяти. Пурпурные значки с восклицательным знаком указывают на потенциальные циклы сильных ссылок.
- Щелкните на объекте, чтобы увидеть цепочку владения (кто на кого ссылается).
Что искать:
- Два объекта, которые ссылаются друг на друга через сильные (
strong) ссылки. - Классы (например, ViewController), которые не освобождаются после закрытия экрана.
2. Instruments
Более мощный и детализированный инструмент для профилирования.
- Allocations: Позволяет отслеживать выделение и освобождение памяти по типам объектов. Ищите постоянно растующее число определенных объектов.
- Leaks: Специализированный инструмент для поиска утечек. Он показывает объекты, на которые больше нет ссылок из "корней" (unreachable), но память для которых не была освобождена.
Как использовать:
- В Xcode: Product → Profile (или
Cmd+I). - Выберите шаблон Leaks или Allocations.
- Запишите сессию, взаимодействуя с приложением.
- Анализируйте стек вызовов для найденных утечек.
3. Логирование жизненного цикла
Ручное добавление логов в deinit для отслеживания освобождения объектов.
class MyViewController: UIViewController {
deinit {
print("(type(of: self)) был освобожден")
}
}
Если это сообщение не появляется при ожидаемом закрытии контроллера — есть утечка.
4. Типичные причины retain cycles и их решение
Пример 1: Замыкание (Closure), захватывающее self
class DataManager {
var data: [String] = []
var onDataUpdated: (() -> Void)? // Сильная ссылка на замыкание
func loadData() {
// ПРОБЛЕМА: Замыкание сильно захватывает `self`.
fetchFromNetwork { [self] newData in
self.data = newData
self.onDataUpdated?() // Если onDataUpdated также ссылается на это замыкание? Цикл!
}
}
}
Решение: Использовать weak или unowned захват.
fetchFromNetwork { [weak self] newData in
guard let self = self else { return }
self.data = newData
self.onDataUpdated?()
}
Пример 2: Делегат (Delegate)
class ChildView {
var delegate: MyDelegate? // Сильная ссылка на делегата
}
class ParentController: MyDelegate {
let childView = ChildView()
init() {
childView.delegate = self // Родитель сильно владеет childView, а childView сильно ссылается на родителя -> ЦИКЛ.
}
}
Решение: Объявить делегата как weak.
protocol MyDelegate: AnyObject { ... } // Протокол должен быть class-bound
class ChildView {
weak var delegate: MyDelegate?
}
Лучшие практики:
- Всегда используйте
weakдля ссылок на делегаты и источники данных (data sources). - Для захвата
selfв замыканиях, которые могут пережить объект, используйте[weak self]. - Используйте
unownedтолько когда вы абсолютно уверены, что захваченный объект будет жить дольше замыкания (иначе — краш). - Для реактивного программирования (RxSwift, Combine) не забывайте о
DisposeBagиAnyCancellableдля управления подписками.