KVO是iOS中常用的一种观察机制,具体用法这里不做过多描述。先说一下KVO的两种崩溃场景:
1.addObserver给同一个对象添加了相同的keypath;
2.removeObserver时,对象的keypath观察者重复移除,主要原因是add和remove的次数不匹配造成的。
再说一下实际开发中本人遇到的崩溃情况,之前做视频播放App,播放中可以自由切换列表播放源,在播放的过程中需要使用KVO对播放对象AVPlayerItem进行监听,具体监听如下:
//播放状态
addObserver(item, observer: self, keypath: "status", options: .new, context: nil)
//缓存进度
addObserver(item, observer: self, keypath: "loadedTimeRanges", options: .new, context: nil)
//当前缓存不够播放了
addObserver(item, observer: self, keypath: "playbackBufferEmpty", options: .new, context: nil)
//当前缓存可以播放
addObserver(item, observer: self, keypath: "playbackLikelyToKeepUp", options: .new, context: nil)
在切换到下一个播放源时,需要移除上一个AVPlayerItem对象上的Observer,再给新的AVPlayerItem添加上述监听。虽然代码层面上基本看不出什么问题,而且经过检查也确保
addObserver和removeObserver都是成对进行,然而理论终归是理论,在我们的App中,当用户量大了之后,崩溃记录上总会出现一下偶现的崩溃,防不胜防啊。
经过思考,我想到了一个解决办法,思路上通过一个中间类,在addObserver和removeObserver都加一层判断,具体代码如下:
import UIKit
class CRProxy: NSObject {
var lock = NSLock()
///用来记录观察者、需要观察的对象、keypath的数组
lazy var kvoInfo: [KVOObject] = {
return [KVOObject]()
}()
func CR_addObserver(target:NSObject, observer: NSObject, keypath: String, options: NSKeyValueObservingOptions = [], context: UnsafeMutableRawPointer?) {
if keypath.isEmpty { return }
//加个锁更安全
lock.lock()
//判断是否是同一个target添加重复的keypath,已存在相同的记录就直接返回,不作处理
if kvoInfo.contains(where: { ($0.keypath == keypath && $0.target == target) }) {
lock.unlock()
return
}
kvoInfo.append(KVOObject(target: target, keypath: keypath, observer: observer))
target.addObserver(observer, forKeyPath: keypath, options: options, context: context)
lock.unlock()
}
func CR_removeObserver(target:NSObject, observer: NSObject, keypath: String) {
if keypath.isEmpty { return }
lock.lock()
//判断要移除的观察者是否存在,存在就移除,否则直接返回
if kvoInfo.isEmpty || !kvoInfo.contains(where: { ($0.keypath == keypath && $0.target == target) }) {
lock.unlock()
return
}
kvoInfo.removeAll(where: { $0.target == target && $0.keypath == keypath})
lock.unlock()
}
struct KVOObject {
var keypath = ""
var observer: NSObject?
var target: NSObject?
init(target: NSObject, keypath: String, observer: NSObject) {
self.target = target
self.keypath = keypath
self.observer = observer
}
}
}
调用的话直接创建一个CRProxy的全局变量的对象,最好和播放器,来调用即可,具体事例如下:
//kov代理,预防kvo崩溃
lazy var proxy: CRProxy = {
return CRProxy()
}()
//监听PlayItem的缓存进度、播放状态等
func addKVOObserver(item: AVPlayerItem) {
//播放状态
proxy.CR_addObserver(target: item, observer: self, keypath: "status", options: .new, context: nil)
//缓存进度
proxy.CR_addObserver(target: item, observer: self, keypath: "loadedTimeRanges", options: .new, context: nil)
//当前缓存不够播放了
proxy.CR_addObserver(target: item, observer: self, keypath: "playbackBufferEmpty", options: .new, context: nil)
//当前缓存可以播放
proxy.CR_addObserver(target: item, observer: self, keypath: "playbackLikelyToKeepUp", options: .new, context: nil)
}
//移除PlayItem上的观察者
func removeKVOObserver(item:AVPlayerItem) {
proxy.CR_removeObserver(target: item, observer: self, keypath: "status")
proxy.CR_removeObserver(target: item, observer: self, keypath: "loadedTimeRanges")
proxy.CR_removeObserver(target: item, observer: self, keypath: "playbackBufferEmpty")
proxy.CR_removeObserver(target: item, observer: self, keypath: "playbackLikelyToKeepUp")
}
即取即用,直接给CRProxy这个类写进项目中就行。亲测手动多次添加和移除相同的观察对象也不会崩溃,放心使用,有问题请及时留言。