SwiftBond源码简析
最近一段时间在看swift的数据绑定,所以找到了开源库swiftbond
这是个很巧妙的设计,充分使用了swift的语言特性。
关于这个库的实现过程,上一篇blog也有讲,不过显然那只是个原理,并不是最终的结果。
我们看看看看这个库的核心类Dynamic<T>,这个类是bond库的灵魂,这是个模板类,用于各种类型的数据变量。正是这个类使得变量在变化的同时触发一系列事件,达到绑定的目的。
// MARK: Dynamic public class Dynamic<T> { private var dispatchInProgress: Bool = false internal var _value: T? { didSet { objc_sync_enter(self) if let value = _value { if !self.dispatchInProgress { dispatch(value) } } objc_sync_exit(self) } } public var value: T { set { _value = newValue } get { if _value == nil { fatalError("Dynamic has no value defined at the moment!") } else { return _value! } } } public var valid: Bool { get { return _value != nil } } private func dispatch(value: T) { // clear weak bonds self.bonds = self.bonds.filter { bondBox in bondBox.bond != nil } // lock self.dispatchInProgress = true // dispatch change notifications for bondBox in self.bonds { bondBox.bond?.listener?(value) } // unlock self.dispatchInProgress = false } public let valueBond = Bond<T>() public var bonds: [BondBox<T>] = [] private init() { _value = nil valueBond.listener = { [unowned self] v in self.value = v } } public init(_ v: T) { _value = v valueBond.listener = { [unowned self] v in self.value = v } } public func bindTo(bond: Bond<T>) { bond.bind(self, fire: true, strongly: true) } public func bindTo(bond: Bond<T>, fire: Bool) { bond.bind(self, fire: fire, strongly: true) } public func bindTo(bond: Bond<T>, fire: Bool, strongly: Bool) { bond.bind(self, fire: fire, strongly: strongly) } }
剥离一些细节,我们看看这个类的两个灵魂变量,那就是valueBond,bonds。这两个变量最终放置的都是类Bond的实例,至于Bond我们暂时先不讲,它的灵魂变量是个Listener,所以你可以先把Bond看成一个执行block。
- valueBond 是用来改变Dynamic自身变量的block。通过Bond类的源码可以知道,这是个强引用。
- bonds 用来指向别的Dynamic的valueBond,当自身变量发生变化时就会执行这些bonds,这是个弱引用
这样我们就形成了一个绑定链,比如DynamicA.bonds->DynamicB.valueBond, DynamicB.bonds->DynamicC.valueBond
这样当DynamicA的值发生变化是,就会触发DynamicB,DynamicC的变化。
对于ios开发,数据绑定,通常最终会反映到一个具体的控件,比如UILable, UITextField, UIButton等等。
那么swiftbond是通过给每个控件添加一个Dynamic类型的属性来实现绑定的。
比如UILable的实现
import UIKit private var textDynamicHandleUILabel: UInt8 = 0; private var attributedTextDynamicHandleUILabel: UInt8 = 0; extension UILabel: Bondable { public var dynText: Dynamic<String> { if let d: AnyObject = objc_getAssociatedObject(self, &textDynamicHandleUILabel) { return (d as? Dynamic<String>)! } else { let d = InternalDynamic<String>(self.text ?? "") let bond = Bond<String>() { [weak self] v in if let s = self { s.text = v } } d.bindTo(bond, fire: false, strongly: false) d.retain(bond) objc_setAssociatedObject(self, &textDynamicHandleUILabel, d, objc_AssociationPolicy(OBJC_ASSOCIATION_RETAIN_NONATOMIC)) return d } } public var dynAttributedText: Dynamic<NSAttributedString> { if let d: AnyObject = objc_getAssociatedObject(self, &attributedTextDynamicHandleUILabel) { return (d as? Dynamic<NSAttributedString>)! } else { let d = InternalDynamic<NSAttributedString>(self.attributedText ?? NSAttributedString(string: "")) let bond = Bond<NSAttributedString>() { [weak self] v in if let s = self { s.attributedText = v } } d.bindTo(bond, fire: false, strongly: false) d.retain(bond) objc_setAssociatedObject(self, &attributedTextDynamicHandleUILabel, d, objc_AssociationPolicy(OBJC_ASSOCIATION_RETAIN_NONATOMIC)) return d } } public var designatedBond: Bond<String> { return self.dynText.valueBond } }
关于方法objc_getAssociatedObject,objc_setAssociatedObject的使用,请自行查阅资料。
可以看出这里面添加了两个Dynamic类型的属性dynText,dynAttributedText。
当一个viewModel的变量绑定到一个UILable的时候,其实是绑定到这个dynText上了,也就是说UILable的text属性值是经由dynText来改变的,所以dynText的bonds里一定要保存能改变UILable.text的Bond(也就是Listener),这是由上面蓝色代码实现的。
通常如果是一个Dynamic类型的变量的话,它自身会strong reference能改变自身值得valueBond,但是如果这个绑定链的终端是个非Dynamic类型的值的话,那自然需要新创建一个Bond,但是这个Bond并没有被任何一个变量强引用,会丢失的,对于控件,是让新添加的Dynamic类型变量来retain这个Bond的。
当然如果是在controller里创建的Bond的话,应该让controller或者它retain的变量去retian这个Bond了。
既然是个绑定链,自然中间会产生多个Dynamic类型的变量打个比方
UILable.dynText<-DynamicA<-DynamicB<-viewModel.dynText
对于UILable.dynText和viewModel.dynText,通常都会由controller直接或者间接强引用,所以不用担心会被释放掉。
但是对于中间产生的DynamicA,DynamicB并没有被哪个类强引用着,那么是怎么解决的呢?
最终,我们还得来看看类Bond
public class Bond<T> { public typealias Listener = T -> Void public var listener: Listener? public var bondedDynamics: [Dynamic<T>] = [] public var bondedWeakDynamics: [DynamicBox<T>] = [] public init() { } public init(_ listener: Listener) { self.listener = listener } public func bind(dynamic: Dynamic<T>) { bind(dynamic, fire: true, strongly: true) } public func bind(dynamic: Dynamic<T>, fire: Bool) { bind(dynamic, fire: fire, strongly: true) } public func bind(dynamic: Dynamic<T>, fire: Bool, strongly: Bool) { dynamic.bonds.append(BondBox(self)) if strongly { self.bondedDynamics.append(dynamic) } else { self.bondedWeakDynamics.append(DynamicBox(dynamic)) } if fire && dynamic.valid { self.listener?(dynamic.value) } } public func unbindAll() { let dynamics = bondedDynamics + bondedWeakDynamics.reduce([Dynamic<T>]()) { memo, value in if let dynamic = value.dynamic { return memo + [dynamic] } else { return memo } } for dynamic in dynamics { var bondsToKeep: [BondBox<T>] = [] for bondBox in dynamic.bonds { if let bond = bondBox.bond { if bond !== self { bondsToKeep.append(bondBox) } } } dynamic.bonds = bondsToKeep } self.bondedDynamics.removeAll(keepCapacity: true) self.bondedWeakDynamics.removeAll(keepCapacity: true) } }
我们可以看到有两个数组bondedDynamics,bondedWeakDynamics,
没错,就是由Bond来强引用的,对于上面举的那个例子。
结果就是
UILable.dynText.valueBond强引用DynamicA
DynamicA.valueBond强引用DynamicB
DynamicB.valueBond强引用viewModel.dynText
大家可能会注意到viewModel.dynText会被controller和DynamicB.valueBond两个实例强引用。
对于这个问题,我也问过作者,结论就是为了统一实现方法,并且因为没有形成引用环,只要controller释放的话,就会都释放掉的,所以没有问题。但是这个地方在实装的时候确实值得小心。
对于类Bond里的另外一个属性bondedWeakDynamics,这个是为了解决双向绑定设置的。
还是上面的例子,如果是双向绑定的话,反向引用应该是这样的关系
viewModel.dynText.valueBond弱引用DynamicB
DynamicB.valueBond弱引用DynamicA
DynamicA.valueBond弱引用UILable.dynText
当然这儿的引用到不是(也没办法)解决中间Dynamic丢失的问题。
而是为了实现Bond.unbindAll方法而保存Dynamic的。
上面说到双向绑定,那么是怎么阻止循环更新的?这就用到了Dynamic类里的dispatchInProgress变量,它在触发执行绑定的bonds时,会判断以及更新这个变量,会阻断循环更新的执行。
另外,对Dynamic的value赋值后,所有的Bond都是无条件触发的,不管这个value赋值前后是否一样。所以尽量不要在这个绑定链里放一些特别复杂的处理。
大概就是这个样子。