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赋值前后是否一样。所以尽量不要在这个绑定链里放一些特别复杂的处理。

大概就是这个样子。