【iOS】源码学习-分类、扩展、关联对象
前言
我们已经学习过分类和扩展的区别,这篇博客重点看一下分类、扩展和关联对象的底层原理。
分类实现原理
分类是一种将类的实现分散到过个源文件的方式。分类可以为现有的类添加新的方法。分类不能添加实例变量,只能添加属性和方法,属性也不会自动生成实例变量,只生成setter、getter方法。分类的特性是可以在运行时阶段动态地为已有的类添加新行为。分类就是对装饰模式的一种具体实现,它的作用主要在于不改变原有类的前提下,动态地给这个类添加一些方法。
分类的源码:
objc
struct category_t {
// 没有ivar列表
const char *name; // 分类的名称
classref_t cls; // 分类所属的类
WrappedPtr<method_list_t, method_list_t::Ptrauth> instanceMethods; // 分类中定义的实例方法列表
WrappedPtr<method_list_t, method_list_t::Ptrauth> classMethods; // 分类中定义的类方法列表
struct protocol_list_t *protocols; // 分类中实现的协议列表
struct property_list_t *instanceProperties; // 分类中定义的属性列表,只存储属性元数据,不负责提供实例存储
// Fields below this point are not always present on disk.
struct property_list_t *_classProperties;
method_list_t *methodsForMeta(bool isMeta) const {
if (isMeta) return classMethods;
else return instanceMethods;
}
property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi) const;
protocol_list_t *protocolsForMeta(bool isMeta) const {
if (isMeta) return nullptr;
else return protocols;
}
};
从结构体可知,分类中的方法可以在运行时动态改变,但结构体不能。因此如果想要在运行时给原来的结构体添加一个属性,就要通过关联对象的方式。关联对象的本质是在类的定义之外为类增加额外的存储空间,实现一种映射关系。
分类的加载流程:
- 编译阶段将分类中的信息整合到一个category_t结构体中。
- 暂存未附着分类:

- 附着到类、元类:

- 调度+load:

总结一下:分类加载流程为镜像进来之后,Runtime先将分类登记到未附着表,等待目标类methodize(方法化)/remethodize(重新方法化)时,再将分类的方法、属性、协议挂到类/元类上。如果分类实现了load方法,再按照"主类先、分类后"的规则调用。
扩展实现原理
扩展与分类相似,是特殊的分类,也可称作是匿名分类。与分类不同,扩展可以添加实例变量和属性 。扩展中声明方法和属性必须在类的主实现文件中实现,否则会导致编译错误。并且类扩展中添加的实例变量和方法默认为@private类型,其使用范围只能在自身类中。扩展是在编译阶段与类同时编译的,是类的一部分。
根据源码调试探索,我们可知:
类的扩展在编译器会作为类的一部分和类一起编译进来。类的扩展只是声明,依赖于当前的主类,可以理解为一个.h文件。
总结一下分类与扩展的区别:
- 分类只能增加方法,但可以通过关联属性来增加属性。
- 扩展既可以增加方法也可以增加属性,但都是私有的。
- 扩展只能在自身类中使用,而不能是子类或其他地方。
- 类扩展是在编译阶段添加到类中的,而分类是运行时。
关联对象实现原理
在OC中,关联对象是一种动态给对象添加属性的机制。关联对象能在运行时把任意值关联到一个对象上,有三个API:
- 设置关联对象所有关联值
objc
void
objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
{
_object_set_associative_reference(object, key, value, policy);
}
- 获取关联对象所有关联值
objc
id
objc_getAssociatedObject(id object, const void *key)
{
return _object_get_associative_reference(object, key);
}
- 移除关联对象所有关联值(一般不用手动调)
objc
void objc_removeAssociatedObjects(id object)
{
if (object && object->hasAssociatedObjects()) {
_object_remove_associations(object, /*deallocating*/false);
}
}
关联对象不需要去管理内存的原因:
对象第一次添加关联对象时,会通过objc_setAssociatedObject在isa里标记has_assoc,Runtime通过这个标记快速判断对象是否挂载了关联对象。而当对象dealloc时,Runtime自动调用以下流程,不用手动调用objc_removeAssociatedObjects,逐层删除哈希表中的记录,回收内存。
关联对象存放在Runtime全局双层哈希表,不属于对象本身的内存布局。对象销毁时,Runtime全权接管这部分外挂数据的回收,和对象原生成员变量一样自动释放。
objc- (void)dealloc { _objc_rootDealloc(self); }
objcvoid _objc_rootDealloc(id obj) { ASSERT(obj); obj->rootDealloc(); }
objcinline void objc_object::rootDealloc() { // TaggedPointer本身没有堆内存,没有关联对象,直接return if (isTaggedPointer()) return; // fixme necessary? // We only get here through a msgSend, so the class is realized. #if !ISA_HAS_INLINE_RC _object_dispose_nonnull_realized((id)this); #else // 快速释放条件:无弱引用、无关联对象、无C++析构、无sidetable引用计数 if (fastpath(isa().nonpointer && !isa().weakly_referenced && !isa().has_assoc && #if ISA_HAS_CXX_DTOR_BIT !isa().has_cxx_dtor && #else !isa().getClass(false)->hasCxxDtor() && #endif !isa().has_sidetable_rc)) { assert(!sidetable_present()); free(this); // 如果满足直接free } else { _object_dispose_nonnull_realized((id)this); // 不满足就走完整清理 } #endif // ISA_HAS_INLINE_RC }
objc// 对象销毁入口函数 id _object_dispose_nonnull_realized(id obj) { // 执行实例销毁逻辑,析构对象内部状态 objc_destructInstance_nonnull_realized(obj); free(obj); // 释放对象内存 return nil; }
objc// 对象实例销毁核心逻辑,将对象身上挂着的各种运行时机机制清除掉 static void *objc_destructInstance_nonnull_realized(id obj) { // Read all of the flags at once for performance. // 一次性读取所有标志位以优化性能 bool cxx = obj->hasCxxDtor(); // 检查是否有C++析构函数 bool assoc = obj->hasAssociatedObjects(); // 检查是否有关联对象 // This order is important. // 先析构再移除关联 if (cxx) object_cxxDestruct(obj); // 执行C++析构函数 if (assoc) _object_remove_associations(obj, /*deallocating*/true); // 移除关联对象 obj->clearDeallocating(); // 清理弱引用和sidetable引用计数 return obj; }
具体示例看一下:
objc
#import "LYDPerson.h"
@interface LYDPerson (LYD)
@property (nonatomic, copy) NSString *name;
- (void)cate_instanceMethod;
+ (void)cate_classMethod;
@end
#import "LYDPerson+LYD.h"
#import <objc/runtime.h>
static const void* nameKey = &nameKey;
@implementation LYDPerson (LYD)
// setter方法
- (void)setName:(NSString *)name {
// p1:要绑定数据的对象 p2:唯一标识,和getter里key一致 p3:要存储的值 p4:内存策略,和property对应
objc_setAssociatedObject(self, nameKey, name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
// getter方法
- (NSString *)name {
// 根据self和nameKey取之前存的值
return objc_getAssociatedObject(self, nameKey);
}
- (void)cate_instanceMethod {
NSLog(@"%s", __func__);
}
+ (void)cate_classMethod {
NSLog(@"%s", __func__);
}
@end

关联对象的底层不是存在对象的结构体里 ,Runtime不会修改LYDPerson的内存布局,不会增加实例大小。而是存在Runtime全局哈希表里,以self为key,指向一个二级哈希表。
Runtime通过两步查找:
- 先根据对象地址找到对象自己的关联表,即定位是哪个对象的外挂存储区。
键:每个实例的伪装指针,通过位运算将对象的内存地址转换为整形,避免直接暴露指针。
值:指向该实例专属的第二层哈希表objectAssociationMap的指针。
objc
object->ObjectAssociationMap
- 然后在对象对应的关联表中根据key找到具体的关联值
objc
key->ObjcAssociation
这里的ObjcAssociation不只是value,还带上了一些属性修饰策略。
键:开发者定义的静态键。
值:封装关联值和内存策略的objcAssociation结构体。

- AssociationsManager:是管理所有关联对象的关联数据的中心管理器。它负责存储和检索关联对象的整体结构。它是一个单例,确保在整个运行时环境中只有一个实例。
- AssociationsHashMap:是一个哈希表,用于高效地查找和存储对象与其关联数据之间的映射。它在内部使用了一个哈希表,这个字段的key是被添加关联对象的对象地址,value是ObjectAssociationMap。
- ObjectAssociationMap:是一个结构体或类,用于管理一个对象的所有关联键及其对应的关联值。key是我们调用objc_setAssociatedObject传入的key,value是ObjectAssociation。
关联对象添加实例变量的本质其实是在class_rw_t里附加方法和属性元数据,再借助Runtime的全局哈希表存值。
具体看一下_object_set_associative_reference和_object_get_associative_reference的源码:
objc
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;
// 那当前类指针
Class objectClass = object->getIsa();
// 确保类已经完成Runtime层面的realize
objectClass->realizeIfNeeded();
// 判断是否允许关联对象
if (objectClass->forbidsAssociatedObjects())
_objc_fatal("objc_setAssociatedObject called on instance (%p) of class %s which does not allow associated objects", object, object_getClassName(object));
// 获取对象的伪装指针,第一层哈希表使用的key
DisguisedPtr<objc_object> disguised{(objc_object *)object};
// 包装要存储的值
ObjcAssociation association{policy, value};
// retain the new value (if any) outside the lock.
// 加锁前完成新值的内存管理操作
association.acquireValue();
bool isFirstAssociation = false;
{
// 一个RAII型的管理器,处理构造加锁、析构解锁
// RAII:Resource Acquisition Is Initialization的缩写,即资源获取初始化,C++里一种经典的资源管理模式。也就是把资源的获取(如加锁、开文件、分配内存)放在对象的构造函数里,把资源的释放(解锁、关文件、释放内存)放在析构函数里。只要对象的生命周期结束,资源就会被自动释放。
AssociationsManager manager;
// 获取全局关联表(第一层总表)
AssociationsHashMap &associations(manager.get());
// 给关联对象设值,做新增/覆盖操作
if (value) {
// 往第一层表插入查找当前对象对应的二级表
auto refs_result = associations.try_emplace(disguised, ObjectAssociationMap{});
// 判断是第几次关联:如果本次才是新建的二级表,标记当前对象是一次添加关联
if (refs_result.second) {
/* it's the first association we make */
isFirstAssociation = true;
}
/* establish or replace the association */
// 拿到当前对象专属的第二层表
auto &refs = refs_result.first->second;
// 往第二层表里插入key+关联数据
auto result = refs.try_emplace(key, std::move(association));
if (!result.second) {
// key存在就替换旧值
association.swap(result.first->second);
}
// 给关联对象设为nil,做删除操作
} 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();
}
objc
id
_object_get_associative_reference(id object, const void *key)
{
// 创建一个空的ObjcAssociation来存储后续查到的关联项
ObjcAssociation association{};
{
AssociationsManager manager;
AssociationsHashMap &associations(manager.get());
// 先按照object查对象的关联表
AssociationsHashMap::iterator i = associations.find((objc_object *)object);
if (i != associations.end()) {
// 命中第一层后取出第二层
ObjectAssociationMap &refs = i->second;
ObjectAssociationMap::iterator j = refs.find(key);
if (j != refs.end()) {
association = j->second;
// 根据条件进行retain操作
association.retainReturnedValue();
}
}
}
// 锁只保护全局两层哈希表的读写,绝不拿着锁去做对象引用计数、自动释放、方法调用这类复杂逻辑
// 返回getter取值和ObjcAssociation里记录的内存策略
// 若策略是retain/copy:取出对象后执行autorelease;若策略是assign:直接返回,不做释放操作
return association.autoreleaseReturnedValue();
}
区分一下retainReturnedValue和autoreleaseReturnedValue:
- retainReturnedValue:读取关联值时,按照当初设置的内存策略,对取出的值做引用计数、拷贝处理,保证内存安全。(偏内部逻辑)
- OBJC_ASSOCIATION_RETAIN/OBJC_ASSOCIATION_RETAIN_NONATOMIC:执行retain,对象引用计数+1,持有该对象。
- OBJC_ASSOCIATION_COPY/OBJC_ASSOCIATION_COPY_NONATOMIC:不走retain,而是检查对象是否遵守NSCopying协议,调用copy生成新副本,用副本作为返回值。
- OBJC_ASSOCIATION_ASSIGN:不做retain、不做copy,直接返回原指针,弱引用。
- autoreleaseReturnedValue:把取出的关联值加入自动释放池,延迟销毁,是Runtime对外getter的标准收尾逻辑。(偏对外接口)
无论原策略是retain、copy,还是assign,取值后都强制加入自动释放池autorelease,规避即时释放带来的野指针、访问崩溃风险,是Runtime的防护设计。