美文网首页
iOS KVO预防崩溃处理

iOS KVO预防崩溃处理

作者: 李二侠 | 来源:发表于2021-07-01 11:27 被阅读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这个类写进项目中就行。亲测手动多次添加和移除相同的观察对象也不会崩溃,放心使用,有问题请及时留言。

    相关文章

      网友评论

          本文标题:iOS KVO预防崩溃处理

          本文链接:https://www.haomeiwen.com/subject/blkjultx.html