阅读 420

RxSwift之内存管理

关于内存管理,小伙伴们一定对闭包导致的循环引用有过了解。而RxSwift中大量使用到了闭包回调,那么RxSwift中该如何进行内存管理呢?

说明:本文中的示例代码只是关键代码,其余代码请自行脑补

一、swift闭包

如下面这段代码:

var myClosure: (() -> Void)?

myClosure = {
    print("\(self.name)")
}
        
myClosure?()
复制代码
在上面这段代码中,myClosure和name是 
viewController的一个属性。
复制代码

很明显,这是会引起内存泄漏的闭包循环引用self->myClosure->self

要打破这种循环引用也很简单。 1.使用 weak 关键字。

myClosure = { [weak self] in
    print("\(self?.name)")
}
复制代码

2.使用 unowned 关键字。

myClosure = { [unowned self] in
    print("\(self.name)")
}
复制代码

使用weakunowned关键字只能解决普通的循环引用问题,如果是下面这种情况呢?

myClosure = { [unowned self] in
    
DispatchQueue.global().asyncAfter(deadline: .now()+2, execute: {
        
        print("\(self.name)")
    })
}
复制代码
执行结果:
Fatal error: Attempted to read an unowned reference but the object was already deallocated
复制代码

使用 unowned 关键字结果会崩溃,那么使用 weak 关键字呢?

myClosure = { [weak self] in
    
    DispatchQueue.global().asyncAfter(deadline: .now()+2, execute: {
        
        print("\(self?.name)")
    })
}
复制代码

执行结果虽然不会崩溃,但是打印出来的值是 nil,也和我们的期望不一致。

所以,在延迟调用这种情况下,只使用weakunowned 都不能完美结局这个问题。那就没办法解决了吗?

myClosure = { [weak self] in
    
    guard let strongSelf = self else { return }
    DispatchQueue.global().asyncAfter(deadline: .now()+2, execute: {
        
        print("\(strongSelf.name)")
    })
}
复制代码

执行可以打印出 name 的值。使用 let strongSelf = self 将弱引用转为强引用,则可以完美解决。这就是著名的 weak-strong dance

前面对swift的Closure引起的内存管理问题进行了简单的回顾,那么给小伙伴们留一个问题:是不是所有的Closure都需要使用 weak 弱引用?

二、RxSwift中的内存管理

示例1:UI订阅

self.textFiled.rx.text.orEmpty
    .subscribe(onNext: { (text) in
        self.title = text
    })
    .disposed(by: disposeBag)
复制代码

示例1会有循环引用吗?我们来分析一下: self -> textFiled -> subscribe -> self,通过持有链分析,确实会有循环引用问题,通过执行验证,controller确实没有销毁。所以需要使用 weak 弱引用。

示例二:UI绑定

self.textFiled.rx.text.orEmpty
    .bind(to: self.rx.title)
    .disposed(by: disposeBag)
复制代码

实例二会有循环引用吗?执行发现,并没有循环引用产生。因为 self.rx.title仅作为参数,并没有被引用。所以不会有循环引用问题。

示例三:持有序列,序列订阅中持有self

self.observable = Observable<Any>.create({ (observer) -> Disposable in
    
    observer.onNext("Hello")
    
    return Disposables.create()
})

self.observable?.subscribe(onNext: { (value) in
    
    print(self.name)
})
    .disposed(by: disposeBag)
复制代码

从执行结果来看,controller无法销毁,产生了循环引用。那我们来分析一下持有链。 在 subscribe 订阅时会创建 observer 持有 onNext 闭包。然后 observer 会被传递给 sinksink 又会将自身封装为 AnyObserver 回传给 create 闭包,作为 observer 参数。

self -> observable -> subscribe -> observer -> onNext{} -> self 通过持有链分析,也能很清晰的发现确实是循环引用的。

示例4:持有序列,创建序列中持有self

self.observable = Observable<Any>.create({ (observer) -> Disposable in
    
    observer.onNext("Hello")
    
    print(self.name)
    
    return Disposables.create()
})

self.observable?.subscribe(onNext: { (value) in
    
    print("\(value)")
})
    .disposed(by: disposeBag)
复制代码

self -> observable -> create{} -> self 分析持有链,示例中的代码也会循环引用。

示例5:持有观察者

Observable<Any>.create { (observer) -> Disposable in
    self.observer = observer
    
    observer.onNext("Hello")
    
    return Disposables.create()
    }
    .subscribe(onNext: { (value) in
        
        print(self.name)
    })
    .disposed(by: disposeBag)
复制代码

我们先分析 self.observer = observer 会产生循环引用吗? 我们知道,在创建序列时 create 闭包中持有了 self。但是 self 并没有持有,create 创建的序列。所以并不会产生循环引用。 那么在订阅时,是否是也不会产生循环引用呢? 我们在示例4中已经大概分析过 subscribe 的流程,所以我们知道 observer 中会保存 subscribe 中的 onNext 闭包,而 observer 又被 self 所持有了,那么就会形成如下持有链 self -> observer -> onNext{} -> self 所以必然会形成循环引用。

经过上面5个示例的分析,小伙伴们一定对RxSwift什么时候会产生循环引用已经有了一个大致的了解。但是很多小伙伴肯定也会有疑问,平时自己在项目中,并不会如上面的示例中这样使用RxSwift。那么RxSwift的实际项目使用中,我们又该如何进行RxSwift内存管理呢?

三、RxSwift内存管理实战

使用场景:控制器间响应,控制器1订阅响应,控制器2发出信号。

控制器2 代码:

// 创建外部可订阅的序列
fileprivate var mySubject = PublishSubject<Any>()
var publicOB : Observable<Any>{
    return mySubject.asObservable()
}

// 点击屏幕 发出信号
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
    
    mySubject.onNext("RxSwift")
}
复制代码
控制器1 代码:

// 点击屏幕 push到控制器2 
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
    
    let vc = NextViewController()
    
    // 订阅控制器2中的可观察序列
    vc.publicOB.subscribe(onNext: { (value) in
        print("\(value)")
    })
        .disposed(by: disposeBag)
    
    self.navigationController?.pushViewController(vc, animated: true)
}
复制代码

执行代码,完美,Perfect!控制器2 释放了,没有循环引用。似乎一切都是那么恰到好处。

那么我们再使用RxSwift自身的计数来验证一下。

控制器1 代码:

// 点击屏幕 push到控制器2 
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
    
    // 打印 RxSwift计数
    print("RxSwift计数: \(RxSwift.Resources.total)")
    
    let vc = NextViewController()
    
    // 订阅控制器2中的可观察序列
    vc.publicOB.subscribe(onNext: { (value) in
        print("\(value)")
    })
        .disposed(by: disposeBag)
    
    self.navigationController?.pushViewController(vc, animated: true)
}
复制代码

再次执行,会发现,每次push再pop后,虽然 控制器2 被销毁了,但是RxSwift计数会增加1,这表示虽然控制器2被销毁了,没有产生循环引用,但是 publicOB 的订阅然后存在,没有取消。

那么,这就是不完美的,那应该如何解决呢? 我们再来分析上面的代码。

RxSwift核心之Disposable中已经分析过,当 self.disposeBag 自动销毁时,disposeBag 中的所有订阅关系都会被取消,那么在上面的代码中,我们是将对 vc.publicOB 的订阅加入到 控制器1disposeBag 中的。那么在 控制器1disposeBag销毁前,其内部的订阅都不会取消。所以,当 控制器1 push到 控制器2 再pop回到 控制器1 时,控制器1disposeBag 并没有被销毁,所以对 vc.publicOB 的订阅关系仍然存在。

分析了 RxSwift计数 增加的原因,那么我们又该如何解决呢?

方式一: 既然是因为我们将对 vc.publicOB 的订阅加入到 控制器1disposeBag 中导致无法取消订阅,那么我们就将订阅添加到 vc.disposeBag 中,让其和 控制器2 共存亡。

vc.publicOB.subscribe(onNext: { (value) in
    print("\(value)")
})
    .disposed(by: vc.disposeBag)
复制代码

控制器2 pop时,控制器2执行销毁,vc.disposeBag 也会被销毁,而且内部的订阅也会被取消。

方式二: 自动取消订阅除了使用 DisposeBag 之外,还可以使用 takeUtil 操作符。

_ = vc.publicOB.takeUntil(vc.rx.deallocated)
    .subscribe(onNext: { (value) in
        print("\(value)")
    })
复制代码

takeUtil 操作符将使得订阅一直持续到控制器的 dealloc 事件产生为止。

方式三: 如果 控制器2 是被 控制器1 所持有的,那么当pop之后,控制器2 是不会被销毁的,那么又该如何处理呢?

RxSwift的内存管理可以让其自动管理订阅,其实也可以手动取消订阅,发送 completed 或者 error 事件,都可以取消订阅关系。

控制器1 代码:

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
    
    print("RxSwift计数: \(RxSwift.Resources.total)")

    // 控制器1 持有 控制器2    
    self.vc.publicOB.subscribe(onNext: { (value) in
        print("\(value)")
    })
        .disposed(by: disposeBag)
    
    self.navigationController?.pushViewController(vc, animated: true)
}
复制代码
override func viewWillDisappear(_ animated: Bool) {
    super.viewWillDisappear(animated)
    
    // 页面即将消失时发送 `completed` 事件
    mySubject.onCompleted()
}
复制代码

上面的代码可以实现RxSwift的订阅管理,但是在使用中发现,第二次push到 控制器2 后,控制器1 无法收到信号了。

原因是,订阅在收到 completed 事件后,已经取消了订阅,无法再次接收到信号。

既然知道了原因,那么我们可以在再次push时,重新激活 publicOB

fileprivate var mySubject = PublishSubject<Any>()
var publicOB : Observable<Any>{
    mySubject = PublishSubject<Any>()
    return mySubject.asObservable()
}
复制代码

这样就可以在push时,获得一个新的 publicOB 就可以再次发送接收信号了。

以上就是RxSwift内存管理的一些示例和解决方式。可能栗子举得很局限,不能代表项目中的实际情况,但是仍希望其中分析与解决方式能带给小伙伴们一起启发。若有不足之处,请评论指正。