「OC」源码学习——关联属性再探索

「OC」源码学习——关联属性再探索

前言

在寒假学习小蓝书的时候就已经接触过了关联属性,现在在学习源码的过程之中,在进行复习巩固以及深入学习

引入

对于分类来说,我可以进入源码查看他的实现

// 表示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__);
}
@end

int 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/44094390
    if (!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;
            
            // 在第二层哈希表中查找开发者定义的 key
            ObjectAssociationMap::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 关联属性底层探索

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值