当前位置: 首页 > news >正文

「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 类创建了实例 p1p2,则第一层哈希表中会有两个键:

  • p1 的地址 → p1 的关联表
  • p2 的地址 → p2 的关联表

2. 第二层哈希表(ObjectAssociationMap)

  • 键(Key):开发者定义的静态键(如 @selector(name) 或全局变量地址)。
  • 值(Value):封装关联值和内存策略的 ObjcAssociation 结构体。

示例: 若 p1p2 都通过 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(); 
}
  1. DisguisedPtr<objc_object>:

    • 将对象指针伪装为整型,防止调试工具直接暴露内存地址
    • 实现方式:对指针值进行位运算混淆(如 ptr ^ DISGUISE_MASK)
  2. AssociationsManager:

    • 全局单例管理器,通过 C++ RAII 模式管理互斥锁
    • 构造函数加锁,析构函数解锁,确保线程安全
  3. AssociationsHashMap:

    • 第一层哈希表,键为 DisguisedPtr<objc_object>,值为 ObjectAssociationMap
    • 使用 C++11 unordered_map 实现,冲突处理为链地址法
  4. ObjectAssociationMap:

    • 第二层哈希表,键为 const void* (开发者传入的 key),值为 ObjcAssociation
    • 存储具体的关联值及其内存策略
    • 会存储多个不同的关联对象
  5. 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();
}
  1. AssociationsManager:

    • 通过 C++ RAII 模式管理互斥锁,构造函数中加锁,析构函数解锁
    • 确保在哈希表操作期间的线程安全
  2. 双层哈希表查找:

    • 第一层:对象地址 → ObjectAssociationMap*
    • 第二层:开发者key → ObjcAssociation
    • 例如:对象0x7f8a3b01 的 “name” 属性需两次哈希查找
  3. retainReturnedValue:

    • 根据关联策略(如 OBJC_ASSOCIATION_RETAIN)对值执行 retain
    • 特殊处理 COPY 策略:如果值支持 NSCopying 协议,执行 copy 操作
  4. 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;
}

总结

img

参考文章

iOS-底层原理 19:类扩展 与 关联对象 底层原理探索

iOS 关联属性底层探索

相关文章:

  • leetcode 131. Palindrome Partitioning
  • 【Qt】QCustomPlot相关
  • 2025一带一路暨金砖国家技能发展与技术创新大赛第三届企业信息系统安全赛项
  • 【面板数据】上市公司外资持股数据集(2005-2023年)
  • 防火墙高可用(HA)主备验证实验(eNSP)
  • TTL和死信交换机实现延迟队列
  • 4款顶级磁力下载工具,速度提升器,可以变下变播
  • 第三章 第二大脑的运作机理 整理笔记
  • 套索回归与岭回归通俗讲解
  • TCP建立连接为什么不是两次握手,而是三次,为什么不能在第二次握手时就建立连接?
  • uniapp-商城-68-shop(1-商品列表,获取数据,utils、tofixed 、parseInt的使用)
  • Python容器
  • 基于 LangChain + Chroma 实现文档向量化入库(含摘要处理 + RAG 查询):完整实战流程
  • Linux基本指令篇 —— cd指令
  • 【TypeScript】结构化类型系统与标明类型系统
  • [Protobuf] 快速上手:安全高效的序列化指南
  • Anaconda 常用命令汇总
  • RocketMQ核心特性与最佳实践
  • 用HTML5实现实时ASCII艺术摄像头
  • QT6安装与概念介绍
  • 网站备案 接入商备案/seo百科
  • 微商城网站制作/在线看seo网站
  • 网站不备案有什么影响/上海网络推广服务公司
  • 哪个网站可以付费做淘宝推广/成都自然排名优化
  • 内部网站建设app/论坛seo网站
  • 企业网站属于广告吗/关键词搜索站长工具