iOS探索RxSwift之内存管理

1,138 阅读2分钟

一、RxSwift内存管理

1.observeWeakly

接下来我们来看一个例子:

        self.person.rx.observe(String.self, "name")
            .subscribe(onNext: { change in
                print("observe订阅到了KVO:\(String(describing: change))")
            }).disposed(by: disposeBag)

        self.person.rx.observeWeakly(String.self, "name")
            .subscribe(onNext: { change in
                print("observeWeakly订阅到了KVO:\(String(describing: change))")
            }).disposed(by: disposeBag)

看到了两个不同的observe observeWeakly,分别订阅了name的change。

observe和observeWeakly 有什么区别吗?

1.observe有可能会出现释放不干净的情况
2.observeWeakly连weak一起监听解决释放不干净的情况

进入源码查看:

image.png 再次进入observeWeaklyKeyPathFor:

    private func observeWeaklyKeyPathFor(_ target: NSObject, keyPath: String, options: KeyValueObservingOptions) -> Observable<AnyObject?> {
        let components = keyPath.components(separatedBy: ".").filter { $0 != "self" }
        let observable = observeWeaklyKeyPathFor(target, keyPathSections: components, options: options)
            .finishWithNilWhenDealloc(target)
        if !options.isDisjoint(with: .initial) {
            return observable
        }
        else {
            return observable
                .skip(1)
        }
    }

切割字符components 在传入observeWeaklyKeyPathFor 处理返回序列。断点调试看看:

image.png 获取到propertyName属性名称,propertyAttributes 属性的详细pointervalue。在通过isWeakProperty 判断是否是弱引用声明。在注册KVOObservable 属性监听。 image.png 在propertyObservable.flatMapLatest接收KVOObservable属性变化监听的回调的容错处理。 image.png 这里注意先执行.subscribe订阅闭包 在执行.flatMapLatest闭包。

进入KVOObservable代码: image.png KVOObservable继承ObservableType实现KVOObservableProtocol协议,保存属性,循环订阅属性消息on(.next)

2.RxSwift-KVO流程

现在我们看KVOObserver 类,KVOObservable->subscribe->KVOObserver(parent: self)做了什么。

private final class KVOObserver
    : _RXKVOObserver
    , Disposable {
    typealias Callback = (Any?) -> Void
    var retainSelf: KVOObserver?

    init(parent: KVOObservableProtocol, callback: @escaping Callback) {
        #if TRACE_RESOURCES
            _ = Resources.incrementTotal()
        #endif
        super.init(target: parent.target, retainTarget: parent.retainTarget, keyPath: parent.keyPath, options: parent.options.nsOptions, callback: callback)
        self.retainSelf = self
    }

    override func dispose() {
        super.dispose()
        self.retainSelf = nil
    }

    deinit {
        #if TRACE_RESOURCES
            _ = Resources.decrementTotal()
        #endif
    }

}

_RXKVOObserver->继承NSObject为什么呢?

第一个想到的是获取对象的属性参数,得到元类信息。

第二个这个可能是个中间类,对rx的属性path进行观察(观察者移交)。

在看super.init->_RXKVOObserver.init image.png 进入断点调试 image.png 从断点上看"student.nickName"订阅了两次,一层一层的订阅,在_RXKVOObserver进行KVO属性监听。 然后发现observeWeaklyKeyPathFor是一个递归调用的方法。

so:回到最初的observeWeakly就是底层实现了KVO对属性的监听。

3.Rx内存管理

然后我们在回到KVOObservable代码看到: image.png_RXKVOObserver在内部也是弱引用接收target image.png unowned var 就是oc中的 unsafe_unretained 一样效果。(为什么用unsafe_unretained而不是weak呢)因为性能更高。

现在我们看一个例子:

        myClosure = { [weak self] in
            self?.name = "NY"
        }
        self.myClosure!()
        
        
    deinit {
        print("\(self)销毁 VC")
    }

上面代码如果不加[weak self]会产生循环引用,这个是基本情况。那如果出现别的情况呢?

也可以把weak换成unowned也能解决循环引用。 image.png

        myClosure = { [weak self] in
            DispatchQueue.global().asyncAfter(deadline: .now()+2) {
                self?.name = "NY"
                print(self?.name as Any)
            }
        }

修改代码增加异步延迟操作2秒,在赋值打印运行程序。

image.png

我们发现并没有崩溃,也没有报错。只是打印nil,但是这个结果并不友好,不利于我们发现问题。

这么解决nil问题呢,这个代码大家都会的。再次声明self 为强引用

    guard let self = self else { return }   //4.2版本之后 保留关键字
    self.name = "NY"
    print(self.name as Any)

image.png

接下来在看一个例子:

self.accountTF.rx.text.orEmpty
            .subscribe(onNext:{ text in
                self.title = text
            }).disposed(by: disposeBag)

会发生循环引用吗? image.png 没有输出销毁VC,这段代码发生了循环引用!!!具体是为什么呢?

修改代码添加 [weak self]

self.accountTF.rx.text.orEmpty
            .subscribe(onNext:{ [weak self]text in
                self?.title = text
            }).disposed(by: disposeBag)

看到运行结果,销毁了VC并且rx计数归0。 image.png 核心问题:
那么RX的引用计数是在什么时候销毁归0的呢?

我们在DisposeBase中设置断点,运行运行代码调试. image.png 在首页显示,VC销毁之后rx在进行引用计数减法。

继续修改代码:

self.accountTF.rx.text.orEmpty
            .bind(to: self.rx.title)
            .disposed(by: disposeBag)

这样修改后的代码会发生循环引用吗? image.png 看打印就已经知道,这样修改后没有发生循环引用。为什么呢?rxswift内部是这么处理的呢?

在通过一个例子研究一下:

self.observable = Observable<Any>.create { anyObserver -> Disposable in
            anyObserver.onNext("Hello World")
            print(self)
            return Disposables.create()
        }
        
        self.observable?.subscribe(onNext: {
            print("订阅到了:\($0) --")
        }).disposed(by: self.disposeBag)

在创建序列时打印print(self),这里会产生循环引用吗? image.png 答案是会产生循环引用,并没有销毁VC。 self->observable->create{}->self

这步要注意.subscribe中调用print(self)也会循环引用。 image.png

继续看代码:

Observable<Any>.create { anyObserver -> Disposable in
            self.observer = anyObserver
            anyObserver.onNext("Hello World")
            return Disposables.create()
        }
        .subscribe(onNext: { item in
            print(self)
            print("订阅到了:\(item)")
        })
        .disposed(by: self.disposeBag)

这个代码会产生循环引用吗? image.png 答案是会,self -> anyObserver -> subscribe -> self 所以产生了循环引用关系。

let vc = LGDetialViewController()
        vc.publicOB.subscribe(onNext:{ item in
            print("在\(self)订阅到了\(item)")
        }).disposed(by: disposeBag)

这段代码中会产生rxswift计数循环引用,并不是VC页面上的循环引用。 image.png 每次点击都会增加rx的引用计数。为什么呢?

我们看到打印VC并没有销毁,是vc产生了循环引用。怎么处理这个问题呢?怎么销毁掉VC?

当前DetialViewController的disposed用的不应该是用VC的disposeBag。

vc.disposeBag然后在运行代码 image.png 看到了rx计数并没有累加,问解决。

或者使用_ = vc.publicOB.take(until: vc.rx.deallocated)也能解决问题,任务直到vc.rx.deallocated 详情页销毁的时候结束。

如果当DetialViewController页是用mySubject.onCompleted()结束,vc中使用DetialViewController的let dvc声明需要在DetialViewController页中再次激活:

mySubject = PublishSubject<Any>()//激活

总结

  1. observeWeakly:的主要递归函数是observeWeaklyKeyPathFor对属性进行监听,KVOObservable是最终接收监听属性的类。
  2. RxSwift-KVO流程:KVOObservable->subscribe->KVOObserver->_RXKVOObserver(移交观察者)-OC原生KVO
  3. Rx内存管理:unowned var 就是oc中的 unsafe_unretained 一样效果。及在RxSwift中[weak self]的使用,RxSwift自身有针对内部使用的引用计数,主要是用disposeBag来进行维护。subscribe(onNext)和subscribe{}的执行代码不一样,结果也不一样subscribe(onNext)容易出现循环引用(要注意细节)。