在 Swift 中使用 objc_getAssociatedObject, objc_setAssociatedObject 时需要注意的事项

| Swift , iOS

 

内容概览

  • 前言
  • 回顾 ObjC 中的用法
  • 在 Swift 中的用法
  • 注意事项
  • 总结

 

前言

 

objc_getAssociatedObject, objc_setAssociatedObject 是较为常用的 ObjC 运行时方法。利用这两个方法,我们可以很方便地在运行时为 NSObject 及其子类添加属性。

然而,在 Swift 中使用这两个方法的时候,我们需要注意一些细节。否则,我们就有可能会遇到一些麻烦。

如果您不了解在 ObjC 中如何使用这两个方法,Ficow 会和您一起回顾。

 

回顾 ObjC 中的用法

 

在 ObjC 中,由于 selector 是不存在副本的常量字符串,我们可以将 selector 作为 key 传入关联对象方法中:

- (void)setAssociatedObject:(id)obj {
    objc_setAssociatedObject(self, @selector(associatedObject), obj, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (id)associatedObject {
    return objc_getAssociatedObject(self, @selector(associatedObject));
}

甚至,我们还可以使用 _cmd 简化 associatedObject 这个 getter 的写法:

objc_getAssociatedObject(self, _cmd);

那么,在 Swift 中有类似的写法吗?

 

在 Swift 中的用法

 

在 Swift 中,关联对象方法要求 key 的类型是 UnsafeRawPointer,因此我们需要传入一个指针。

那么,我们就可以写出类似如下形式的代码:

private var associatedValueStringKey = ""

extension NSObject {
    var associatedValue: Any? {
        get {
            return objc_getAssociatedObject(self, &associatedValueStringKey)
        }
        set {
            objc_setAssociatedObject(self, &associatedValueStringKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
        }
    }
}

调用方代码如下:

let obj = NSObject()
obj.associatedValue = NSObject()
print(obj.associatedValue)

有人可能会问,associatedValueStringKey 变量的值不需要设置吗?

答案是:不需要。

因为,关联对象方法使用的是传入的地址,字符串的值并不影响该方法的效果。

 

注意事项

 

不要忘记 &

如果传入的 key 从 &associatedValueStringKey 变为了 associatedValueStringKey,以下代码依然可以编译,只是无法正常工作:

extension NSObject {
    var associatedValue: Any? {
        get {
            return objc_getAssociatedObject(self, associatedValueStringKey)
        }
        set {
            objc_setAssociatedObject(self, associatedValueStringKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
        }
    }
}

如果丢掉了 &,以下调用方代码输出的结果极有可能为 nil,也有可能直接导致程序 crash

let obj = NSObject()
obj.associatedValue = NSObject()
print(obj.associatedValue)

为什么会这样?

让我们在 getter 和 setter 中打印 associatedValueStringKey 的地址:

    var associatedValue: Any? {
        get {
            withUnsafePointer(to: associatedValueStringKey) { (pointer) in
                print("get", pointer)
            }
            return objc_getAssociatedObject(self, associatedValueStringKey)

        }
        set {
            withUnsafePointer(to: associatedValueStringKey) { (pointer) in
                print("set", pointer)
            }
            objc_setAssociatedObject(self, associatedValueStringKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
        }
    }

输出内容如下:

set 0x00007ffee9ea11d0
get 0x00007ffee9ea11e0

我想您已经知道原因了,这两个地址不一样!在 setter 和 getter 中,associatedValueStringKey 代表并不是您最初定义的那个 key,而是分别复制之后的 String 实例。

所以,key 不相同,也就无法获取到预期的结果!

不过,Ficow 有一个办法可以防止 & 弄丢:

    var associatedValue: Any? {
        get {
            return withUnsafeMutablePointer(to: &associatedValueStringKey) { (pointer) -> Any? in
                return objc_getAssociatedObject(self, pointer)
            }
        }
        set {
            withUnsafeMutablePointer(to: &associatedValueStringKey) { (pointer) in
                objc_setAssociatedObject(self, pointer, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
            }
        }
    }

withUnsafeMutablePointer 函数要求 to 是一个 inout 参数,这样就可以让编译器提示您:不可以弄丢 &

 

不要使用 #function 作为 key

可能有人会将 #function 作为 key 传入关联对象方法中:

    var associatedValue: Any? {
        get {
            return objc_getAssociatedObject(self, #function)
        }
        set {
            objc_setAssociatedObject(self, #function, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
        }
    }

如果您已经阅读了 Ficow 的这篇 Swift 中的 #function 到底是什么?,那么您就知道 #function 其实是一个 String 实例。

这时候,这个问题的成因就和上面的丢掉 & 是一样的。

您可以在 getter 和 setter 中输出 #function 的地址:

    var associatedValue: Any? {
        get {
            withUnsafePointer(to: #function) { (pointer) in
                print("get", pointer)
            }
            return objc_getAssociatedObject(self, #function)
        }
        set {
            withUnsafePointer(to: #function) { (pointer) in
                print("set", pointer)
            }
            objc_setAssociatedObject(self, #function, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
        }
    }

输出结果:

set 0x00007ffee12cd290
get 0x00007ffee12cd298

确实是不同的地址呢!

 

使用 Bool 或 UInt8 作为 key 可以节省内存空间

其实,我们可以把 String 类型的 key:

private var associatedValueStringKey = ""

替换为 UInt8 类型的 key:

private var associatedValueIntKey: UInt8 = 0
// 或者
private var associatedValueBoolKey = true

话不多说,请看代码:

print(MemoryLayout<String>.size) // 16
print(MemoryLayout<UInt8>.size) // 1
print(MemoryLayout<Bool>.size) // 1

内存占用量的差距还是挺明显的喔!如果一个大型项目中定义了超多的 key,这个地方的内存使用量是值得考虑的。

 

总结

 

运行时很灵活、很强大,但是有些人会选择避开它。

然而,只要掌握了运行时相关的知识,我们就可以有更多可用的解决方案。何乐而不为呢?

希望本文对您有所帮助。如有谬误,Ficow 欢迎您留言指出~

 

参考内容:
Associated Objects
Swift内存布局

 

觉得不错?点个赞呗~

本文链接:在 Swift 中使用 objc_getAssociatedObject, objc_setAssociatedObject 时需要注意的事项

转载声明:本站文章如无特别说明,皆为原创。转载请注明:Ficow Shen's Blog

评论区(期待你的留言)