Всем привет! Меня зовут Фируза, я занимаюсь iOS-разработкой в компании SimbirSoft. Хочу поделиться результатами исследования утечек памяти, с которыми я столкнулась на одном из проектов.

Все началось с того, что я заметила подтормаживания UI и обратила внимание на рост занимаемой памяти в процессе использования приложения. В пределах нескольких сценариев расход памяти незначительно колебался – объекты создавались и освобождались.

Другие сценарии при непродолжительном использовании приводили к значительному росту памяти, используемой приложением.

При активном и продолжительном использовании могут «убежать» сотни мегабайт.

Утечки памяти – это частая проблема, с ней сталкиваются многие разработчики. Поэтому предлагаю разобрать основные причины возникновения и способы обнаружения утечек.

Статья будет интересна для iOS-разработчиков любого уровня. Изучив материал, вы сможете увеличить производительность вашего приложения, а также разобраться в том, как работают инструменты Xcode.

Причины утечки памяти в iOS

Утечки памяти в iOS происходят в тот момент, когда объекты в памяти приложения больше не нужны, но они не освобождаются. Во время использования приложения это может привести к увеличению использования памяти и снижению производительности.

Существует несколько распространенных причин утечек памяти в iOS:

  1. Циклы сильных ссылок. Возникают, когда два объекта держат сильные ссылки друг на друга, предотвращая освобождение объектов из памяти.

    1.1 Неправильное использование делегатов. Это объекты, которые получают уведомления от других объектов. Если делегат взаимно содержит сильную ссылку на объект, от которого он получает уведомления, это может вызвать утечку памяти.

    1.2 Неправильное использование замыканий. Это блоки кода, которые можно передавать в качестве аргументов функциям. Если замыкание содержит сильную ссылку на объект, это также влечет утечку памяти.

  2. Неправильное использование синглтонов. Аналогичная ситуация возникает, когда синглтон – объект, который сохраняется в течение всего времени существования приложения, содержит сильную ссылку на объект, которого не должно быть.

Чтобы предотвратить утечку памяти в iOS, важно различать типы ссылок в iOS и понимать, как они влияют на освобождение объектов. Если вы еще не знакомы с Xcode Instruments, самое время сделать это — именно они могут помочь найти и диагностировать утечки памяти в вашем приложении.

Как обнаружить утечки памяти в приложении для iOS

Существуют несколько способов обнаружения утечек памяти в iOS. Рассмотрим каждый из них.

С помощью Xcode Memory Graph

Xcode Memory Graph Debugger — это инструмент для отладки памяти, входящий в состав Xcode. Он позволяет разработчикам визуально исследовать объекты и их связи в памяти приложения, и помогает находить утечки памяти.

Чтобы использовать Xcode Memory Graph Debugger, нужно запустить профилирование вашего приложения в Xcode и выбрать инструмент Memory Graph Debugger. Он позволит просматривать визуальные диаграммы объектов и их связей в памяти, и исследовать их подробнее, чтобы установить причины утечек памяти или других проблем.

Для начала отредактируйте схему вашего приложения в Xcode, выберите “Edit Scheme…”:

Нажмите на схему “Run”, а затем выберите раздел “Diagnostics”. В этом разделе есть две настройки, которые необходимо включить:

  1. Первый параметр — Malloc Scribble в группе “Memory Management”. Это метод отладки поврежденной памяти в iOS, который помогает выявлять ошибки памяти, заполняя неинициализированную память предопределенным шаблоном (часто называемым “scribble”). Когда приложение обращается к этой памяти, это приводит к сбою программы или неожиданному поведению, а шаблон памяти можно использовать для определения источника ошибки. 

  2. Второй параметр, который необходимо включить — Malloc Stack Logging. Он позволяет разработчикам отслеживать выделение и освобождение памяти в своих приложениях. Если этот параметр включен, система регистрирует информацию о каждом выделении и освобождении памяти.

Эти параметры влияют на производительность, поэтому не стоит их использовать по умолчанию.

Запустите приложение и нажмите кнопку Debug Memory Graph в представлении переменных Xcode в левом нижнем углу:

При выделении одного из экземпляров в навигаторе отладки вам будет представлен граф отношений между объектами. Благодаря визуальному представлению можно отслеживать то, как создавались ссылки.

Рассмотрим простой пример.

Объявите два контроллера:

class FirstViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .white
        navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Next", style: .plain, target: self, action: #selector(onNextTapped))
    }
    	
    @objc func onNextTapped() {
        navigationController?.pushViewController(SecondViewController(), animated: true)
    }
}
class SecondViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .blue
        
        notificationConfigure()
    }
    
    func notificationConfigure() {
       NotificationCenter.default.addObserver(forName: NSNotification.Name("test"), object: nil, queue: .main) { notification in
            self.someFunc()
        }
    }
    
    func someFunc() {}

    deinit {
        NotificationCenter.default.removeObserver(self)
    }
}

Запустите приложение и нажмите кнопку “Next”. Далее нажмите кнопку “Debug Memory Graph”.

Здесь можно увидеть, что мы имеем одну ссылку на SecondViewController.

После этого закройте SecondViewController и откройте его заново. Перейдя в Debug Memory Graph, можно заметить, что ссылок на SecondViewController стало две.

Теперь вернитесь к коду SecondViewController и поправьте его:

class SecondViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .blue
        
        notificationConfigure()
    }
    
    func notificationConfigure() {
        NotificationCenter.default.addObserver(forName: NSNotification.Name("test"), object: nil, queue: .main) { [weak self] notification in // Добавили [weak self], чтобы устранить цикл сильных ссылок
            self?.someFunc()
        }
    }
    
    func someFunc() {}

    deinit {
        NotificationCenter.default.removeObserver(self)
    }
}

Повторив процедуру открытия экрана, можно увидеть, что количество ссылок держится на отметке 1. При закрытии экрана ссылка удаляется.

После завершения отладки лучше отключить параметры Malloc Scribble и MallocStackLogging, так как они негативно влияют на производительность.

С помощью Instruments

Instruments можно использовать для диагностики утечек памяти во время выполнения приложений. После запуска приложения вы можете увидеть в реальном времени, сколько ресурсов потребляется в строке Allocations, и были ли обнаружены какие-либо утечки памяти в строке Leaks:

Выберите Leaks:

Если обнаружены утечки, вы увидите ​​подробную информацию.

С помощью инструмента «Анализ»

Инструмент «Анализ» — это функция в Xcode для статического анализа кода после потенциальных утечек памяти. Она предназначена только для Objective-C, поэтому подробно останавливаться на ней не будем.

С помощью unit-тестов

Вы можете обнаружить потенциальные утечки памяти, используя unit-тесты. 

Используйте код из первого примера и напишите unit-тест:

func testMemory() {
        let secondViewController = SecondViewController()
        secondViewController.notificationConfigure()
        
        addTeardownBlock { [weak secondViewController] in
            XCTAssertNil(secondViewController, "Memory leak")
        }
    }

Запустите тест:

Добавьте [weak self] и запустите тест заново:

С помощью Symbolic breakpoint 

Это тип останова в Xcode, используемый для отладки кода. Может быть полезным, когда вы хотите исследовать определенный кусок кода или узнать, когда вызывается определенный метод.

Чтобы установить Symbolic breakpoint, вы можете выбрать в верхней панели Xcode “Debug” →  “BreakPoints” → “Create symbolic breakpoint”. Вы можете создать точку останова, чтобы отследить вызовы метода dealloc, и проверить, был ли освобожден контроллер из памяти.

Необходимо ввести имя функции или метода, которые вы хотите отслеживать, и нажать кнопку “Done”. Заполните поля в соответствии со следующими данными:

Name: UIViewControllerdealloc
Symbol: -[UIViewController dealloc]
Action -> Log Message: --- dealloc @(id)[$arg1 description]@ @(id)[$arg1 title]@

Запустите приложение. Если брекпоинт не срабатывает, и в консоль не выводится информации, это значит, что происходит утечка памяти.

Теперь добавьте в код [weak self] и запустите заново:

Из вывода в консоли вы можете увидеть, что память освобождается.

Заключение

Допустив утечки памяти в приложении, можно столкнуться со снижением производительности, и даже аварийным завершением работы приложения. Поэтому для стабильной работы важно своевременно обнаруживать и устранять эти проблемы. Надеюсь, этот материал помог вам глубже погрузиться в возможные способы оптимизации расхода памяти. 

Спасибо за внимание!

Авторские материалы для разработчиков также читайте в наших соцсетях – ВКонтакте и Telegram.

Комментарии (3)


  1. varton86
    00.00.0000 00:00

    Спасибо за статью. Есть еще один простой способ отслеживать высвобождение памяти - добавить в контроллер:

        deinit {

            debugPrint("???? deinit \(self)")

        }


    1. spiceginger
      00.00.0000 00:00

      И зачастую самый быстрый :)


    1. mobileSimbirSoft Автор
      00.00.0000 00:00

      Спасибо! Несмотря на то, что использование print в методе deinit может помочь выявить утечки памяти, такой подход дает неполную информации и результат в первом приближении. Рассмотренные инструменты помогут сузить круг поиска и локализовать проблему.