objc_getAssociatedObject
, objc_setAssociatedObject
是较为常用的 ObjC 运行时方法。利用这两个方法,我们可以很方便地在运行时为 NSObject
及其子类添加属性。
然而,在 Swift 中使用这两个方法的时候,我们需要注意一些细节。否则,我们就有可能会遇到一些麻烦。
如果您不了解在 ObjC 中如何使用这两个方法,Ficow 会和您一起回顾。
在 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 中,关联对象方法要求 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
确实是不同的地址呢!
其实,我们可以把 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