ReactiveSwift 一个与多个信号订阅和相关的内存开销
ReactiveSwift one vs multiple signal subscriptions and related memory overhead
我有一个简单的信号,在其中一个应用程序组件中,returns 一组项目:
var itemsSignal: Signal<[Item], Never>
这些项目 可能 包含以 table 视图形式呈现在屏幕上的数据更新。任务是将更新应用于屏幕上出现的单元格。
关于如何做到这一点,我可以想到两种可能的方法。该应用程序是用 MVVM 风格编写的,但我只是为了示例的目的。
第一种方法是在视图控制器代码级别订阅此信号一次,然后在observeValues
块中检查我们收到更新的任何地方屏幕上的项目带有一些 for 循环并更新相应单元格的状态。这样我们将只有一个订阅,但在我看来,当我们基本上使用视图控制器级别代码将更新从源传递到屏幕上的各个单元格时,这会引入不必要的代码耦合。
第二种方法是从每个单独的单元格(在现实单元格的视图模型中)订阅此信号并应用一些过滤,如下所示:
disposables += COMPONENT.itemsSignal
.flatten()
.filter({ [=11=].itemId == itemId })
.observeValues({
...
})
但这会创建多个 订阅 - 每个单元格一个。
我实际上更喜欢第二种方法,因为在我看来,从设计的角度来看它更简洁,因为它不会泄漏任何不必要的知识来查看控制器级别的代码。无论何时在不同的屏幕上重复使用同一个单元格,这种自我更新行为都会被继承并且开箱即用。
问题是由于多次订阅,第二种方法 memory/cpu 贵多少?在这个项目中,我们使用 ReactiveSwift
,但我认为这也适用于其他 Rx
库。
纯粹从反应式编程的角度来看,我理解为什么第二种方法很有吸引力。但是 UITableView
的性质使得您确实需要在行更新中涉及 table。最简单的方法是使用 iOS 13 中引入的新奇的 UITableViewDiffableDataSource
,但如果您不使用它,则必须调用 reloadData
或 reloadRows(at:)
告诉 table 在数据更改时更新所有行或特定行。
如果您订阅每个单元格,那么它可能在某些或大多数情况下都有效,但如果您有更改单元格高度的数据更新(例如,通过在标签中显示更长的文本并导致它换行到第二行)然后单元格将无法正确调整大小,因为 table 不知道该行已更新。
我同意你的看法,行不能以完全独立的方式管理它们自己的更新是令人不快的,但据我所知,UITableView
出于性能原因以这种方式工作。
在 RxSwift 中,我们的 RxCocoa 库已经实现了您的第一个想法,并在 tableView 上做了一个简单的 reloadData。
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 序列的 Observable;这是您的第一个想法,但它只负责项目的插入和删除。我将创建每个当前存在的单元订阅的 [ID: Info]
的第二个 Observable。在单元格 creation/reuse,它被赋予一个 ID 并订阅第二个可观察对象并过滤掉它感兴趣的信息。在单元格 prepareForReuse
它取消订阅。
由于每个 现有 单元格只有一个订阅,因此实际上没有那么多新订阅(取决于单元格的高度和 table 查看。)我的应用通常在任何时候都有数千个订阅 运行,因此通过这种“每个单元格”方法添加的额外订阅数量甚至都不会引起注意。
我有一个简单的信号,在其中一个应用程序组件中,returns 一组项目:
var itemsSignal: Signal<[Item], Never>
这些项目 可能 包含以 table 视图形式呈现在屏幕上的数据更新。任务是将更新应用于屏幕上出现的单元格。
关于如何做到这一点,我可以想到两种可能的方法。该应用程序是用 MVVM 风格编写的,但我只是为了示例的目的。
第一种方法是在视图控制器代码级别订阅此信号一次,然后在observeValues
块中检查我们收到更新的任何地方屏幕上的项目带有一些 for 循环并更新相应单元格的状态。这样我们将只有一个订阅,但在我看来,当我们基本上使用视图控制器级别代码将更新从源传递到屏幕上的各个单元格时,这会引入不必要的代码耦合。
第二种方法是从每个单独的单元格(在现实单元格的视图模型中)订阅此信号并应用一些过滤,如下所示:
disposables += COMPONENT.itemsSignal
.flatten()
.filter({ [=11=].itemId == itemId })
.observeValues({
...
})
但这会创建多个 订阅 - 每个单元格一个。
我实际上更喜欢第二种方法,因为在我看来,从设计的角度来看它更简洁,因为它不会泄漏任何不必要的知识来查看控制器级别的代码。无论何时在不同的屏幕上重复使用同一个单元格,这种自我更新行为都会被继承并且开箱即用。
问题是由于多次订阅,第二种方法 memory/cpu 贵多少?在这个项目中,我们使用 ReactiveSwift
,但我认为这也适用于其他 Rx
库。
纯粹从反应式编程的角度来看,我理解为什么第二种方法很有吸引力。但是 UITableView
的性质使得您确实需要在行更新中涉及 table。最简单的方法是使用 iOS 13 中引入的新奇的 UITableViewDiffableDataSource
,但如果您不使用它,则必须调用 reloadData
或 reloadRows(at:)
告诉 table 在数据更改时更新所有行或特定行。
如果您订阅每个单元格,那么它可能在某些或大多数情况下都有效,但如果您有更改单元格高度的数据更新(例如,通过在标签中显示更长的文本并导致它换行到第二行)然后单元格将无法正确调整大小,因为 table 不知道该行已更新。
我同意你的看法,行不能以完全独立的方式管理它们自己的更新是令人不快的,但据我所知,UITableView
出于性能原因以这种方式工作。
在 RxSwift 中,我们的 RxCocoa 库已经实现了您的第一个想法,并在 tableView 上做了一个简单的 reloadData。
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 序列的 Observable;这是您的第一个想法,但它只负责项目的插入和删除。我将创建每个当前存在的单元订阅的 [ID: Info]
的第二个 Observable。在单元格 creation/reuse,它被赋予一个 ID 并订阅第二个可观察对象并过滤掉它感兴趣的信息。在单元格 prepareForReuse
它取消订阅。
由于每个 现有 单元格只有一个订阅,因此实际上没有那么多新订阅(取决于单元格的高度和 table 查看。)我的应用通常在任何时候都有数千个订阅 运行,因此通过这种“每个单元格”方法添加的额外订阅数量甚至都不会引起注意。