iOS KVO预防崩溃处理

2021-07-01  本文已影响0人  李二侠

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这个类写进项目中就行。亲测手动多次添加和移除相同的观察对象也不会崩溃,放心使用,有问题请及时留言。

上一篇下一篇

猜你喜欢

热点阅读