Во время моего последнего технического разговора я случайно сказал: … модульные тесты на утечки Rx. Для меня было совершенно неожиданным осознание того, что почти никто даже не знает о возможности проведения таких тестов. Я не могу представить без них запуск моего реактивного кода в производство.

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

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

Предотвращение утечек

Есть два популярных способа предотвратить эту проблему: список захвата и спецификатор unowned / weak. Хочу добавить две дополнительные опции.

Старайтесь вообще не быть императивным

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

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

Используйте Binder

С UIKit очень сложно использовать декларативный стиль. Вот почему мы так любим RxCocoa с его расширениями. Но что делать, если для вас нет подходящего расширения? Например, вы хотите изменить много разных свойств. Вероятно, вы будете использовать императивный стиль:

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

Сейчас пора вспомнить, что RxCocoa уже решил эту проблему за нас. Есть удобная структура Binder, которая используется повсюду в реализации расширений UIKit. Вот его метод инициализации, который расскажет вам все о назначении этого типа. Он обрабатывает цикл сохранения за вас, удерживая слабую ссылку на целевой объект.

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

Мы не используем здесь второй параметр, потому что тип rx.tap Void. Конечно, вы можете изменить его на нужный тип и выполнить более сложную логику внутри обратного вызова.

Вы только посмотрите на последнюю строчку! Стало максимально понятным, каждый разработчик легко может понять, что здесь происходит. Если коллега-разработчик интересуется, как именно вы создали тень, он проверит свойство enableShadow. До этого времени нормально видеть только строку подписки.

Автоматизируйте это!

Ну вообще-то ... Все еще плохо. Да, существует множество удобных инструментов, позволяющих избежать утечек, но как гарантировать их правильное использование? Ручная проверка кода недостаточно, потому что разработчики - не роботы. Мы легко можем пропустить критическую ошибку, что приведет к сбою из-за нехватки памяти.

Есть способ автоматизировать это! Напишем тест на утечки Rx-памяти.

Включить режим отладки

Вы можете легко включить режим отладки, следуя этой инструкции. Для пользователей CocoaPods просто добавьте следующий скрипт в конец вашего Podfile:

post_install do |installer|
  installer.pods_project.targets.each do |target|
    if target.name == 'RxSwift'
      target.build_configurations.each do |config|
        if config.name == 'Debug'
          config.build_settings['OTHER_SWIFT_FLAGS'] ||= ['-D', 'TRACE_RESOURCES']
        end
      end
    end
  end
end

Флаг компиляции TRACE_RESOURCES необходим для доступа к структуре Resources. Это простое пространство имен для счетчика всех выделенных объектов RxSwift. Этот счетчик отслеживает объекты Observable, Observer и Disposable.

Модульный тест

С помощью этого инструмента довольно легко написать модульный тест на утечки. Нам нужно:

  1. Сделайте снимок A выделенных в данный момент ресурсов.
  2. Инициализировать тестируемый объект, инициировать подписку, дождаться деинициализации sut
  3. Сделайте снимок B выделенных в данный момент ресурсов.
  4. Убедитесь, что A == B. Это означает, что вся подписка была удалена, а ресурсы освобождены.

Всего 6 строк кода, чтобы гарантировать, что ваш контроллер представления правильно создает подписки. Вот и все, больше не беспокойтесь о [weak self], просто запустите тесты, и все готово.

Поразмыслив немного над этим, моя команда приняла это решение. Мы наследуем все наши классы тестов от настраиваемого подкласса XCTestCase. Это гарантирует отсутствие утечек памяти во всех тестовых случаях. Репо RxSwift использует этот паттерн для всех своих тестов.

Примечания

Будьте осторожны с синглетонами, у которых есть реактивные свойства (например, RxKeyboard). Он инициализируется после первого доступа и живет до завершения работы приложения (ресурсы не высвобождаются). Порядок тестов случайный, поэтому вы не знаете, когда будет создан одноэлементный объект. Это может быть причиной неудачных тестов. Мой совет - используйте fake AppDelegate и инициализируйте там все свои синглтоны Rx.

Подожди немного. Использование observeOn(MainScheduler.asyncInstance) или операторов сдвига времени (например, debounce, delay), скорее всего, приведет к ошибке синхронной проверки высвобождения ресурсов. Я предлагаю вам использовать toEventually Nimble matcher, который опрашивает тестовое условие с заданным интервалом и терпит неудачу, если оно не выполняется по истечении тайм-аута. Или просто реализуйте собственное ожидание как это было на основном репо.

Это действительно так.

  • Понять, почему в дизайне RxSwift существует эталонный цикл
  • Помните, что [weak self] - не единственный инструмент, который можно использовать для предотвращения утечек памяти.
  • Сократите время проверки кода вручную с помощью автоматических проверок
  • Пусть Rx войдет в свое сердце

Я собираюсь поделиться большим опытом работы с RxSwift на Saint AppsСonf. Встретиться со мной там!

Если вам понравился этот совет, хлопните в ладоши (50) и поделитесь, чтобы помочь другим найти его! Следуйте за мной, чтобы узнать больше о Rx. Twitter: M0rtyMerr