在 iOS底层系列17 -- 分类的加载机制 探讨了分类category的加载机制,本章来阐述扩展Extension,首先来比较一下两种之间的异同点;
category分类
- 可以给主类添加方法,分类的方法在
运行时
会加载进入主类的class_rw_ext
中; - 可以给主类添加属性,但属性不会生成
setter
,getter方法
和带下划线的成员变量
,必须借助Runtime的关联即属性关联,重写setter,getter方法; - 给主类添加的属性,是不会添加到主类中的,而是通过RunTime的关联维护在一个
静态全局的HashMap中
; - 不能添加成员变量;
Extension扩展
- 可以看成是一种特殊的分类,也称作匿名分类;
- 可以给主类添加方法,但是是私有方法;
- 可以给主类添加属性,能生成成员变量,但是是私有的成员变量;
Extension的创建
- Extension的创建通常有两种方式:
- 第一种:直接在主类的.m文件中书写,写在类的实现之前,如下图所示
第二种:通过 command+N 新建 -> Objective-C File -> 选择Extension
如下所示:
Extension的底层探索
- 首先针对第一种情况创建的Extension,使用Clang命令将YYPerson.m转成YYPerson.cpp,cd至目标文件夹,终端输入
clang -rewrite-objc YYPerson.m -o YYPerson.cpp
,得到的文件如下所示:
可以看出YYPerson 类扩展的方法,在编译过程中,方法就直接添加到了class_ro_t结构体中,作为类的一部分;
然后针对第二种情况创建的Extension,在objc源码工程中运行,在
readClass
函数中加入测试代码,并打下断点;
- 断点断住之后,LLDB调试结果如下所示:
再次证明
类的扩展方法,在编译期时就已经加载到主类的class_ro_t结构体中,与主类合并
-
总结:
- 类的扩展 在编译期时 会作为类的一部分,和类一起编译,加载到类的class_ro_t结构体中;
- 类的扩展只是声明,依赖于当前的主类,没有.m文件,可以理解为一个·h文件;
分类关联对象AssociatedObject
- 上面说了分类中添加的属性,需要借助
关联
重写属性的setter,getter方法,才能访问修改属性值,下面来探索关联的底层原理; - 准备工作:创建分类,定义属性,使用关联重写属性的setter,getter方法,外界调用;
setter方法的设值流程
- 调用
objc_setAssociatedObject
函数,外界初始化YYPerson实例对象,然后设置second_name
属性,来到分类YYPerson+Add_Category
中的setter方法;
-
可以看到
objc_setAssociatedObject
函数有四个参数:- 参数1 -- Object:要关联的类对象Object,即给谁添加关联属性;
- 参数2 -- key:属性对应的标识符字符串,方便下次查找,setter与getter方法中是对应的;
- 参数3 -- value :属性值;
- 参数4 -- policy:关联属性的策略,即nonatomic、atomic、assign等;
-
关联属性到对象在底层中的实现主要涉及到四个核心类分别为:
-
AssociationsManager
:关联管理者,提供一个AssociationsHashMap
对象; -
AssociationsHashMap
:用来存储关联属性到对象的一个全局的HashMap -
ObjcAssociationMap
:用来存储的HashMap; -
ObjcAssociation
: 封装了value与policy的对象
-
下面提供一张图表示四者之间的关系:
- 进入
objc_setAssociatedObject
的底层实现如下:
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
{
SetAssocHook.get()(object, key, value, policy);
}
- 点击进入
get()
的方法实现:
- 点击进入
SetAssocHook
的方法实现:
返回值是
ChainedHookFunction
类型;上面两者结合调用可以理解成
SetAssocHook.get()
等价于_base_objc_setAssociatedObject
;在
_base_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));
DisguisedPtr disguised{(objc_object *)object};
ObjcAssociation association{policy, value};
// retain the new value (if any) outside the lock.
association.acquireValue();
bool isFirstAssociation = false;
{
AssociationsManager manager;
AssociationsHashMap &associations(manager.get());
if (value) {
//在全局AssociationsHashMap中 获取object对应的ObjectAssociationMap
auto refs_result = associations.try_emplace(disguised, ObjectAssociationMap{});
if (refs_result.second) {//yes
/* it's the first association we make */
//object对象 第一次创建 associationMap
isFirstAssociation = true;
}
//获取到ObjcAssociationMap
auto &refs = refs_result.first->second;
//在ObjcAssociationMap中 获取key对应的ObjectAssociation
//key对应的ObjectAssociation 不存在 直接插入
//key对应的ObjectAssociation 存在 直接覆盖
auto result = refs.try_emplace(key, std::move(association));
if (!result.second) {//no 已经存在
association.swap(result.first->second);
}
} else {
//在AssociationsHashMap中 获取 object对应的ObjectAssociationMap
auto refs_it = associations.find(disguised);
if (refs_it != associations.end()) {
auto &refs = refs_it->second;
//在ObjcAssociationMap中 获取key对应的ObjectAssociation
auto it = refs.find(key);
if (it != refs.end()) {
association.swap(it->second);
//将ObjectAssociation从ObjectAssociationMap中抹除
refs.erase(it);
//若ObjectAssociationMap的键值对为0,则从AssociationsHashMap中 抹除
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();
}
- 源码分析如下:
- 1.首先
DisguisedPtr
将目标对象(即YYPerson)封装成一个数组结构类型,类型为disguised{(objc_object *)object} DisguisedPtr
; - 2.
ObjcAssociation association{policy, value}
将value值与policy策略封装到ObjcAssociation
类中; - 3.
AssociationsManager manager
创建manager对象; - 4.
AssociationsHashMap &associations(manager.get())
获取associations
对象是一个哈希表结构且是全局唯一的,AssociationsHashMap里面存储的是<DisguisedPtr
,ObjectAssociationMap
>键值对,ObjectAssociationMap
也是哈希表结构存储的是<const void *
,ObjcAssociation
>键值对; - LLDB调试结果如下:
- 5.判断value值是否存在,分别执行不同的逻辑:
- 5.1.当value值存在时,执行
associations.try_emplace(disguised, ObjectAssociationMap{})
,其含义是:拿目标对象(YYPerson)也就是所谓的Key,在全局HashMap即AssociationsHashMap中进行遍历,最后返回一个refs_result
结果,LLDB打印如下:
-
try_emplace
函数是HashMap的一个函数方法,进入其内部实现:
- 可以看出其返回值
refs_result
是一个键值对即std::pair
; - 根据key(YYPerson)在AssociationsHashMap中找到对应的value,若value存在,则返回一个包含有YYPerson的ObjectAssociationMap的pair键值对;
- 根据key(YYPerson)在AssociationsHashMap中查找对应的value,若value不存在,则返回一个包含空的ObjectAssociationMap{}的pair键值对;
- 在AssociationsHashMap中根据Key值查找的逻辑:是调用
LookupBucketFor
函数,有两个同名方法,其中第二个方法属于重载函数,区别于第一个的是第二个参数没有const修饰,通过调试可知,外部的调用是调用的第二个重载函数,而第二个LookupBucketFor方法,内部的实现是调用第一个LookupBucketFor方法;
-
LookupBucketFor
函数实现如下:
- 然后执行
if (refs_result.second)
即判断当前类(YYPerson)是否是第一次进行关联,因为在try_emplace
函数中返回的结构体std::pair
,如果是第一次进行关联bool = true,如果不是第一次进行关联bool = false,refs_result.second
本质就是获取std::pair
结构体中bool成员的值; - 接着执行
auto &refs = refs_result.first->second
,这里获取的是ObjcAssociationMap
,然后调用refs.try_emplace(key, std::move(association))
,根据Key值(即标识属性的字符串)往ObjcAssociationMap中插入association
对象,这里的association
就是封装了[policy,value]的ObjcAssociation对象,最后if (!result.second)
即判断是否已经插入过了,如果是,则替换之前插入的association
对象; - 5.2.当value值不存在时,执行以下代码:
- 详细逻辑见注释;
getter方法的取值流程
- 断点依次进入以下函数:
- 最后来到
_object_get_associative_reference
函数,实现如下:
id _object_get_associative_reference(id object, const void *key)
{
//创建空的关联对象
ObjcAssociation association{};
{
//创建一个AssociationsManager管理类
AssociationsManager manager;
//获取全局唯一的静态哈希map
AssociationsHashMap &associations(manager.get());
//找到迭代器,即获取buckets
AssociationsHashMap::iterator i = associations.find((objc_object *)object);
//如果这个迭代查询器不是最后一个 获取
if (i != associations.end()) {
//找到ObjectAssociationMap的迭代查询器获取一个经过属性修饰符修饰的value
ObjectAssociationMap &refs = i->second;
//根据key查找ObjectAssociationMap,即获取bucket
ObjectAssociationMap::iterator j = refs.find(key);
if (j != refs.end()) {
//获取ObjcAssociation
association = j->second;
association.retainReturnedValue();
}
}
}
//返回value
return association.autoreleaseReturnedValue();
}
LLDB调试结果:
AssociationsHashMap的唯一性
- 前面说到AssociationsHashMap是全局唯一的,也就是说所有关联属性到对象的数据都存放在这个全局唯一的HashMap中,在源码中有体现:
- 我们将AssociationsManager的构造函数与析构函数的加锁与解锁代码去除,如下:
- 然后在
_object_set_associative_reference
中,加入以下代码:
- 看到控制台的打印结果,表明AssociationsHashMap确实是全局唯一的;