# 前言
本文主要验证了 Publisher
通过 receive(subscriber:)
被订阅时存在的内存泄漏风险。
上篇 博文 中我们使用 Combine
封装了 MJRefresh
,对 share()
通过不同的操作符生成其他 Publisher
,并且改写了 ViewModel
的代码。这篇博文中我们会通过 Combine
封装 UIButton
为例验证下 Combine
中的内存管理。
# 准备工作
我们需要导航和两个 ViewController
最终的样子如下图:
首页 | 计数器页面 |
---|---|
由于要观察内存释放,所以后续的 Publisher
,Subscription
和 Subscriber
都会使用 class
实现,并且在其 deinit {}
中做出相关打印,例如 TestButton
的 deinit {}
:
class TestButton: UIButton {
deinit {
print("TestButton deinit! ____#")
}
}
复制代码
# 为 UIButton 的 title 提供 Subscriber 支持
在上篇博文中我们已经知道官方提供的 Subscribers.Sink
和 Subscribres.Assign
都是 class
类型,并且遵守了 Cancellable
协议,并且官方在 Sink
的 func cancel()
上明确的阐述了:
- Canceling should also eliminate any strong references it currently holds.
- 取消还应该消除它当前持有的任何强引用。
但是 AnySubscriber
并不遵守 Cancellable
所以将 UIButton
的 title
封装为 AnySubscriber
会存在一定的问题(后文有说明):
extension UIButton {
// FIXME: - 这里使用 AnySubscriber 包裹会存在问题,后续验证中给与说明!
public func subscriber(forTitle state: UIControl.State) -> AnySubscriber<String, Never> {
let sinkSubscriber = Subscribers.Sink<String, Never> { _ in
print("Subscriber<Button.title> finished! ____&")
} receiveValue: { [weak self] value in
self?.setTitle(value, for: state)
}
return .init(sinkSubscriber)
}
}
复制代码
# 为 UIControl 的 event 提供 Publisher 能力
SubScription
定义如下:
public protocol Subscription : Cancellable, CustomCombineIdentifierConvertibl
复制代码
其中 CustomCombineIdentifierConvertibl
协议为 class
提供了默认实现,因此我们见到的绝大部分 Subscriptions
都是 class
类型。
使用 Subscriptin
将 UIControl
的 event
封装为 Publisher
,同样在 func cancel()
中释放引用:
extension UIControl {
// FIXME: - 第一种实现方式,问题和 Button 的 subscriber(forTitle:) 存在关联性
public func publisher1(forAction event: UIControl.Event) -> AnyPublisher<UIControl, Never> {
let publisher = ControlPublisher1(control: self, event: event)
.eraseToAnyPublisher()
return publisher
}
}
// NOTE: - 第一种实现方式,对于 <Control: UIControl> 的约束可以去除
fileprivate final class ControlPublisher1<Control: UIControl>: Publisher {
typealias Failure = Never
typealias Output = Control
private weak var control: Control?
private let event: UIControl.Event
private var cancelStore: [(() -> Void)] = []
init(control: Control, event: UIControl.Event) {
self.control = control
self.event = event
}
deinit {
// NOTE: - 1. 虽然能够实现内存释放, 但 share() 操作后 ControlPublisher1 会强引用
// NOTE: - 2. 此种情况下 deinit 不会执行, 形成循环引用
// cancelStore.forEach { $0() }
Swift.print("ControlPublisher1<\(type(of: control))> deinit! ____#")
}
func receive<S>(subscriber: S) where S : Subscriber, Never == S.Failure, Control == S.Input {
let subscription = ControlSubscription(subscriber: subscriber, control: control, event: event)
// cancelStore.append(subscription.cancel)
subscriber.receive(subscription: subscription)
}
}
fileprivate final class ControlSubscription<S: Subscriber, Control: UIControl>: Combine.Subscription where S.Input == Control, S.Failure == Never {
// NOTE: - 同上
private weak var control: Control?
private var subscriber: S?
init(subscriber: S, control: Control?, event: UIControl.Event) {
print("ControlSubscription<\(type(of: control))> init! ____^")
self.control = control
self.subscriber = subscriber
control?.addTarget(self, action: #selector(doAction(sender:)), for: event)
}
deinit {
Swift.print("ControlSubscription<\(type(of: control))> deinit! ____#")
}
// NOTE: - 流的发送在 doAction(sender:) 中,这里不做任何事情
// NOTE: - 通常收到的 demand 都是 .unlimited
func request(_ demand: Subscribers.Demand) {
guard demand > 0 else { return cancel() }
}
// NOTE: - 遵守官方在 Subscribers.Sink/cancel() 中的注释
// NOTE: - 这里释放掉对 subscriber 的引用,同时注意线程安全问题
func cancel() {
subscriber = nil
print("ControlSubscription<\(type(of: control))> cancel! ____@")
}
@objc private func doAction(sender: UIControl) {
if let control = control {
_ = subscriber?.receive(control)
}
}
}
复制代码
# 验证 UIControl event Publisher
#1 验证普通的 .sink()
在计数器页面 func bindViewModel() {}
添加如下代码:
let addPublisher = addButton.publisher1(forAction: .touchUpInside)
.map { [weak self] _ -> String in
self?.count += 1
return "\(self?.count ?? 0)"
}
addPublisher
.sink { value in
print("多次订阅 - 1 - 接收到值: \(value)")
}
.store(in: &cancellable)
复制代码
进入 计数器页面并点击 计数加一 后退出页面,打印结果如下:
ControlSubscription<Optional<UIControl>> init! ____^
ControlPublisher1<Optional<UIControl>> deinit! ____#
多次订阅 - 1 - 接收到值: 1
ViewController deinit! ____#
ControlSubscription<Optional<UIControl>> cancel! ____@
ControlSubscription<Optional<UIControl>> deinit! ____#
TestButton deinit! ____#
TestButton deinit! ____#
复制代码
无内存泄漏。ControlPublisher1
在被订阅后就销毁了,在 ViewControll
释放掉 cancelable
时 ControlSubscription
的 cancel()
被调用,所以可以推测 sink(xx)
内部将 ControlSubscription
的 cancel()
包裹到了返回值 AnyCancellable
内部的 cancel()
中调用(会强引用 subscription
)。
记录点1: 回到 ControlSubscription
注释掉 cancel()
中的 subscriber = nil
,发现并不影响打印结果,为什么?
#2 验证 receive(subscriber:)
替换 addPublisher.sink {xx}
为如下代码:
let sinkSubscriber = Subscribers.Sink<String, Never>.init(receiveCompletion: { _ in }) { value in
print("sinkSubscriber 接收到值: \(value)")
}
addPublisher.receive(subscriber: sinkSubscriber)
复制代码
同 #1
操作得到打印结果为:
ControlSubscription<Optional<UIControl>> init! ____^
ControlPublisher1<Optional<UIControl>> deinit! ____#
sinkSubscriber 接收到值: 1
ViewController deinit! ____#
TestButton deinit! ____#
TestButton deinit! ____#
复制代码
结果 ControlSubscription
未能释放,内存泄漏。
记录点2: 此时 sinkSubscriber
是否释放了?
-
增加
sinkSubscriber.store(in: &cancellable)
后,ControlSubscription
的cancel()
被调用,且正常释放。 -
在
1
的基础上使用AnySubscriber(sinSubscriber)
包裹监听addPublisher
能到与1
同样的效果。
记录点3: 同样此时注释掉 cancel()
的 subscriber = nil
不影响打印结果,为什么?
#3 测试自己封装的 AnySubscriber
理论效果同 #2
中包裹 sinkSubscriber
一样, 将 addPublisher.receive(subscriber: sinkSubscriber)
替换为,我们封装的 UIButton.subscriber(forTitle:)
:
addPublisher.receive(subscriber: countButton.subscriber(forTitle: .normal))
复制代码
同样点击 计数加一 后 countButton
显示为 1
后返回,打印结果如下:
ControlSubscription<Optional<UIControl>> init! ____^
ControlPublisher1<Optional<UIControl>> deinit! ____#
ViewController deinit! ____#
TestButton deinit! ____#
TestButton deinit! ____#
复制代码
ControlSubscription
未能释放,同样存在内存泄漏。这里的解决办法放在后文中。
#4 通过 PassthroughSubject 转换测试
放在后面的 TestSubscriber
中一同展示。
#5 小结
通过三次订阅,我们得出结论:
ControlPubisher
离开作用域后就销毁了,.eraseToAnyPublisher()
生成了结构体。ControlSubscription
被订阅后,会在订阅者或者订阅产生的AnyCancellable
调用cancel()
后销毁。ControlSubscription
被订阅时会被订阅者强引用。receive(subscriber:mySubscriber)
可能存在隐性的内存泄漏问题,这点在官方的说明中并没有体现出来。mySubscriber
避免直接使用AnySubscriber
。mySubscriber
应该遵守Cancellable
协议,并且在合适的时机调用cancel()
(如.store(in:)
)。
# 探究 SubScriber
#1 创建 TestSubscriber
有了上述的验证,我们自定义的订阅者 应该和官方实现的类似——遵守 Combine.Subscriber
和 Combine.Cancellable
。如下是创建 TestSubscriber
的全部代码和相关注释:
/// 应当遵守官方备注: 在 cancel 释放内存(引用).
/// 对于自定义的 Subscriber 应该和官方的 `Sink` 类似, 遵守 `Cancellable`
/// 在传入 `Publisher` 的 `receiver(subscriber:)` 后
/// 必须在适合的时机调用 `cancel`, 如 `store(in: )`
final class TestSubscriber: Combine.Subscriber, Combine.Cancellable {
typealias Input = String
typealias Failure = Never
// FIXED: - 需要强引用一次 subscription, 保证其生命周期内 subscription 一直存在
// var subscription: Subscription?
// FIXED: - 使用 cancelAction 代替
var cancelAction: (() -> Void)?
// FIXED: - 这里不强引用, 防止测试阶段 Button 不释放,并且 demo 忽略性能损失. 可强引用, 注意 cancel 释放
private weak var button: UIButton?
private var state: UIButton.State
convenience init() {
self.init(button: nil, state: .normal)
}
init(button: UIButton?, state: UIButton.State) {
self.button = button
self.state = state
}
deinit {
print("TestSubscriber deinit! ____#")
}
func receive(subscription: Subscription) {
// FIXED: - 测试多次订阅不同 publisher, 只对最后一个订阅生效, 可不调用 cancel
cancelAction?()
cancelAction = subscription.cancel
// @note: - 这里可以限制请求次数
subscription.request(.unlimited)
}
func receive(_ input: String) -> Subscribers.Demand {
print("TestSubscriber 接收到值: \(input)")
button?.setTitle(input, for: state)
return .none
}
func receive(completion: Subscribers.Completion<Never>) {
// FIXED: - 对 Subject 转换来的 publisher, 接收到 completion 后不需要特殊处理
// 但是对 TestSubscriber 实例调用 receive(completion:) 无法释放内存
cancelAction = nil
print("TestSubscriber 结束订阅! ____&")
}
func cancel() {
// FIXED: - 对于 自定义的 `Publisher` 准换的 `Publishers/Share` 或者 `Publishers/Multicast` 必须调用 cancel
// FIXED: - 对于 `Subject` 无此要求
// Tips: - Share 实现参考官方注释
cancelAction?()
cancelAction = nil
}
}
复制代码
其中对 subscription
的强引用是必须的,如不引用(注释掉 cancelAction = subscription
)则订阅 addPublisher
后离开 作用域 后 ControlSubscription
和 TestSubscriber
会立即被释放,造成没有任何值的输入:
// 不强引用
let testSubscriber = TestSubscriber()
addPublisher
.receive(subscriber: testSubscriber)
复制代码
这里在 viewDidLoad()
最后增加了打印
ControlSubscription<Optional<UIControl>> init! ____^
ControlSubscription<Optional<UIControl>> deinit! ____#
TestSubscriber deinit! ____#
ControlPublisher1<Optional<UIControl>> deinit! ____#
ViewDidLoad!
ViewController deinit! ____#
TestButton deinit! ____#
TestButton deinit! ____#
复制代码
修改为强引用后,务必加入到 cancellable
的集合中:
let testSubscriber2 = TestSubscriber(button: countButton, state: .normal)
testSubscriber2.store(in: &cancellable)
addPublisher
.receive(subscriber: testSubscriber2)
复制代码
#2 验证使用 Subject 时的内存管理
我们在 ViewController
中增加如下属性:
private let receiveSubject = PassthroughSubject<String, Never>()
复制代码
使用 receiveSubject
作为 testSubscriber3
的发布者:
func bindViewModel() {
...
addPublisher.sink { [weak self] value in
self?.receiveSubject.send(value)
}
.store(in: &cancellable)
let testSubscriber3 = TestSubscriber(button: countButton, state: .normal)
// NOTE: - 这里并没有加入到 Set<AnyCancellabel> 中
// testSubscriber3.store(in: &cancellable)
receiveSubject.receive(subscriber: testSubscriber3)
}
...
deinit {
// NOTE: - 通知订阅者订阅结束
receiveSubject.send(completion: .finished)
...
}
复制代码
打印结果:
ControlSubscription<Optional<UIControl>> init! ____^
ControlPublisher1<Optional<UIControl>> deinit! ____#
TestSubscriber 接收到值: 1
TestSubscriber 结束订阅! ____&
TestSubscriber deinit! ____#
ViewController deinit! ____#
ControlSubscription<Optional<UIControl>> cancel! ____@
ControlSubscription<Optional<UIControl>> deinit! ____#
TestButton deinit! ____#
TestButton deinit! ____#
复制代码
testSubscriber3
并没有加入到 cancellable
中,但是我们增加了 receiveSubject.send(completion: .finished)
,TestSubscriber
能被正常释放,可见 Subject
中的 send(completion:)
方法中做了内存方面的处理。
其实官方文档中对我们创建自己的 Publisher
有建议的:
/// # Creating Your Own Publishers
///
/// Rather than implementing the ``Publisher`` protocol yourself, you can create your own publisher by using one of several types provided by the Combine framework:
///
/// - Use a concrete subclass of ``Subject``, such as ``PassthroughSubject``, to publish values on-demand by calling its ``Subject/send(_:)`` method.
/// - Use a ``CurrentValueSubject`` to publish whenever you update the subject’s underlying value.
/// - Add the `@Published` annotation to a property of one of your own types. In doing so, the property gains a publisher that emits an event whenever the property’s value changes. See the ``Published`` type for an example of this approach.
复制代码
官方更主张我们使用已有的 Subject
类型来创建 Publisher
。所以把上面的 receiveSubject
封装到我们的 ControlPublisher3
中,就会出现如下实现:
extension UIControl {
public func publisher3(forAction event: UIControl.Event) -> AnyPublisher<UIControl, Never> {
// NOTE: - 根据 event 生成关联对象的 key,见源码或补充部分
let eventKey = event.publisherActionKey
if let wraped = objc_getAssociatedObject(self, eventKey) as? ControlPublisher3 {
return wraped.publisher
} else {
let publisher = ControlPublisher3(control: self, event: event)
// NOTE: - 这里需要强持有 publisher, 类似于 View 强持有 ViewModel
objc_setAssociatedObject(self, eventKey, publisher, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
return publisher.publisher
}
}
}
fileprivate final class ControlPublisher3 {
private let subject = PassthroughSubject<UIControl, Never>()
let publisher: AnyPublisher<UIControl, Never>
private weak var control: UIControl?
init(control: UIControl, event: UIControl.Event) {
self.control = control
self.publisher = self.subject.eraseToAnyPublisher()
control.addTarget(self, action: #selector(doAction(sender:)), for: event)
}
deinit {
subject.send(completion: .finished)
Swift.print("ControlPublisher3<\(type(of: control))> deinit! ____#")
}
@objc private func doAction(sender: UIControl) {
if let control = control {
subject.send(control)
}
}
}
复制代码
是不是和我们写在 ViewModel
中的 Publisher
很像? 这时我们就不必要求 Subscriber
是 Cancellable
类型:
let addPublisher = addButton.publisher3(forAction: .touchUpInside)
.map { [weak self] _ -> String in
self?.count += 1
return "\(self?.count ?? 0)"
}
let testSubscriber3 = TestSubscriber(button: countButton, state: .normal)
addPublisher.receive(subscriber: AnySubscriber(testSubscriber3))
复制代码
同样操作后打印如下:
TestSubscriber 接收到值: 1
ViewController deinit! ____#
TestButton deinit! ____#
TestButton deinit! ____#
TestSubscriber 结束订阅! ____&
TestSubscriber deinit! ____#
ControlPublisher3<Optional<UIControl>> deinit! ____#
复制代码
无内存泄漏,直接使用 receive(subscriber:)
也没有内存泄漏的风险。
#3 小结
还记得我们的 记录点<1, 2, 3> 吗?通过上面的推测和 TestSubscriber
我们可以明确,Subscriber
和 Subscription
是相互强引用的,任何一方主动释放引用后就能打破此循环引用(释放的时机)。
到这就结束了吗? 当然不!
- 还记得代码备注中的
Publishers/Share
和share
操作符吗? - 为什么
ControlPublisher
不使用遵守Publisher
的方式实现?
# Share 的影响
让我们先来实现一下遵守 Publisher
的 ControlPublisher2
:
extension UIControl {
func publisher2(forAction event: UIControl.Event) -> AnyPublisher<UIControl, Never> {
let eventKey = event.publisherActionKey
if let publisher = objc_getAssociatedObject(self, eventKey) as? AnyPublisher<UIControl, Never> {
return publisher
} else {
let publisher = ControlPublisher2(control: self, event: event)
.eraseToAnyPublisher()
objc_setAssociatedObject(self, eventKey, publisher, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
return publisher
}
}
}
fileprivate final class ControlPublisher2: Publisher {
typealias Failure = Never
typealias Output = UIControl
private weak var control: UIControl?
// NOTE: - 经典类型摸除, 参考文章见补充
// NOTE: - 模仿多次订阅
private var sendControls: [((UIControl) -> Void)] = []
private var sendFinished: [(() -> Void)] = []
init(control: UIControl, event: UIControl.Event) {
self.control = control
control.addTarget(self, action: #selector(doAction(sender:)), for: event)
}
deinit {
sendFinished.forEach { $0() }
sendControls.removeAll()
sendFinished.removeAll()
Swift.print("ControlPublisher2<\(type(of: control))> deinit! ____#")
}
func receive<S>(subscriber: S) where S : Subscriber, Never == S.Failure, UIControl == S.Input {
sendControls.append({ ctr in _ = subscriber.receive(ctr) })
sendFinished.append({ subscriber.receive(completion: .finished) })
// NOTE: - 不需要关心 request(_) 和 cancel() 直接使用 empty 实现
subscriber.receive(subscription: Subscriptions.empty)
}
@objc private func doAction(sender: UIControl) {
if let control = control {
sendControls.forEach { $0(control) }
}
}
}
复制代码
在 ViewController
中
let addPublisher = addButton.publisher2(forAction: .touchUpInside)
.map { [weak self] _ -> String in
self?.count += 1
return "\(self?.count ?? 0)"
}
//.share()
let testSubscriber3 = TestSubscriber(button: countButton, state: .normal)
// testSubscriber3.store(in: &cancellable)
addPublisher.receive(subscriber: AnySubscriber(testSubscriber3))
复制代码
不打开 share()
一切正常,打开 share()
后则必须将 testSubscriber3
存储到 Set<AnyCancellable>
中,才能正常释放。
但打开 share()
使用 ControlPunlisher3
则不需要对 testSubscriber3
进行 cancel
操作。
share()
的更多细节请参阅 Publishers/Share
和 func share()
的官方注释。
# 总结
对 UI
事件的 Combine
封装应该遵从官方指导使用现有的 Subject
实现,注意保持 Publisher
在产生其的 UI视图 的生命周期中应一直存在(参照 ControlPublishr3
)。
对于类似 Moya
的网络请求, 或者 XTDemo
中本地 json
文件解析的 BundleJsonDataPublisher
这类
- 只在模块内部使用;
- 在
func request(_ demand: Subscribers.Demand) {}
处理Subscriber
; Subscription
有正确的处理内部引用时机;- 不会使用
share()
,multicast()
进行转换;
的,应该使用 Subscription
来实现自己的 Publisher
。
# 补充
#1 类型摸除
#2 UIControl.Event 的关联对象 key
```Swift
@available(iOS 13.0, *)
fileprivate struct AssociatedActionKeys {
static var kDefaultKey: Void?
static var touchDown: Void?
...
@available(iOS 14.0, *)
static var menuActionTriggered: Void?
...
static var allEvents: Void?
}
@available(iOS 13.0, *)
extension UIControl.Event {
var publisherActionKey: UnsafeRawPointer {
if #available(iOS 14.0, *) {
if case .menuActionTriggered = self {
return .init(UnsafeMutableRawPointer(&AssociatedActionKeys.valueChanged))
}
}
switch self {
case .touchDown:
return .init(UnsafeMutableRawPointer(&AssociatedActionKeys.touchDown))
....
default:
return UnsafeRawPointer(UnsafeMutableRawPointer(&AssociatedActionKeys.kDefaultKey))
}
}
}
```
复制代码
#3 对 multicast 或 share 的其他验证
请在源码中自行探索
#4 源码
感谢您的阅读。