Swift在扩展中关联对象

1,563 阅读4分钟

Objective-C 最让人诟病的也许就是不能给已有类添加属性, 但是可以通过 Objective-C 的运行时机制关联自定义属性到对象上, 几乎弥补了这个痛点.

Swift Extension 比 Objective-C Category 增色不少, extension 能够给已有类添加计算型属性, 这已经是很大的进步, 但是仍然不能添加存储属性. Swift 中也可以使用 Objective-C runtime 的关联对象(Associated Objects)的方式添加属性, 弥补这一痛点.

关联对象(Associated Objects)

Swift 中提供三个与 Objective-C 类似的方法将自定义的属性关联到对象上:

  1. objc_setAssociatedObject
  2. objc_getAssociatedObject
  3. objc_removeAssociatedObjects

注意: 使用 objc_removeAssociatedObjects 时要小心, 这个方法会删除对象关联的所有属性, 就可能导致把别人添加的关联属性也删掉. 如果要删除某一个属性, 使用 objc_setAssociatedObject 方法, value 置为 nil.

下面给 UIView 添加三种不同类型的属性: isShow, displayName, width.

extension UIView {
    // 嵌套结构体
    private struct AssociatedKeys {
        static var isShowKey = "isShowKey"
        static var displayNameKey = "displayNameKey"
        static var widthKey = "widthKey"
    }
    
    // Bool 类型
    var isShow: Bool {
        get {
            return objc_getAssociatedObject(self, &AssociatedKeys.isShowKey) as! Bool
        }
        set {
            objc_setAssociatedObject(self, &AssociatedKeys.isShowKey, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
        }
    }
    
    // String 类型
    var displayName: String? {
        get {
            return objc_getAssociatedObject(self, &AssociatedKeys.displayNameKey) as? String
        }
        set {
            objc_setAssociatedObject(self, &AssociatedKeys.displayNameKey, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_COPY_NONATOMIC)
        }
    }
    
    // Float 类型
    var width: Float {
        get {
            return objc_getAssociatedObject(self, &AssociatedKeys.widthKey) as! Float
        }
        set {
            objc_setAssociatedObject(self, &AssociatedKeys.widthKey, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
        }
    }
}

上述列子说明几点:

  • 嵌套私有结构体, 声明与扩展属性对应的键(key). Swift Extension 提供了丰富的功能, 可以在 Extension 中嵌套类型, 使用 private 私有访问控制, 不会污染整个命名空间, 而且能够统一管理关联对象键.
  • Swift 的基本类型Int, Float, Double, Bool能够自动隐式地转换成 Objective-C 的 NSNumber 类型, 所以不需要显示的包装成 NSNumber 类型进行关联.
  • 如果使用 OBJC_ASSOCIATION_ASSIGN 关联策略时要注意, 文档中指出是弱引用, 但不完全等同于 weak, 更像是 unsafe_unretained 引用, 关联对象被释放后,关联属性仍然保留被释放的地址, 如果不小心访问关联属性, 就会造成野指针访问出错.

Specifies a weak reference to the associated object.

抽取关联对象方法

我们可以把关联对象的方法提取成公共方法, 在 NSObject 类的 extension 里实现, 只要继承自 NSObject 的类就能够调用关联对象方法, 通过Swift 泛型来关联不同类型的属性.

extension NSObject {
    func setAssociated<T>(value: T, associatedKey: UnsafeRawPointer, policy: objc_AssociationPolicy = objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC) -> Void {
        objc_setAssociatedObject(self, associatedKey, value, policy)
    }
    
    func getAssociated<T>(associatedKey: UnsafeRawPointer) -> T? {
        let value = objc_getAssociatedObject(self, associatedKey) as? T
        return value;
    }
}

我们只需要在 UIView+Extension.swift 中调动上面两个方法即可, 目前只支持有可选类型的属性.

extension UIView {
    private struct AssociatedKeys {
        static var displayNameKey = "displayNameKey"
    }
    
    var displayName: String? {
        get {
            return getAssociated(associatedKey: &AssociatedKeys.displayNameKey)
        }
        set {
            setAssociated(value: newValue, associatedKey: &AssociatedKeys.displayNameKey)
        }
    }
}   

关联闭包属性

开发中有时会给已有类关联闭包属性, 比如给 UIViewController 类添加一个 pushCompletion 的闭包属性, 当导航控制器 push 动作完成后调用该控制器的 pushCompletion 闭包.
先按照最基本的方式来关联对象, 如下:

typealias pushCompletionClosure = ()->()

extension UIViewController {
    private struct AssociatedKeys {
        static var pushCompletionKey = "pushCompletionKey"
    }
    
    var pushCompletion: pushCompletionClosure? {
        get {
            return objc_getAssociatedObject(self, &AssociatedKeys.pushCompletionKey) as? pushCompletionClosure
        }
        set {
            objc_setAssociatedObject(self, &AssociatedKeys.pushCompletionKey, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_COPY_NONATOMIC)
        }
    }
}

开开心心编译一发, 发现编译报错:

关联闭包报错
定位问题发现出在 objc_setAssociatedObject 这个方法上. 原来闭包属性需要包装一下才能进行关联, 下面给出两种解决办法:

  1. 使用泛型包装闭包属性, 利用 NSObject+Extension.swift 中的 setAssociated 方法来关联闭包.
  2. 创建私有闭包容器类, 利用闭包容器间接关联闭包属性.

泛型包装闭包属性

setAssociated 方法需要泛型参数, 当传入闭包后, 就会把闭包包装成泛型.

set {
   setAssociated(value: newValue, associatedKey: &AssociatedKeys.pushCompletionKey)
}

闭包容器

使用闭包容器的方式关联闭包属性, 过程分为两步:

  1. 在 extension 中嵌套创建容器类, 容器类中定义需要关联的闭包属性.
  2. 关联对象时把容器类对象关联到已有类, 间接的就把闭包属性关联到已有类.

闭包容器的方式是把闭包属性包装到了容器中, 再把容器对象关联到已有类上, 跟泛型包装闭包有异曲同工之处, 因此必须通过容器对象来访问闭包, 如果需要给类关联的闭包属性相对较多, 这种方式也不失为一种好方法, 能统一管理闭包属性, 代码层级结构也比较清晰.

typealias pushCompletionClosure = ()->()

extension UIViewController {
    private struct AssociatedKeys {
        static var pushCompletionKey = "pushCompletionKey"
    }
    
    // 嵌套闭包容器类
    class closureContainer {
        var pushCompletion: pushCompletionClosure?
    }
    
    // 关联容器属性
    var container: closureContainer? {
        get {
            return objc_getAssociatedObject(self, &AssociatedKeys.pushCompletionKey) as? closureContainer
        }
        set {
            objc_setAssociatedObject(self, &AssociatedKeys.pushCompletionKey, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
        }
    }
}

代码在这里:
github

欢迎大家留言斧正!

参考链接:
swift.gg/2016/10/11/… stackoverflow.com/questions/2…