管中小窥Combine中的内存管理

0.JPEG

# 前言

本文主要验证了 Publisher 通过 receive(subscriber:) 被订阅时存在的内存泄漏风险。

上篇 博文 中我们使用 Combine 封装了 MJRefresh,对 share() 通过不同的操作符生成其他 Publisher,并且改写了 ViewModel 的代码。这篇博文中我们会通过 Combine 封装 UIButton 为例验证下 Combine 中的内存管理。

# 准备工作

我们需要导航和两个 ViewController 最终的样子如下图:

首页 计数器页面
Simulator Screen Shot - iPhone 13 - 2022-04-16 at 19.11.58.png Simulator Screen Shot - iPhone 13 - 2022-04-16 at 19.12.03.png

由于要观察内存释放,所以后续的 PublisherSubscriptionSubscriber 都会使用 class 实现,并且在其 deinit {} 中做出相关打印,例如 TestButtondeinit {}

class TestButton: UIButton {
    deinit {
        print("TestButton deinit! ____#")
    }
}
复制代码

# 为 UIButton 的 title 提供 Subscriber 支持

在上篇博文中我们已经知道官方提供的 Subscribers.SinkSubscribres.Assign 都是 class 类型,并且遵守了 Cancellable 协议,并且官方在 Sinkfunc cancel() 上明确的阐述了:

  • Canceling should also eliminate any strong references it currently holds.
  • 取消还应该消除它当前持有的任何强引用。

但是 AnySubscriber 并不遵守 Cancellable 所以将 UIButtontitle 封装为 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 类型。

使用 SubscriptinUIControlevent 封装为 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 释放掉 cancelableControlSubscriptioncancel() 被调用,所以可以推测 sink(xx) 内部将 ControlSubscriptioncancel() 包裹到了返回值 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 是否释放了?

  1. 增加 sinkSubscriber.store(in: &cancellable) 后,ControlSubscriptioncancel() 被调用,且正常释放。

  2. 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 小结

通过三次订阅,我们得出结论:

  1. ControlPubisher 离开作用域后就销毁了,.eraseToAnyPublisher() 生成了结构体。
  2. ControlSubscription 被订阅后,会在订阅者或者订阅产生的 AnyCancellable 调用 cancel() 后销毁。
  3. ControlSubscription 被订阅时会被订阅者强引用。
  4. receive(subscriber:mySubscriber) 可能存在隐性的内存泄漏问题,这点在官方的说明中并没有体现出来。
  5. mySubscriber 避免直接使用 AnySubscriber
  6. mySubscriber 应该遵守 Cancellable 协议,并且在合适的时机调用 cancel()(如 .store(in:))。

# 探究 SubScriber

#1 创建 TestSubscriber

有了上述的验证,我们自定义的订阅者 应该和官方实现的类似——遵守 Combine.SubscriberCombine.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 后离开 作用域ControlSubscriptionTestSubscriber 会立即被释放,造成没有任何值的输入:

// 不强引用
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 很像? 这时我们就不必要求 SubscriberCancellable 类型:

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 我们可以明确,SubscriberSubscription 是相互强引用的,任何一方主动释放引用后就能打破此循环引用(释放的时机)。

到这就结束了吗? 当然不!

  • 还记得代码备注中的 Publishers/Shareshare 操作符吗?
  • 为什么 ControlPublisher 不使用遵守 Publisher 的方式实现?

# Share 的影响

让我们先来实现一下遵守 PublisherControlPublisher2

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/Sharefunc share() 的官方注释。

# 总结

UI 事件的 Combine 封装应该遵从官方指导使用现有的 Subject 实现,注意保持 Publisher 在产生其的 UI视图 的生命周期中应一直存在(参照 ControlPublishr3)。

对于类似 Moya 的网络请求, 或者 XTDemo 中本地 json 文件解析的 BundleJsonDataPublisher 这类

  1. 只在模块内部使用;
  2. func request(_ demand: Subscribers.Demand) {} 处理 Subscriber;
  3. Subscription 有正确的处理内部引用时机;
  4. 不会使用 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 源码

感谢您的阅读。

猜你喜欢

转载自juejin.im/post/7088153477418844173