学习MJ的视频课程,整理总结知识点–关联对象(Associated)详解

本文通过runtime中objc_setAssociatedObject源码为切入点,同时结合category的底层结构(struct category_t)、对象的底层结构(objc_object),类的底层结构(objc_class)来讲解OC中的对象关联对象。主要回答:1、为什么分类不能添加属性(根本原因)、2、分类中怎样才能添加属性(关联对象)、3、关联对象的原理。

分类为什么不能添加属性

最初学习OC编程的时候,已经知道category中不能直接添加属性,当时很好奇为什么category这么方便的东西不能直接使用属性,后来查资料知道不能直接添加属性是因为底层结构设计导致不允许。当时也只是停留在概念阶段,对于底层原因不够明了,这次通过较系统的学习,有了一定的知识储备后,再来讲解分类为什么不能添加属性。

结合我之前的Category的本质文章,我们已经知道category的底层结构是struct category_t,它在编译阶段是结构体,在运行时,runtime会把category的结构体里面的信息加载到对应的class中去。同时,我们也知道struct category_t的结构体有保存的主要有实例方法列表instanceMethods、类方法列表classMethods、协议列表protocols、实例属性列表instanceProperties、类属性列表_classProperties等。但没有成员成员变量列表ivars。这就直接表明category中没有成员变量这个东西。所以即使利用@property关键字给分类增加属性,也只能增加set方法和get方法的声明,不会有实现,也会有对应的成员变量。

真实因为category的结构体中不能保存成员变量,就决定了在OC中category是没有成员变量的,这是直接原因。

下面,我们思考下category不能直接添加属性,根本原因是什么?为什么在设计category底层结构时不增加一个类似成员变量的东西? 下面我结合之前的文章,分析并解答。

结合我之前的OC对象的本质这篇文章可以知道,成员变量在内存中是存储在OC对象内部的,这一点很重要。而方法列表、属性列表、协议列表是存储在类的内部。同时,OC对象的大小和内存布局是在编译完后就决定的了。而category存储的信息是在运行时才添加到class上,但此时,对象的大小和 内存布局已确定,假如category中有成员变量,假如成员变量能添加到对象里,那么对象的结构和大小肯定要变化,两者相悖。所以category中的是不能有成员变量的。

毕竟category是Objective-C 2.0之后添加的语言特性,category的主要作用是为已经存在的类添加方法。同时,OC是基于C语言这门静态语言的扩展,虽然OC有小巧的runtime实现动态性,但它并不完全是一门动态语言。

在语言设计上,OC在1.0版本时,更多的是在C语言这门静态语言的扩展,然后加上Smalltalk的消息机制。在OC2.0时,优化了runtime,但是category的设计目的是为类增加方法,加上C语言这个底子及OC1.0设计时的历史原因,OC2.0中category增加成员变量比较困难,同时重要性不是很高,所以category不能直接成员变量。但是提供了 第三方管理者 来实现category间接添加属性。

这部分在objc源码中也能有所体现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static Class realizeClassWithoutSwift(Class cls, Class previously)
{
auto ro = (const class_ro_t *)cls->data();
auto isMeta = ro->flags & RO_META;
if (ro->flags & RO_FUTURE) {
// This was a future class. rw data is already allocated.
rw = cls->data();
ro = cls->data()->ro();
ASSERT(!isMeta);
cls->changeInfo(RW_REALIZED|RW_REALIZING, RW_FUTURE);
} else {
// Normal class. Allocate writeable class data.
rw = objc::zalloc<class_rw_t>();
rw->set_ro(ro);
rw->flags = RW_REALIZED|RW_REALIZING|isMeta;
cls->setData(rw);
}

类的方法、属性等在编译期保存在data()的位置,在运行期,直到realizeClassWithoutSwift执行之后,才放到了class_rw_t指向的只读区域const class_ro_t

ps:这块内容主要是自己结合源码分析进行的总结,有错误或不严谨的地方请指出

分类怎样才能添加属性

我们已经知道了分类不能添加属性的直接原因和根本,但是我们在使用category时,很多时候还是需要用属性的set和get方法的。接下来我们探讨分类中间接添加属性的方式。

默认情况下,因为分类底层结构的限制,不能添加成员变量到分类中。但可以通过关联对象来间接实现

关联对象提供了以下API

1
2
3
4
5
6
7
8
// 添加关联对象
void objc_setAssociatedObject(id object, const void * key, id value, objc_AssociationPolicy policy)

// 获得关联对象
id objc_getAssociatedObject(id object, const void * key)

// 移除所有的关联对象
void objc_removeAssociatedObjects(id object)

key的常见用法

用于关联对象的keyconst void * key,说明只要求是个指向常量的指针,使用static时为了限制key的作用域。

1
2
3
static void *MyKey = &MyKey;
objc_setAssociatedObject(obj, MyKey, value, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
objc_getAssociatedObject(obj, MyKey);
1
2
3
static char MyKey;
objc_setAssociatedObject(obj, &MyKey, value, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
objc_getAssociatedObject(obj, &MyKey);
1
2
3
// 使用属性名作为key
bjc_setAssociatedObject(obj, @"property", value, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
objc_getAssociatedObject(obj, @"property");
1
2
3
// 使用get方法的@selecor作为key
objc_setAssociatedObject(obj, @selector(getter), value, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
objc_getAssociatedObject(obj, @selector(getter));

这里更推荐最后一种写法,原因如下

  1. 明了是为哪个属性进行的绑定,比较接近属性set和get
  2. 不用额外定义key
  3. 自带语法提示

关联对象的原理

关联对象是runtime的一部分,所以我们直接看源码即可

1
2
3
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy) {
_object_set_associative_reference(object, (void *)key, value, policy);
}

设置关联对象的核心源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
void _object_set_associative_reference(id object, void *key, id value, uintptr_t policy) {
// retain the new value (if any) outside the lock.
ObjcAssociation old_association(0, nil);
id new_value = value ? acquireValue(value, policy) : nil;
{
AssociationsManager manager;
AssociationsHashMap &associations(manager.associations());
disguised_ptr_t disguised_object = DISGUISE(object);
if (new_value) {
// break any existing association.
AssociationsHashMap::iterator i = associations.find(disguised_object);
if (i != associations.end()) {
// secondary table exists
ObjectAssociationMap *refs = i->second;
ObjectAssociationMap::iterator j = refs->find(key);
if (j != refs->end()) {
old_association = j->second;
j->second = ObjcAssociation(policy, new_value);
} else {
(*refs)[key] = ObjcAssociation(policy, new_value);
}
} else {
// create the new association (first time).
ObjectAssociationMap *refs = new ObjectAssociationMap;
associations[disguised_object] = refs;
(*refs)[key] = ObjcAssociation(policy, new_value);
object->setHasAssociatedObjects();
}
} else {
// setting the association to nil breaks the association.
AssociationsHashMap::iterator i = associations.find(disguised_object);
if (i != associations.end()) {
ObjectAssociationMap *refs = i->second;
ObjectAssociationMap::iterator j = refs->find(key);
if (j != refs->end()) {
old_association = j->second;
refs->erase(j);
}
}
}
}
// release the old value (outside of the lock).
if (old_association.hasValue()) ReleaseValue()(old_association);
}

我们可以总结如下流程
关联对象
得出如下结论:

  1. 关联对象存储在全局的统一的一个AssociationsManager中
  2. AssociationsManager里面有一个_map,管理每个对象的对象关联ObjectAssociationMap
  3. ObjectAssociationMap的key保存的就是外界传递进来的参数key,value保存的就是关联值,里面包含policyvalue
  4. 设置关联对象为nil,就相当于是移除关联对象
  5. AssociationsManager添加了锁,并自己做旧值的释放

拓展

至于protocol,我们看它的源码

1
2
3
4
5
6
7
8
9
10
11
struct protocol_t : objc_object {
const char *mangledName;
struct protocol_list_t *protocols;
method_list_t *instanceMethods;
method_list_t *classMethods;
method_list_t *optionalInstanceMethods;
method_list_t *optionalClassMethods;
property_list_t *instanceProperties;
uint32_t size; // sizeof(protocol_t)
uint32_t flags;
};

通过源码可知,protocol的结构有一点类似category。
而且,protocol也是没有成员变量列表的,有getter、setter方法的声明,但是没有方法的实现。

总结

  1. category不能直接添加属性的原因
    直接原因:category内部没有用于存储ivar的地方
    根本原因:

    1. OC是最初是基于C语言+Smalltalk,category是OC2.0才有的特性,当时的语言发展史已经决定了类在编译阶段就决定了对象的大小和内存布局。而且OC对象的成员变量是保存在对象中的。
    2. category的设计初衷是为类添加方法。
      这两个条件决定了为category添加变量的需求不是很高,相对于对OC底层大幅改动从而实现在category中直接添加变量,语言的设计者采取第三方管理者(关联对象)来间接实现添加成员变量
  2. 关联对象使用
    个人建议使用@selector(getter)作为关联对象的key

  3. 关联对象的原理
    使用一个全局的AssociationsManager,它的内部有一个map,map的key对应要关联的对象,value保存的是ObjectAssociationMap,这个ObjectAssociationMap关联key和值信息。
    这一部分看上面总结的图会更清晰

参考和源码

Apple Source Browser - objc

评论