swift实战KVO

前言

刚学完swift编程语言,这里直接搞一个swift版KVO,顺便熟悉一下swift

里面讲了 swiftkvo 的基本使用,模仿 KVOController的版本,还有 swift属性包装 小试

写的过程中也碰到了 swiftOC 各自 API 结合过程中碰到的一些问题(毕竟 UIKit还是使用的OC的,且 OC 的一些类也能使用),一起给大家分享一下

案例demo

KVO

KVO 基本是开发中必备技能之一,在一些内容更新中比较常见(修改个人信息,点赞等信息)

由于 Swift 使用的仍然是 Object-c 语言的 UIKit框架,在添加 UI 以及 点击事件的时候碰到了一些问题,且由于编程语言限制,添加点击事件的时候,不能像Object-C一样使用SEL了,而是需要时使用 #Selector()的方式设置指针调用

let btn = UIButton(frame: CGRect(x: 0, y: 100, width: self.view.bounds.size.width, height: 40))
btn.setTitle("点击测试一下KVO", for: .normal)
btn.setTitleColor(UIColor.black, for: .normal)
btn.addTarget(self, action: #selector(self.onClickToModify), for: .touchUpInside)
self.view.addSubview(btn)
复制代码

且由于 swift 使用的仍然是 UIKit 框架(Object-c 编写的),因此函数需要标记 @objc 才能正常访问

@objc func onClickToModify() {
    baseModel?.age = 200
}
复制代码

基础KVO

了解其他KVO之前,先了解一下基本KVO的基本使用

被观察的模型类型如下所示,需要继承自 NSObject,且属性需要添加 @objc dynamic 标识,即 object-c 标识

class KVOBaseTestModel: NSObject {
    //必须要添加 @objc dynamic 参数才可以支持监听
    //且由于OC中没有可选类型,如果基本数据类型出现了可选类型会报错,毕竟基本类型不能赋值为nil
    @objc dynamic var age: Int = 0
    @objc dynamic var name: String?
}
复制代码

添加监听方法addObsercer,回调函数observeValue

//添加监听的方法
baseModel.addObserver(self, forKeyPath: "name", options: [.new, .old], context: nil)

//响应的回调,通过 NSKeyValueChangeKey 可以访问对应字典 change 内的数据
override func observeValue(forKeyPath keyPath: String?, of object: Any?, 
    change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
    print(change as Any)
}
复制代码

KVOController

这里的 KVOController 是模仿 Object-cKVOController 而编写的,编写时减少了一层使用单例统一处理监听和事件的场景,个人感觉不是必要的

其为一个自动释放监听的 KVO 框架,原理则是利用了引用计数的原理,当释放对象时,先释放对象,在释放对象,因此 KVOController 总是作为拥有类的子属性而存在的,当持有 KVOController 的类释放时,监听会自动释放

注意:根据其释放时机,最好一个控制器页面交互场景使用一个 KVOController 实例变量,尽量避免多个控制器使用一个,除非有需要,且使用时,被观察对象模型属性仍然要加上 @objc dynamic字段

KVOController 实现如下所示:

__LLKVOInfo

首先我定义了一个 __LLKVOInfo 基本数据结构,用于保存 被监听者回调键值,便于后续回调和移除使用

这里继承了 NSObject,并实现了 hashisEqual,后面两个则是在 NSSet 进行哈希值对比时需要用到的方法

class __LLKVOInfo: NSObject {
    weak var observer: AnyObject?
    var block: LLBlock
    var keyPath: String
    
    // block 需要设置 escaping 允许逃逸,毕竟没有直接执行,而是保存了起来
    init(observer: AnyObject, block: @escaping LLBlock, keyPath: String) {
        self.observer = observer
        self.block = block
        self.keyPath = keyPath
    }
    
    func hash() -> Int {
        return Int(self.keyPath.hash)
    }
    
    override func isEqual(_ object: Any?) -> Bool {
        if let obj = object as? __LLKVOInfo {
            if obj.keyPath == self.keyPath {
                return true
            }
        }
        return false
    }
}
复制代码

LLKVOController

LLKVOController 是观察的核心类,我们的 KVOController为主动观察类,通过主动观察被观察者来实现自动释放的 KVO 逻辑

由于被观察者有多个键值,且可能存在多个被观察者,因此,保存观察者信息时,我们以被观察者实例为键值,其被观察的多个键值存放在一个集合中,且保证不重复监听

经过考虑,采用了 Object-C 中的 NSMapTable 哈希表,至于没有选用 swift 中的 DictionaryObject-c 中的 Dictionary,毕竟不是每一个被观察者对象都遵循实现了哈希协议(例如:swift中为 Hashable协议)

NSMapTable不需要考虑那么多,可以采用对象指针作为哈希值key,因此更为实用,而 value 保存的 __LLKVOInfo 类型,因此可以使用 NSSetswift 中的 Set结构体,因此不适合,NSSet 解决重复使用的 hashisEqual,因此重写了这两个方法(也可以使用NSDictionary,更好用,这里只是一个案例)

如下所示,初始化了一个,NSMapTable和一个, 锁不多说,为了保证线程安全,NSMapTable如下所示

//默认强引用,会引用被观察者,仅当KVOController释放时,其会被释放和移除观察
//设置弱引用观察者,弱引用被观察者释放时可以不解除监听,如果是单例,则可能会出现多次监听问题
//设置弱引用适用于经常刷新数据的视图,以减少内存开销
//实际并不推荐弱引用,虽然不释放没什么影响,但如果系统缓存了新生成的监听子类
//若监听的属性没释放,可能会有额外性能开销
init(_ isWeakObserved: Bool = false) {
    infosMap = NSMapTable(keyOptions: 
        [isWeakObserved ? .weakMemory : .strongMemory, .objectPointerPersonality],
        valueOptions: [.strongMemory, .objectPointerPersonality])
    semaphore = DispatchSemaphore(value: 1)
}
复制代码

观察的代码如下所示

func observer(_ observedObj: AnyObject, _ keyPath: String, _ block: @escaping LLBlock) {
    //创建基本类型对象,同时用保存数据和数据对比
    let info = __LLKVOInfo(observer: observedObj, block: block, keyPath: keyPath)

    semaphore.wait()
    //获取指定对象的 value 集合
    var infoSet = infosMap.object(forKey: observedObj)

    if let set = infoSet {
        //这里已经有 set 了,说明添加过监听
        if set.contains(info) {
            //已经添加观察了,不再添加
            semaphore.signal()
            return
        }
    }else {
        //创建 set 并加入 InfosMap
        infoSet = NSMutableSet()
        infosMap.setObject(infoSet, forKey: observedObj)
    }

    //添加新监听信息
    infoSet!.add(info)

    semaphore.signal()
    //添加观察
    observedObj.addObserver(self, forKeyPath: keyPath, options: [.new, .old], context: nil)
}
复制代码

响应观察回调 observeValue,如下所示,UnsafeMutableRawPointer 类型不太好用只能一个查找了(可以NSSetNSDictionary,键值多时查找更迅速)

override func observeValue(forKeyPath keyPath: String?, of object: Any?, 
    change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
    let obj = object as AnyObject
    if let infoSet = self.infosMap.object(forKey: obj) {
        for info in infoSet {
            if let info = info as? __LLKVOInfo {
                if (info.keyPath == keyPath) {
                    info.block(change?[NSKeyValueChangeKey.newKey] as Any, obj)
                    return
                }
            }
        }
    }
}
复制代码

当我们的 KVOController 释放时,主动释放和移除里面所有的监听

deinit {
    for observer in self.infosMap.keyEnumerator() {
        let observed = observer as AnyObject
        let obj = self.infosMap.object(forKey: observed)
        for info in obj! {
            observed.removeObserver(self, forKeyPath: (info as! __LLKVOInfo).keyPath)
        }
    }
    print("KVOController释放了")
}
复制代码

swift属性包装之轻量型KVO

上面介绍了一个比较好用的 KVOController,这里使用 swift 特性 属性包装,来解决 KVO 的问题,

优点:代码极少,性能较高,使用简单,且不用@objc dynamic修饰属性,只需要正常使用属性包装即可

缺点:无法同时被多个对象监听,适用一个键值,一次监听的 KVO 使用,可以可以在完善

实现代码如下所示:

typealias LLObserverBlock = (Any) -> Void

@propertyWrapper
struct LLObserver<T> {
    private var observerValue: T? //记得设置初值,也可以通过init来设置
    private var block: LLObserverBlock?
    //默认的属性包装名称,参数名固定
    var wrappedValue: T? {
        get {observerValue}
        set {
            observerValue = newValue
            if let b = block {
                b(newValue as Any)
            }
        }
    }
    //属性映射名称,参数名固定(这里模拟写入数据库操作),调用参数时前面加上$即可(obj.$number)
    var projectedValue: LLObserverBlock {
        get {
            block!
        }
        set {
            block = newValue
        }
    }
    
    init() {
        observerValue = nil
        block = nil
    }
}
复制代码

使用更为简单,如下所示,则可以监听成功

observerModel = KVOObserverTestModel()
//设置监听
observerModel?.$name = { newValue in
    print("name", newValue)
}
observerModel?.$age = { newValue in
    print("age", newValue)
}
复制代码

需注意使用属性包装

class KVOObserverTestModel: NSObject {
    @LLObserver
    var age: UInt?
    
    @LLObserver
    var name: String?
}
复制代码

最后

这算是一次 swift 功能小试,且结合了 object-c 的一些常用类(Object-c的大多数类都还能正常使用),且发现 swift 基本内容功能不足(目前有些版本问题,只能仅仅swift新特性还不够),还仍需要 Object-C 中的一些基础功能来补全,因此,想开发好 ios, 只会 swift 编程语言是不行的,还要了解 Object-C,才能更好的前进

おすすめ

転載: juejin.im/post/7048576077060390942