【iOS】分类、关联对象

文章目录

分类实现原理

之前我们简单学习过分类与扩展的原理,知道了扩展是在编译时就被添加到类中,而分类是运行时才会被整合到类信息中的,这里我们探究一下分类编译后的底层结构

先看源码实现:

objc 复制代码
struct category_t {
    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;//只存属性元数据,,主要包含属性名和编码后的 attributes 描述;它让 runtime 知道这个属性怎么声明,但它本身不负责提供实例存储
    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;
    }
};
//有方法列表、协议列表、属性列表但是没有ivar列表,就是说分类底层其实根本不会负责扩展实例内存布局

分类的加载流程

  1. 在编译阶段将分类中的信息整合到一个category_t数据结构中,拿到 category_t
  2. 暂存未附着分类:addUnattachedCategoryForClass
  3. 附着到类/元类:methodizeClassremethodizeClassattachCategories
  4. 调度 +loadadd_category_to_loadable_listcall_load_methods

一句话来说就是:镜像进来之后,runtime先将分类登记到未附着表,等待目标类method/remethodize时,再将分类的方法、属性、协议挂到类/元类上,如果分类实现了load方法,再按照"主类先、分类后"的规则调用

关联对象

首先我们看这个例子:

objc 复制代码
#import "ZYLPerson+Pop.h"
#import <objc/runtime.h>
static const void* nameKey = &nameKey;
@implementation ZYLPerson (Pop)

- (void)setName:(NSString *)name {
  objc_setAssociatedObject(self,nameKey , name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

- (NSString *)name {
  return objc_getAssociatedObject(self, nameKey);
}

- (void)cate_instanceMethod {
  NSLog(@"%s", __func__);
}

+ (void)cate_classMethod {
  NSLog(@"%s", __func__);
}

@end 

上面的例子我们简单实现了关联对象的流程,下面我们看看关联对象的底层。主要是一个双层哈希表结构

runtime是通过两步查找,首先根据对象地址找到对象自己的关联表。

objc 复制代码
object -> ObjectAssociationMap//即定位是哪个对象的外挂存储区
  //键:每个实例的伪装指针,通过位运算将对象的内存地址转换为整形,避免直接暴露指针
  //值:指向该实例专属的第二层哈希表objectAssociationMap的指针

然后再在对象对应的关联表中根据key找到具体的关联值

objc 复制代码
key -> ObjcAssociation//这里的ObjcAssocation不只是value,本质上还带了policy,即一些属性修饰策略
  //开发者定义的静态键
  //值:封装关联值和内存策略的objcAssociation结构体

源码探索

objc 复制代码
void
_object_set_associative_reference(id object, const void *key, id value, uintptr_t policy)
{
    if (!object && !value) return;

    Class objectClass = object->getIsa();//拿当前类指针
    objectClass->realizeIfNeeded();//确保类已经完成runtime层面的realize

    if (objectClass->forbidsAssociatedObjects())//判断是否允许关联对象
        _objc_fatal("objc_setAssociatedObject called on instance (%p) of class %s which does not allow associated objects",
                    object, object_getClassName(object));

    DisguisedPtr<objc_object> disguised{(objc_object *)object};//获取对象的伪装指针,第一层hash表使用的key
    ObjcAssociation association{policy, value};//包装要存储的值

    association.acquireValue();//加锁前完成新值的内存管理操作

    bool isFirstAssociation = false;
    {
        AssociationsManager manager;//一个RALL型的管理器,处理构造加锁、析构解锁
        AssociationsHashMap &associations(manager.get());//获取全局关联表

        if (value) {
            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));//按key插入,如果原来不存在就插入,存在就不插入,返回已有项
            if (!result.second) {//如果key已经存在就替换旧值
                association.swap(result.first->second);
            }
        } 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);//如果第二层表空了,就直接将第一层表也删除了
                    }
                }
            }
        }
    }

  //这一步必须在锁外操作,他可能调用_noteAssociatedObjects从而触发对象的+initailize,导致触发其他关联对象操作等
    if (isFirstAssociation)
        object->setHasAssociatedObjects();
//锁外释放旧值
    association.releaseHeldValue();
}

AssociationManager:全局的单例管理器,通过C++的RALL模式管理互斥锁,构造函数加锁,析构函数解锁,保证线程安全

接下来我们看读源码

objc 复制代码
id
_object_get_associative_reference(id object, const void *key)
{
    ObjcAssociation association{};//创建一个空的ObjcAssociation,用来存储后续查到的关联项

    {
        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;
                association.retainReturnedValue();//这里会根据条件进行retain操作
            }
        }
    }
//锁只保护共享数据结构本身,不拿着锁做更复杂的对象生命周期交互。
    return association.autoreleaseReturnedValue();//返回前根据getter policy决定是否需要autorelease一下
}
  1. retainReturnedValue:

根据关联策略(如 OBJC_ASSOCIATION_RETAIN)对值执行 retain

特殊处理 COPY 策略:如果值支持 NSCopying 协议,执行 copy 操作

  1. autoreleaseReturnedValue:

将返回值加入当前自动释放池,延迟释放时机

即使策略是 ASSIGN 也强制 autorelease,避免野指针风险

关联对象不需要我们去管理内存

objc 复制代码
inline void
objc_object::rootDealloc()
{
    // 小对象无需内存收
    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++析构函数,如果有需要先执行析构函数
#else
                 !isa.getClass(false)->hasCxxDtor() && // 旧版本检查类是否有C++析构
#endif
                 !isa.has_sidetable_rc))     // 未使用sidetable存储引用计数
    {
        assert(!sidetable_present());//快速路径
        free(this);  // 直接释放内存
    } 
    else {
        object_dispose((id)this);  // 需要复杂处理的场景
    }
}

// 对象销毁入口函数,如果对象牵涉weak、关联对象、C++析构、side tbale等复杂机制,就不能走直接free
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;
}
相关推荐
Mr -老鬼2 小时前
EasyClick iOS版 CLI 与 Trae iOS版智能体
ios·自动化·ai编程·tare·ec·easyclick·易点云测
wechatbot8882 小时前
企业微信 iPad 协议接口全功能开发实战
汇编·人工智能·ios·微信·企业微信·ipad
源码之家3 小时前
计算机毕业设计:Python股票市场智能分析与LSTM预测系统 Flask框架 TensorFlow LSTM 数据分析 可视化 大数据 大模型(建议收藏)✅
人工智能·python·信息可视化·数据挖掘·flask·lstm·课程设计
nap-joker3 小时前
基于基因的微生物组表示增强了宿主表型分类
人工智能·分类·数据挖掘
Digitally3 小时前
如何将视频从 iPhone 传输到电脑
ios·电脑·iphone
isNotNullX3 小时前
数据分析怎么做?数据分析框架是什么?
数据挖掘·数据分析
pop_xiaoli18 小时前
【iOS】dyld加载
macos·ios·objective-c·cocoa
小糖学代码19 小时前
LLM系列:2.pytorch入门:3.基本优化思想与最小二乘法
人工智能·python·算法·机器学习·ai·数据挖掘·最小二乘法