Runtime 原理
1 | struct category_t { |
可以看出,分类中可以添加实例方法,类方法,甚至可以实现协议,添加属性,但不可以添加成员变量
instanceProperties 的存在是我们可以通过 objc_setAssociatedObject
和 objc_getAssociatedObject
向分类中增加实例变量的原因,不过这个和一般的实例变量是不一样的
所以,Category 可以使用 @property
,但不会生成带下划线的成员变量,也不会生成 getter 和 setter(@property
只是帮助声明了 setter/getter,并没有提供实现)。我们可以使用 Runtime 为已有的类添加新的属性并生成 getter 和 setter 方法
语法
1 | void objc_setAssociatedObject (id object, void *key, id value, objc_AssociationPolicy policy); |
参数说明
- id object:被关联的对象(一般为 self)
- const void
*key
:关联的key,要求唯一,因此避免使用 @””(一般为新增属性的 getter) - id value:关联的对象(一般为新增的属性)
- objc_AssociationPolicy policy:内存管理的策略
key
关联的 key 值有三种推荐值
- 声明
static char kAssociatedObjectKey;
,使用 &kAssociatedObjectKey 作为 key 值 - 声明
static void *kAssociatedObjectKey = &kAssociatedObjectKey;
,使用 kAssociatedObjectKey 作为 key 值 - 用 selector ,使用 getter 方法的名称作为 key 值(推荐)
设置关联对象值时,若想令两个健匹配到相同的一个值,则二者必须是完全相同的指针才行。
所以 key 值最好定义为一个全局静态变量,而不能每次都用 @”xxx”
推荐使用 selector,因为这种方法省略了声明参数的代码,并且能很好地保证 key 的唯一性
内存管理策略
其中 objc_AssociationPolicy 是关联对象的属性,如下
1 | OBJC_ASSOCIATION_ASSIGN --- assign |
例子
1 | @interface UIView (VN_ShortCut) |
1 | @implementation UIView (VN_ShortCut) |
为什么内存管理策略中没有 weak 选项,即 OBJC_ASSOCIATION_WEAK
如果真的有 weak 选项,我们期望的结果是当被关联对象被释放之后,从关联对象身上取出的“属性”是 nil
首先我们要搞懂 weak 属性的实现原理,简单来说,Runtime 在底层维护一个全局的 weak 表,每次当一个 weak 指针被赋值对象的时候,会将对象地址和 weak 指针地址注册到 weak 表中,其中对象地址作为 key;当对象被废弃时,可根据对象地址快速寻找到指向它的所有 weak 指针,并将 weak 指针置为 nil,同时移出 weak 表
所以,实现 weak 的前提是存在一个 weak 指针指向到被引用对象的地址,而通过对以上源码的研究,我们可以知道关联对象和被关联对象之间并没有这样一个 weak 指针,因此无法实现 OBJC_ASSOCIATION_WEAK
更具体的,如下代码,一个 weak 指针或属性,都会在编译时就变转化成 objc_initWeak
,这样运行时才能正确往 weak 表里面添加变量。但是关联对象并没有实例变量,所以不能实现 weak
1 | // 情况 1,weak 变量 |
p.s. 注意这篇文章的解释是错的
如何实现 weak 属性
注意 OBJC_ASSOCIATION_ASSIGN
的作用是 assign 而不是 weak,所以当关联的对象被释放的时候并不会被自动置为 nil,因此获取到的对象将会是一个野指针。
直观的方法 1
如果要实现 weak 的效果,解决方法是新建一个替身,weak 引用住该对象,然后使用 OBJC_ASSOCIATION_RETAIN_NONATOMIC
存储该替身。
以上面的例子为例,代码如下
1 | - (UICollectionView *)vn_cellCollectionView |
其中 QVNWeakProxy 只要继承自 NSObject 并拥有一个 weak 的 target 的属性即可
优雅的方法 2
变量用 __weak
修饰,因此被 block 捕获的时候不会增加引用计数;block 使用 copy 修饰,可以将栈 block 转为堆 block,防止被释放
1 | - (UICollectionView *)vn_cellCollectionView |