【iOS】源码学习-分类、扩展、关联对象

【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;
    }
};

从结构体可知,分类中的方法可以在运行时动态改变,但结构体不能。因此如果想要在运行时给原来的结构体添加一个属性,就要通过关联对象的方式。关联对象的本质是在类的定义之外为类增加额外的存储空间,实现一种映射关系。

分类的加载流程:

  1. 编译阶段将分类中的信息整合到一个category_t结构体中。
  2. 暂存未附着分类:
  1. 附着到类、元类:
  1. 调度+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);
}
objc 复制代码
void
_objc_rootDealloc(id obj)
{
    ASSERT(obj);

    obj->rootDealloc();
}
objc 复制代码
inline 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通过两步查找:

  1. 先根据对象地址找到对象自己的关联表,即定位是哪个对象的外挂存储区。

键:每个实例的伪装指针,通过位运算将对象的内存地址转换为整形,避免直接暴露指针。

值:指向该实例专属的第二层哈希表objectAssociationMap的指针。

objc 复制代码
object->ObjectAssociationMap
  1. 然后在对象对应的关联表中根据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的防护设计。
相关推荐
Zklys1 小时前
Cmake的学习笔记step1
c++·笔记·学习
飞翔中文网1 小时前
Java学习笔记之接口
java·笔记·学习
吃好睡好便好1 小时前
矩阵的左除和右除
人工智能·学习·线性代数·算法·矩阵
OBiO20131 小时前
从血清型到启动子升级——如何规避心肌 AAV 肝脏泄露?
学习
_李小白2 小时前
【android opencv学习笔记】Day 30: 滤波算法之拉普拉斯算子
android·opencv·学习
不羁的木木2 小时前
Form Kit(卡片开发服务)学习笔记04-交互事件与跳转处理
笔记·学习·交互·harmonyos
一尘之中9 小时前
从C语言底层设计到系统架构评估:软件架构知识体系全景
学习·系统架构·ai写作
星夜夏空9911 小时前
FreeRTOS学习(4)——内存映射
数据库·学习·mongodb
不羁的木木11 小时前
ArkWeb实战学习笔记05-综合实战:构建混合应用
笔记·学习·harmonyos