前言
刚学完swift
编程语言,这里直接搞一个swift版KVO
,顺便熟悉一下swift
里面讲了 swift
中 kvo
的基本使用,模仿 KVOController
的版本,还有 swift属性包装
小试
写的过程中也碰到了 swift
与 OC
各自 API
结合过程中碰到的一些问题(毕竟 UIKit
还是使用的OC
的,且 OC
的一些类也能使用),一起给大家分享一下
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-c
的 KVOController
而编写的,编写时减少了一层使用单例
统一处理监听和事件的场景,个人感觉不是必要的
其为一个自动释放监听的 KVO
框架,原理则是利用了引用计数的原理,当释放对象时,先释放父
对象,在释放子
对象,因此 KVOController
总是作为拥有类的子属性
而存在的,当持有 KVOController
的类释放时,监听会自动释放
注意:根据其释放时机,最好一个控制器页面
或交互场景
使用一个 KVOController
实例变量,尽量避免多个控制器使用一个,除非有需要,且使用时,被观察对象模型属性仍然要加上 @objc dynamic
字段
KVOController
实现如下所示:
__LLKVOInfo
首先我定义了一个 __LLKVOInfo
基本数据结构,用于保存 被监听者
、回调
、键值
,便于后续回调和移除使用
这里继承了 NSObject
,并实现了 hash
和 isEqual
,后面两个则是在 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
中的 Dictionary
和Object-c
中的 Dictionary
,毕竟不是每一个被观察者对象都遵循实现了哈希协议(例如:swift
中为 Hashable
协议)
而 NSMapTable
不需要考虑那么多,可以采用对象指针
作为哈希值key
,因此更为实用,而 value
保存的 __LLKVOInfo
类型,因此可以使用 NSSet
,swift
中的 Set
为结构体
,因此不适合,NSSet 解决重复使用的 hash
和 isEqual
,因此重写了这两个方法(也可以使用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
类型不太好用只能一个查找了(可以NSSet
换NSDictionary
,键值多时查找更迅速)
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
,才能更好的前进