ReactiveSwift one против нескольких подписок на сигналы и связанных с ними затрат памяти

#ios #swift #rx-swift #reactive-swift

#iOS #swift #rx-swift #реактивный-быстрый

Вопрос:

У меня есть простой сигнал в одном из компонентов приложения, который возвращает массив элементов:

 var itemsSignal: Signal<[Item], Never>
 

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

Я могу придумать два возможных способа, как это можно сделать. Приложение написано в стиле MVVM, но я просто для примера.

Первый способ — подписаться на этот сигнал один раз на уровне кода контроллера представления, затем при observeValues проверке блока везде, где мы получаем обновления для элементов на экране с помощью некоторого for-цикла, и обновлять состояния для соответствующих ячеек. Таким образом, у нас будет только одна подписка, но это приведет к ненужному, на мой взгляд, связыванию кода, когда мы в основном используем код уровня контроллера представления для передачи обновлений из источника в отдельные ячейки на экране.

Второй способ — подписаться на этот сигнал из каждой отдельной ячейки (в модели представления ячейки в реальности) и применить некоторую фильтрацию следующим образом:

 disposables  = COMPONENT.itemsSignal
    .flatten()
    .filter({ $0.itemId == itemId })
    .observeValues({ 
        ... 
    })
 

Но это создает несколько подписок — по одной для каждой отдельной ячейки.

На самом деле я предпочитаю второй метод, поскольку, на мой взгляд, он намного чище с точки зрения дизайна, поскольку он не пропускает никаких ненужных знаний для просмотра кода уровня контроллера. И везде, где одна и та же ячейка повторно используется на другом экране, это самообновляющееся поведение будет унаследовано и будет работать «из коробки».

Вопрос в том, насколько дороже второй метод памяти / процессора из-за нескольких подписок? В этом проекте мы используем ReactiveSwift , но я думаю, что это актуально и для других Rx библиотек.

Ответ №1:

В RxSwift наша библиотека RxCocoa уже реализует вашу первую идею и выполняет простую повторную загрузку данных в TableView.

 items
    .bind(to: tableView.rx.items) { (tableView, row, element) in
        let cell = tableView.dequeueReusableCell(withIdentifier: "Cell")!
        cell.textLabel?.text = "(element) @ row (row)"
        return cell
    }
 

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

 items
    .bind(to: tableView.rx.items(cellIdentifier: "Cell", cellType: UITableViewCell.self)) { (row, element, cell) in
        cell.textLabel?.text = "(element) @ row (row)"
    }
 

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

В качестве ответа на ваш основной вопрос о ресурсах… Я часто использую гибридное решение, в котором есть наблюдаемый объект, который просто содержит последовательность идентификаторов объектов; это ваша первая идея, но она заботится только о вставке и удалении элементов. Я сделаю второе наблюдение [ID: Info] за тем, на что подписывается каждая существующая в настоящее время ячейка. При создании / повторном использовании ячейки ей присваивается идентификатор, она подписывается на этот второй наблюдаемый объект и отфильтровывает только интересующую ее информацию. В ячейке prepareForReuse он отписывается.

Поскольку это только одна подписка на существующую ячейку, на самом деле не так много новых подписок (в зависимости от высоты ячейки и высоты табличного представления). В моих приложениях обычно одновременно выполняются тысячи подписок, поэтому количество дополнительных подписок, добавленных при таком подходе «на ячейку», даже не заметно.

Ответ №2:

Чисто с точки зрения реактивного программирования, я понимаю, почему второй подход привлекателен. Но природа UITableView такова, что вам действительно нужно включать таблицу в обновления строк. Самый простой способ сделать это — использовать новомодный UITableViewDiffableDataSource , представленный в iOS 13, но если вы его не используете, тогда необходимо вызвать либо reloadData или reloadRows(at:) сообщить таблице обновить все строки или определенные строки при изменении их данных.

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

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

Комментарии:

1. Высота содержимого здесь не является фактором, мне интересно только о занимаемой памяти и, возможно, о некоторых других накладных расходах обоих решений.

2. Если вы создадите подписку tableView(_:cellForRowAt:) и отмените ее при повторном использовании ячейки (может быть, с чем-то вроде .take(until: cell.reactive.prepareForReuse) ?), Я не могу представить, что объем памяти будет большой проблемой, потому что подписок будет столько, сколько ячеек видно на экране. Но если вы пытаетесь заранее создать и подписаться на модель представления каждой ячейки, это определенно создаст ненужные накладные расходы, в зависимости от количества элементов.

3. Но это также, вероятно, потребует от вас создания itemSignal MutableProperty<[Item]> , чтобы вы всегда могли получить последний массив элементов tableView(_:cellForRowAt:) .