如何为分类添加属性

Runtime 原理

1
2
3
4
5
6
7
8
9
struct category_t { 
const char *name; // 原类名,而不是分类名
// 要扩展的类对象,编译期间是不会定义的,而是在 Runtime 阶段通过 name 对应到对应的类对象
classref_t cls;
struct method_list_t *instanceMethods; // 分类中新增的对象方法列表
struct method_list_t *classMethods; // 分类中新增的类方法列表
struct protocol_list_t *protocols; // 分类中新增的协议列表
struct property_list_t *instanceProperties; // 分类中新增的属性列表
};

可以看出,分类中可以添加实例方法,类方法,甚至可以实现协议,添加属性,但不可以添加成员变量

instanceProperties 的存在是我们可以通过 objc_setAssociatedObjectobjc_getAssociatedObject 向分类中增加实例变量的原因,不过这个和一般的实例变量是不一样的

所以,Category 可以使用 @property,但不会生成带下划线的成员变量,也不会生成 getter 和 setter(@property 只是帮助声明了 setter/getter,并没有提供实现)。我们可以使用 Runtime 为已有的类添加新的属性并生成 getter 和 setter 方法

语法

1
2
3
4
5
6
void objc_setAssociatedObject (id object, void *key, id value, objc_AssociationPolicy policy);

id objc_getAssociatedObject(id object, void *key);

void objc_removeAssociatedObject(id object); // 移除 object 上的所有关联对象

参数说明

  • id object:被关联的对象(一般为 self)
  • const void *key:关联的key,要求唯一,因此避免使用 @””(一般为新增属性的 getter)
  • id value:关联的对象(一般为新增的属性)
  • objc_AssociationPolicy policy:内存管理的策略

key

关联的 key 值有三种推荐值

  1. 声明 static char kAssociatedObjectKey;,使用 &kAssociatedObjectKey 作为 key 值
  2. 声明 static void *kAssociatedObjectKey = &kAssociatedObjectKey; ,使用 kAssociatedObjectKey 作为 key 值
  3. 用 selector ,使用 getter 方法的名称作为 key 值(推荐)

设置关联对象值时,若想令两个健匹配到相同的一个值,则二者必须是完全相同的指针才行。

所以 key 值最好定义为一个全局静态变量,而不能每次都用 @”xxx”

推荐使用 selector,因为这种方法省略了声明参数的代码,并且能很好地保证 key 的唯一性

内存管理策略

其中 objc_AssociationPolicy 是关联对象的属性,如下

1
2
3
4
5
OBJC_ASSOCIATION_ASSIGN             --- assign
OBJC_ASSOCIATION_RETAIN_NONATOMIC --- nonatomic, retain
OBJC_ASSOCIATION_COPY_NONATOMIC --- nonatomic, copy
OBJC_ASSOCIATION_RETAIN --- retain(strong)
OBJC_ASSOCIATION_COPY --- copy

例子

1
2
3
@interface UIView (VN_ShortCut)
@property (nonatomic, weak) UICollectionView *vn_cellCollectionView;
@end
1
2
3
4
5
6
7
8
9
10
11
@implementation UIView (VN_ShortCut)
- (UICollectionView *)vn_cellCollectionView
{
return objc_getAssociatedObject(self, @selector(vn_cellCollectionView));
}

- (void)setVn_cellCollectionView:(UICollectionView *)cellCollectionView
{
objc_setAssociatedObject(self, @selector(vn_cellCollectionView), cellCollectionView, OBJC_ASSOCIATION_ASSIGN);
}
@end

为什么内存管理策略中没有 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
2
3
4
5
6
7
8
9
10
// 情况 1,weak 变量
__weak typeof(self) weakSelf = self; // weakSelf 是指向原对象的指针,会被存进 weak 表
// 情况 2,weak 属性
@property (nonatomic, weak) id delegate;
// 本质是转换为 setter
- (void)setDelegate:(id)delegate {
if (_delegate != delegate) {
objc_initWeak(&_delegate, delegate);
}
}

p.s. 注意这篇文章的解释是错的

如何实现 weak 属性

注意 OBJC_ASSOCIATION_ASSIGN 的作用是 assign 而不是 weak,所以当关联的对象被释放的时候并不会被自动置为 nil,因此获取到的对象将会是一个野指针。

直观的方法 1

如果要实现 weak 的效果,解决方法是新建一个替身,weak 引用住该对象,然后使用 OBJC_ASSOCIATION_RETAIN_NONATOMIC 存储该替身。

以上面的例子为例,代码如下

1
2
3
4
5
6
7
8
9
10
11
- (UICollectionView *)vn_cellCollectionView
{
QVNWeakProxy *proxy = objc_getAssociatedObject(self, @selector(vn_cellCollectionView));
return (UICollectionView *)proxy.target;
}

- (void)setVn_cellCollectionView:(UICollectionView *)cellCollectionView
{
QVNWeakProxy *proxy = [[QVNWeakProxy alloc] initWithTarget:cellCollectionView];
objc_setAssociatedObject(self, @selector(vn_cellCollectionView), proxy, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

其中 QVNWeakProxy 只要继承自 NSObject 并拥有一个 weak 的 target 的属性即可

优雅的方法 2

变量用 __weak 修饰,因此被 block 捕获的时候不会增加引用计数;block 使用 copy 修饰,可以将栈 block 转为堆 block,防止被释放

1
2
3
4
5
6
7
8
9
10
11
12
- (UICollectionView *)vn_cellCollectionView
{
id (^block)(void) = objc_getAssociatedObject(self, @selector(vn_cellCollectionView));
return (block ? block() : nil);
}

- (void)setVn_cellCollectionView:(UICollectionView *)cellCollectionView
{
id __weak weakObject = cellCollectionView;
id (^block)(void) = ^{return weakObject;};
objc_setAssociatedObject(self, @selector(vn_cellCollectionView), block, OBJC_ASSOCIATION_COPY);
}
  1. Runtime 原理
  2. 语法
    1. key
    2. 内存管理策略
  3. 例子
  4. 为什么内存管理策略中没有 weak 选项,即 OBJC_ASSOCIATION_WEAK
  5. 如何实现 weak 属性
    1. 直观的方法 1
    2. 优雅的方法 2