「OC」源码学习——关联属性再探索
「OC」源码学习——关联属性再探索
文章目录
- 「OC」源码学习——关联属性再探索
- 前言
- 引入
- **1. 第一层哈希表(AssociationsHashMap)**
- **2. 第二层哈希表(ObjectAssociationMap)**
- 源码探索
- objc_setAssociatedObject
- `objc_getAssociatedObject`
- 移除关联对象
- 总结
- 参考文章
前言
在寒假学习小蓝书的时候就已经接触过了关联属性,现在在学习源码的过程之中,在进行复习巩固以及深入学习
引入
对于分类来说,我可以进入源码查看他的实现
// 表示Objective-C分类(Category)的运行时内部结构
struct category_t {// 分类的名称,例如为NSString添加的分类名为"MyCategory"const char *name;// 指向该分类所扩展的原始类的引用(编译时填充为指向类结构的指针)classref_t cls;// 实例方法列表(包装指针,包含指针验证逻辑,如Ptrauth用于ARM64e架构的指针签名)WrappedPtr<method_list_t, method_list_t::Ptrauth> instanceMethods;// 类方法列表(同样包含指针验证)WrappedPtr<method_list_t, method_list_t::Ptrauth> classMethods;// 该分类遵循的协议列表(protocol_list_t结构指针)struct protocol_list_t *protocols;// 实例属性列表(property_list_t结构指针,用于声明@property)struct property_list_t *instanceProperties;// 类属性列表(注意:此字段在磁盘上的分类结构中可能不存在,运行时加载时动态填充)struct property_list_t *_classProperties;// 根据目标是否为元类返回对应方法列表// isMeta=true时返回类方法列表,否则返回实例方法列表method_list_t *methodsForMeta(bool isMeta) const {return isMeta ? classMethods : instanceMethods;}// 根据目标元类状态返回属性列表(需结合header_info头信息处理)property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi) const;// 返回协议列表(元类不持有协议,因此返回nullptr)protocol_list_t *protocolsForMeta(bool isMeta) const {return isMeta ? nullptr : protocols;}
};
可以看到在category_t
之中,并没有Ivar
的实例变量列表,而且发现分类里即是声明了属性,但并不会给我们生成getter/setter
。那就需要使用动态关联属性的方式自己写一个getter/setter
,具体例子如下:
// MyPerson的分类
@interface MyPerson (Test)
@property (nonatomic, copy) NSString *name;
- (void)cate_instanceMethod;
+ (void)cate_classMethod;
@end@implementation MyPerson (Test)
- (void)setName:(NSString *)name {objc_setAssociatedObject(self, MyNameKey, name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}- (NSString *)name {return objc_getAssociatedObject(self, MyNameKey);
}- (void)cate_instanceMethod {NSLog(@"%s", __func__);
}+ (void)cate_classMethod {NSLog(@"%s", __func__);
}
@endint main(int argc, const char * argv[]) {@autoreleasepool {NSLog(@"main");MyPerson *p = [MyPerson alloc];[p speak];p.name = @"bb";NSLog(@"%@", p.name); }return 0;
}
以这个例子我们先来了解一下这个关联对象的双层哈希表的结构
1. 第一层哈希表(AssociationsHashMap)
- 键(Key):每个实例的伪装指针
DisguisedPtr<objc_object>
- 通过位运算将对象的内存地址转换为整型,避免直接暴露指针。
- 每个实例的地址唯一,即使同一类创建多个实例,它们的地址也不同。
- 值(Value):指向该实例专属的 第二层哈希表(ObjectAssociationMap) 的指针。
示例: 若 MyPerson
类创建了实例 p1
和 p2
,则第一层哈希表中会有两个键:
p1
的地址 →p1
的关联表p2
的地址 →p2
的关联表
2. 第二层哈希表(ObjectAssociationMap)
- 键(Key):开发者定义的静态键(如
@selector(name)
或全局变量地址)。 - 值(Value):封装关联值和内存策略的
ObjcAssociation
结构体。
示例: 若 p1
和 p2
都通过 objc_setAssociatedObject
设置了 name
属性:
p1
的第二层表中存储key: name → value: "Alice"
p2
的第二层表中存储key: name → value: "Bob"
源码探索
objc_setAssociatedObject
我们先看一下objc_setAssociatedObject
的源码
void
_object_set_associative_reference(id object, const void *key, id value, uintptr_t policy)
{// 此代码在 object 和 key 传入 nil 时仍能工作。某些代码可能依赖此行为而不崩溃。需显式检查处理。// 原始问题记录:rdar://problem/44094390if (!object && !value) return;// 检查对象所属类是否禁止关联对象(通过 class_ro_t->flags 的 RO_FORBIDS_ASSOCIATED_OBJECTS 标志位)if (object->getIsa()->forbidsAssociatedObjects())_objc_fatal("objc_setAssociatedObject 在类 %s 的实例 (%p) 上被调用,但该类禁止关联对象", object_getClassName(object), object);// 将对象指针伪装为 DisguisedPtr(防止调试工具直接暴露内存地址)DisguisedPtr<objc_object> disguised{(objc_object *)object};// 创建关联对象封装结构,包含内存策略和值ObjcAssociation association{policy, value};// 在加锁前执行新值的 retain/copy 操作(根据内存策略)association.acquireValue(); // 标记是否是首次关联(用于后续触发 has_assoc 标志位更新)bool isFirstAssociation = false;{// 获取全局关联对象管理器(内部包含互斥锁,确保线程安全)AssociationsManager manager;// 获取全局关联对象哈希表 AssociationsHashMap 的引用AssociationsHashMap &associations(manager.get());if (value) { // 关联新值// 尝试插入或查找对象对应的二级哈希表(ObjectAssociationMap)// try_emplace 返回 pair<iterator, bool>,second 表示是否为新插入auto refs_result = associations.try_emplace(disguised, ObjectAssociationMap{});if (refs_result.second) { // 新插入条目,说明是首次关联isFirstAssociation = true;}// 获取二级哈希表引用,进行键值操作auto &refs = refs_result.first->second;// 尝试插入或替换当前键的关联值auto result = refs.try_emplace(key, std::move(association));if (!result.second) { // 键已存在,执行替换association.swap(result.first->second); // 交换新旧关联值}} else { // value 为 nil,表示移除关联对象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);}}}}} // 此处自动释放 AssociationsManager 的锁// 在锁外设置 has_assoc 标志位(可能触发 +initialize 方法)// 注意:必须在锁外调用,因为 _noteAssociatedObjects 可能触发其他关联对象操作if (isFirstAssociation)object->setHasAssociatedObjects(); // 设置对象的 HAS_ASSOCIATED_OBJECTS 标志位// 在锁外释放旧值(根据内存策略执行 release 或 autorelease)association.releaseHeldValue();
}
-
DisguisedPtr<objc_object>:
- 将对象指针伪装为整型,防止调试工具直接暴露内存地址
- 实现方式:对指针值进行位运算混淆(如 ptr ^ DISGUISE_MASK)
-
AssociationsManager:
- 全局单例管理器,通过 C++ RAII 模式管理互斥锁
- 构造函数加锁,析构函数解锁,确保线程安全
-
AssociationsHashMap:
- 第一层哈希表,键为 DisguisedPtr<objc_object>,值为 ObjectAssociationMap
- 使用 C++11 unordered_map 实现,冲突处理为链地址法
-
ObjectAssociationMap:
- 第二层哈希表,键为 const void* (开发者传入的 key),值为 ObjcAssociation
- 存储具体的关联值及其内存策略
- 会存储多个不同的关联对象
-
ObjcAssociation:
- 封装关联值和内存策略的结构体
- 内存策略和@property的用法类似
objc_getAssociatedObject
id
_object_get_associative_reference(id object, const void *key)
{// 创建空的关联对象封装结构(用于存储返回值)ObjcAssociation association{};{// 获取全局关联对象管理器(RAII锁:构造函数加锁,析构函数解锁)AssociationsManager manager;// 获取全局第一层哈希表 AssociationsHashMap 的引用AssociationsHashMap &associations(manager.get());// 在第一层哈希表中查找对象的关联表(键为对象指针)AssociationsHashMap::iterator i = associations.find((objc_object *)object);if (i != associations.end()) { // 找到对象关联表// 获取第二层哈希表 ObjectAssociationMap 的引用ObjectAssociationMap &refs = i->second;// 在第二层哈希表中查找开发者定义的 keyObjectAssociationMap::iterator j = refs.find(key);if (j != refs.end()) { // 找到关联值association = j->second; // 复制关联值和策略// 根据策略 retain 值(例如 RETAIN/COPY 策略需增加引用计数)association.retainReturnedValue();}}} // 此处自动释放锁// 将关联值注册到自动释放池(遵循ARC内存管理规则,调用者无需手动释放)// 注意:即使策略是 ASSIGN,这里也会执行 autorelease 以保证安全return association.autoreleaseReturnedValue();
}
-
AssociationsManager:
- 通过 C++ RAII 模式管理互斥锁,构造函数中加锁,析构函数解锁
- 确保在哈希表操作期间的线程安全
-
双层哈希表查找:
- 第一层:对象地址 → ObjectAssociationMap*
- 第二层:开发者key → ObjcAssociation
- 例如:对象0x7f8a3b01 的 “name” 属性需两次哈希查找
-
retainReturnedValue:
- 根据关联策略(如 OBJC_ASSOCIATION_RETAIN)对值执行 retain
- 特殊处理 COPY 策略:如果值支持 NSCopying 协议,执行 copy 操作
-
autoreleaseReturnedValue:
- 将返回值加入当前自动释放池,延迟释放时机
- 即使策略是 ASSIGN 也强制 autorelease,避免野指针风险
移除关联对象
关联属性不需要我们程序员去管理内存
inline void
objc_object::rootDealloc()
{// TaggedPointer 无需内存回收(特殊标记的小对象,内存直接存储在指针值中)if (isTaggedPointer()) return; // fixme necessary?// 快速路径判断:对象满足以下所有条件时可直接释放if (fastpath(isa.nonpointer && // 使用优化的非指针型isa!isa.weakly_referenced && // 无弱引用指向该对象!isa.has_assoc && // 未设置关联对象
#if ISA_HAS_CXX_DTOR_BIT!isa.has_cxx_dtor && // 无C++析构函数(新版本ISA标志位)
#else!isa.getClass(false)->hasCxxDtor() && // 旧版本检查类是否有C++析构
#endif!isa.has_sidetable_rc)) // 未使用sidetable存储引用计数{assert(!sidetable_present());free(this); // 直接释放内存} else {object_dispose((id)this); // 需要复杂处理的场景}
}// 对象销毁入口函数
id
object_dispose(id obj)
{if (!obj) return nil;objc_destructInstance(obj); // 执行实例销毁逻辑free(obj); // 释放对象内存return nil;
}// 对象实例销毁核心逻辑
void *objc_destructInstance(id obj)
{if (obj) {// 一次性读取所有标志位以优化性能bool cxx = obj->hasCxxDtor(); // 检查是否有C++析构函数bool assoc = obj->hasAssociatedObjects(); // 检查是否有关联对象// 处理顺序非常重要(先析构再移除关联)if (cxx) object_cxxDestruct(obj); // 执行C++析构函数if (assoc) _object_remove_assocations(obj, /*deallocating*/true); // 移除关联对象obj->clearDeallocating(); // 清理弱引用和sidetable引用计数}return obj;
}
总结
参考文章
iOS-底层原理 19:类扩展 与 关联对象 底层原理探索
iOS 关联属性底层探索