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都加一层判断,具体代码如下:
Swift:
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})
target.removeObserver(observer, forKeyPath: 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
}
}
}
OC:
//
// CRProxy.m
// wxyd
//
// Created by Li Dong on 2021/10/8.
//
#import "CRProxy.h"
@interface CRKVOObject : NSObject
@property (nonatomic, copy) NSString * keypath;
@property (nonatomic, strong) NSObject * observer;
@property (nonatomic, strong) NSObject * target;
@end
@implementation CRKVOObject
- (instancetype)initWithTarget:(NSObject *)target
observer:(NSObject *)observer
keypath:(NSString *)keypath {
if (self = [super init]) {
self.target = target;
self.observer = observer;
self.keypath = keypath;
}
return self;
}
@end
@interface CRProxy ()
@property (nonatomic, strong) NSLock *lock;
@property (nonatomic, strong) NSMutableArray *kvoInfo;
@end
@implementation CRProxy
- (NSLock *)lock{
if (!_lock) {
_lock = [NSLock new];
}
return _lock;
}
- (NSMutableArray *)kvoInfo{
if (!_kvoInfo) {
_kvoInfo = [NSMutableArray new];
}
return _kvoInfo;
}
- (void)CR_addTarget:(NSObject *)target
observer:(NSObject *)observer
keypath:(NSString *)keypath
options:(NSKeyValueObservingOptions)options{
if (!target || !observer || !keypath) {
return;
}
[self.lock lock];
//判断是否是同一个target添加重复的keypath,已存在相同的记录就直接返回,不作处理
for (CRKVOObject * obj in self.kvoInfo) {
if ([obj.keypath isEqualToString:keypath] && obj.target == target) {
[self.lock unlock];
return;
}
}
CRKVOObject * obj = [[CRKVOObject alloc]initWithTarget:target observer:observer keypath:keypath];
[target addObserver:observer forKeyPath:keypath options:options context:nil];
[self.kvoInfo addObject:obj];
[self.lock unlock];
}
- (void)CR_removeTarget:(NSObject *)target
observer:(NSObject *)observer
keypath:(NSString *)keypath{
if (!target || !observer || !keypath) {
return;
}
[self.lock lock];
if (!self.kvoInfo.count) {
return;
}
//判断要移除的观察者是否存在,存在就移除,否则直接返回
CRKVOObject *kvoObj = nil;
for (CRKVOObject * obj in self.kvoInfo) {
if ([obj.keypath isEqualToString:keypath] && obj.target == target) {
kvoObj = obj;
break;
}
}
if (kvoObj) {
//移除
[self.kvoInfo removeObject:kvoObj];
[target removeObserver:observer forKeyPath:keypath];
}
[self.lock unlock];
}
@end
调用的话直接创建一个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这个类写进项目中就行。亲测手动多次添加和移除相同的观察对象也不会崩溃,放心使用,有问题请及时留言。
网友评论