前言
最近的学习中笔者发现自己对于分类、扩展相关知识并不是很熟悉,刚好看源码类的加载过程中发现有类扩展与关联对象详解。本篇我们来探索一下这部分相关知识,首先我们要记住扩展是编译时就被添加在类中,而分类是在运行时才被整合到类信息中来的。
分类
这里我们先来看看使用Clang
编译之后,分类的底层结构struct category_t
:
这里我们来看看其中的内容,根据名称我们可以发现其中存储了类指针、实例方法表、类方法表、协议表、属性列表,但是并没有类中有的成员变量表。其实看到这里我们就可以明白,我们不可以在分类中定义成员变量,原因很简单,这里面都没有成员变量表。
这里还有一个结论:分类可以声明属性,并可以生成对应的set、get方法,但没有去实现该方法
分类加载流程:
- 在编译阶段将分类中的方法、属性等编译到一个数据结构
category_t
- 将分类中的方法、属性等合并到一个大数组中去,而后参加编译的分类就会在数组前面
- 将合并后的分类数据插入到原有数据的前面
故而当分类中的方法与原始类中方法重名的时候,会先去调用分类中实现的方法。
扩展
@interface Person ()
@property (nonatomic, assign) NSInteger age; // 私有属性
- (BOOL)validateAge; // 私有方法声明
@end
这里我们将一个扩展直接使用Clang
转化位cpp文件,我们可以看到其直接被存储到了成员变量表中,同时方法也直接被添加到了metholist
中:
故而扩展是在编译阶段与该类同时编译的,是类的一部分。扩展中声明的方法只能在该类的@implementation中实现。所以这也就意味着我们无法对系统的类使用扩展。
扩展和分类的区别
类别、分类
- 专门用来给类添加新的方法
- 不能给类添加成员属性,添加了成员属性也无法取到
- 注意:其实可以通过
runtime
给分类添加属性,即属性关联
,重写setter
、getter
方法
- 注意:其实可以通过
- 分类中用
@property
定义变量,只会生成变量的setter
、getter
方法的声明
,不能生成方法实现和带下划线的成员变量
扩展
- 可以说成是
特殊的分类
,也可称作匿名分类
- 可以
给类添加成员属性
,但是是私有变量
- 可以
给类添加方法
,也是私有方法
关联对象
这里我们来讲解一下如何通过runtime
来给分类添加属性,这里主要分为两部分:
- 通过
objc_setAssociatedObject
设值流程 - 通过
objc_getAssociatedObject
取值流程
流程如上所示
我们先来看看取值流程:
objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key,
id _Nullable value, objc_AssociationPolicy policy)
- 参数一:要关联的对象,即为谁添加关联属性
- 参数二:标识符,方便下次查找
- 参数三:value
- 参数四:属性的策略,即
nonatomic、atomic、assign
等,下面展示一下所有关联对象的属性类型:
下面我们来看看objc_setAssociatedObject
的源码实现:
下面我们进入_object_set_associative_reference
源码实现来看看:
void
_object_set_associative_reference(id object, const void *key, id value, uintptr_t policy)
{
// This code used to work when nil was passed for object and key. Some code
// probably relies on that to not crash. Check and handle it explicitly.
// rdar://problem/44094390
if (!object && !value) return;
if (object->getIsa()->forbidsAssociatedObjects())
_objc_fatal("objc_setAssociatedObject called on instance (%p) of class %s which does not allow associated objects", object, object_getClassName(object));
//object封装成一个数组结构类型,类型为DisguisedPtr
DisguisedPtr<objc_object> disguised{(objc_object *)object};//相当于包装了一下 对象object,便于使用
// 包装一下 policy - value
ObjcAssociation association{policy, value};
// retain the new value (if any) outside the lock.
association.acquireValue();//根据策略类型进行处理
bool isFirstAssociation = false;
{
//初始化manager变量,相当于自动调用AssociationsManager的析构函数进行初始化
AssociationsManager manager;//并不是全场唯一,构造函数中加锁只是为了避免重复创建,在这里是可以初始化多个AssociationsManager变量的
AssociationsHashMap &associations(manager.get());//AssociationsHashMap 全场唯一
if (value) {
auto refs_result = associations.try_emplace(disguised, ObjectAssociationMap{});//返回结构为一个类对
if (refs_result.second) {//判断第二个存不存在,即bool值是否为true
/* it's the first association we make */
isFirstAssociation = true;
}
/* establish or replace the association */
auto &refs = refs_result.first->second;//得到一个空的桶子,找到引用对象类型,即第一个元素的second值
auto result = refs.try_emplace(key, std::move(association));//查找当前的key是否有association关联对象
if (!result.second) {//如果结果不存在
association.swap(result.first->second);
}
} else {//如果传的是空值,则移除关联,相当于移除
auto refs_it = associations.find(disguised);
if (refs_it != associations.end()) {
auto &refs = refs_it->second;
auto it = refs.find(key);
if (it != refs.end()) {
association.swap(it->second);
refs.erase(it);
if (refs.size() == 0) {
associations.erase(refs_it);
}
}
}
}
}
// Call setHasAssociatedObjects outside the lock, since this
// will call the object's _noteAssociatedObjects method if it
// has one, and this may trigger +initialize which might do
// arbitrary stuff, including setting more associated objects.
if (isFirstAssociation)
object->setHasAssociatedObjects();
// release the old value (outside of the lock).
association.releaseHeldValue();//释放
}
我们来看看这段源码的实现过程:
- 首先检查对象所属类是否禁止关联对象(系统类就不可以),若禁止则直接触发崩溃
- 创建一个全局管理关联对象的
AssociationsManager
管理类,并获取唯一的全局静态哈希Map:AssociationsHashMap
- value是否存在:
- 若存在,通过
try_emplace
方法,创建一个空的ObjectAssociationMap
去取查询键值对 - 如果发现没有这个
key
就插入一个空的BucketT
进去并返回true
- 通过
setHasAssociatedObjects
方法标记对象存在关联对象
即置isa
指针的has_assoc
属性为true
- 用当前
policy 和 value
组成了一个ObjcAssociation
替换原来BucketT
中的空 - 标记一下
ObjectAssociationMap
的第一次为false
AssociationsManager
我们先来看看其源码实现
这里我们可以看到AssociationsHashMap
从静态变量中取出,所以全场唯一
下面我们来看看这AssociationsHashMap
以及ObjectAssociationMap
的定义
这里先说一下
DenseMap
,这个东西时LLVM实现的高性能哈希表,支持快速插入、查找、删除(笔者具体也不会)
- 先来看看
ObjectAssociationMap
,他对应的是一个对象的关联属性集合,通过健快速定位到具体的ObjcAssociation
。
这里展示一下该结构体内部包含的内容:关联值的引用计数策略与实际值
- 再来看看
AssociationsHashMap
,这是一个全局管理所有对象的关联属性的集合,这里键为伪装指针(DisguisedPtr
),值为该对象关联属性表ObjectAssociationMap
这里附一张图来讲解这几个表之间的关系
下面来说一下这几个map之间的联系与不同:
AssociationsManager
可以有很多个,但是AssociationsHashMap
类型的map只能有一个,是通过AssociationsManager
来获取的- 这个map中有很多个
ObjectAssociationMap
类型的map,在上文中的讲解中,我们可以明白每个对象都有一个ObjcAssociation
,所以每个对象都会有一个自己的ObjectAssociationMap
类型的map
key的几种用法
- 使用的get方法的@selector作为key
objc_setAssociatedObject(obj, @selector(getter), value, OBJC_ASSOCIATION_RETAIN_NONATOMIC)
objc_getAssociatedObject(obj, @selector(getter))
- 使用指针的地址作为key
static void *MyKey = &MyKey;
objc_setAssociatedObject(obj, MyKey, value, OBJC_ASSOCIATION_RETAIN_NONATOMIC)
objc_getAssociatedObject(obj, MyKey)
- 使用static作为key
static char MyKey;
objc_setAssociatedObject(obj, &MyKey, value, OBJC_ASSOCIATION_RETAIN_NONATOMIC)
objc_getAssociatedObject(obj, &MyKey)
- 使用属性名作为key
objc_setAssociatedObject(obj, @“property”, value, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
objc_getAssociatedObject(obj, @“property”);
流程
设置关联对象:
调用
objc_setAssociatedObject
AssociationsManager
查找或创建与目标对象相关的ObjectAssociationMap
在
ObjectAssociationMap
中查找或创建对应的ObjcAssociation
。将关联值和存储策略设置到
ObjcAssociation
中。
获取关联对象:
调用
objc_setAssociatedObject
AssociationsManager
查找与目标对象相关的ObjectAssociationMap
在
ObjectAssociationMap
中查找对应的ObjcAssociation
。返回
ObjcAssociation
中存储的关联值
移除关联对象:
- 调用
objc_removeAssociatedObjects
或objc_setAssociatedObject
设置为 nil。 AssociationsManager
查找与目标对象相关的ObjectAssociationMap
。- 从
ObjectAssociationMap
中移除对应的`` ObjcAssociation`。 - 如果
ObjectAssociationMap
为空,可能会移除整个映射以释放资源。
- 调用
这个流程其实也就是上文中_object_set_associative_reference
的流程,笔者认为这样理解更好一些,下面再附一张图帮助理解
总结
关联对象就是一个二层哈希的处理,存取的时候都是两层处理,类似于二维数组:
在这里插入图片描述](https://i-blog.csdnimg.cn/direct/f49187f493b9464e990aa734088ed9d9.png)