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

【iOS】 分类 拓展 关联对象

【iOS】 分类 拓展 关联对象

文章目录

  • 【iOS】 分类 拓展 关联对象
    • 前言
    • 拓展
    • 分类
    • 分类与拓展的区别
      • 分类
      • 拓展
      • 关联对象
        • 哈希表(AssociationsHashMap)
      • 大致工作流程
        • set
        • get
        • remove
    • 关联对象的释放时机
    • 总结

前言

之前讲过有关于类对象的内容,这里学习一下有关于类的分类拓展和关联对象的内容:

拓展

这里我们看一下下面这段代码转义程我们的cpp文件:

@interface CJLPerson ()
@property (nonatomic, copy) NSString* name;
- (void)saygogogogo;
@end

image-20250503153819448

这里我们可以看到它被直接存储到了成员变量表中.

方法也是这样直接被添加到metholist

image-20250503154720841

  • 类的扩展 在编译器 会作为类的一部分,和类一起编译进来
  • 类的扩展只是声明依赖于当前的主类,没有.m文件,可以理解为一个·h文件

分类

我们看一下分类的编译后的结构体

image-20250503155037115

发现这个分类的结构体中有类指针,有实例方法表,类方法表,协议表,属性列表,但是没有类有的成员变量表:

这里就说明了

  • 我们不可以在类中定义成员变量.(因为没有成员变量表)

  • 可以声明一个属性,但是只会生成这些属性的getter和setter方法的声明,并不会自动实现这些方法。也就是说,如果你在分类中添加了一个属性,你还需要自己去实现这个属性的getter和setter方法。

  • 既然没有成员变量表,怎么实现我们的一个属性呢,通过关联对象来实现.

分类与拓展的区别

分类

  • 专门用来给类添加新的方法

  • 不能给类添加成员属性,添加了成员属性,也无法取到

  • 注意:其实可以通过runtime 给分类添加属性,即属性关联,重写setter、getter方法

  • 分类中用 @property 定义变量,只会生成变量的setter、getter方法的声明不能生成方法实现 和 带下划线的成员变量

拓展

  • 可以说成是特殊的分类 ,也可称作 匿名分类
  • 可以给类添加成员属性,但是是私有变量
  • 可以给类添加方法,也是私有方法
  • 拓展只可以在本类中使用

关联对象

前面讲过可以通过runtime来给分类添加属性,现在我们就来了解一下有关于这里的关联对象的一个内容.

@interface CJLPerson (Test)
@property (nonatomic, copy) NSString* name;
@end- (void)setName:(NSString *)name {objc_setAssociatedObject(self, @"name", name, OBJC_ASSOCIATION_COPY_NONATOMIC);return;
}

我们先讲一下有关于这个方法的几个参数objc_setAssociatedObject

  • 参数一:要关联的对象,即给谁添加关联属性
  • 参数二:标识符,方便下次查找
  • 参数三:value
  • 参数四:属性的策略,即nonatomic、atomic、assign等

下面这个图展示处理所有对象关联对象的一个属性类型

image-20250503162413296

这里我们来详细了解一下有关于这部分的知识:

void
objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
{_object_set_associative_reference(object, key, value, policy);//接口隔离原则
}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/44094390if (!object && !value) return; // 先处理值,如果为nil没有必要处理if (object->getIsa()->forbidsAssociatedObjects()) //检查对象所属的类是否禁止关联属性(例如 NSWindow 等系统类),若禁止则触发崩溃_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};//将对象指针 object 转换为 DisguisedPtr,通过位操作隐藏指针值,避免内存分析工具直接暴露关联关系ObjcAssociation association{policy, value};// retain the new value (if any) outside the lock.association.acquireValue();bool isFirstAssociation = false; // 设置是否为第一次关联{AssociationsManager manager; //获取全局关联管理器,这个并不是单利,可以创建多次AssociationsHashMap &associations(manager.get()); //获得对应的一个hashmap,associations 是全局唯一的 AssociationsHashMap,存储所有对象的关联数据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;  //得到一个空的桶子,找到引用对象类型,即第一个元素的second值auto result = refs.try_emplace(key, std::move(association)); //查找当前的key是否有association关联对象if (!result.second) { //如果结果不存在association.swap(result.first->second);}} else { //当value为nil的时候,将key从hash表中移出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(); //若为首次关联,调用 setHasAssociatedObjects() 设置对象的 has_assoc 标志位(在对象释放时触发关联对象的清理)// release the old value (outside of the lock).association.releaseHeldValue(); //根据策略释放旧值的引用计数(例如 OBJC_ASSOCIATION_RETAIN 会调用 release)
}

步骤

  • 先处理值,如果为nil没有必要处理.检查对象所属的类是否禁止关联属性(例如 NSWindow 等系统类),若禁止则触发崩溃
  • 获取全局关联管理器,获得对应的一个hashmap,associations 是全局唯一的 AssociationsHashMap,存储所有对象的关联数据
  • 处理键值对,如果插入和更新各自处理,如果值为nil就直接移出hash表的数据
  • 若为首次关联,调用 setHasAssociatedObjects() 设置对象的 has_assoc 标志位(在对象释放时触发关联对象的清理)
  • 根据策略释放旧值的引用计数
哈希表(AssociationsHashMap)

从上面的流程我们可以看出关联对象的一个内存管理全部归我们的一个AssociationsHashMap管理,而不是由当前这个类来管理,也不是它的分类管理

我从上面的流程可以看出他的核心内容是一个AssociationHashMap我们现在主要了解一个这个hashMap的一个结构:

class AssociationsManager {using Storage = ExplicitInitDenseMap<DisguisedPtr<objc_object>, ObjectAssociationMap>;static Storage _mapStorage;public:AssociationsManager()   { AssociationsManagerLock.lock(); }~AssociationsManager()  { AssociationsManagerLock.unlock(); }AssociationsHashMap &get() {return _mapStorage.get();}static void init() {_mapStorage.init();}
};

这里我们可以看到,我们的AssociationHashMap是从这个由static修饰的静态全局变量中取出来的

class AssociationsHashMap : public unordered_map<disguised_ptr_t, ObjectAssociationMap *, DisguisedPointerHash, DisguisedPointerEqual, AssociationsHashMapAllocator> {public:void *operator new(size_t n) { return ::malloc(n); }void operator delete(void *ptr) { ::free(ptr); }};

从上面的结构体可以看出AssociationsHashMap内部维护了一个 ObjectAssociationMap哈希表:

这里的ObjectAssociationMap内部中关联了ObjcAssociationkey的一个关系

class ObjectAssociationMap : public std::map<void *, ObjcAssociation, ObjectPointerLess, ObjectAssociationMapAllocator> {public:void *operator new(size_t n) { return ::malloc(n); }void operator delete(void *ptr) { ::free(ptr); }};

image-20250503172800321

这时候笔者给出一张别人的思维导图来说明这几个表的一个关系:

哈希map结构

接下来笔者主要讲一下这里几个map的一个联系与不同:

  • 可以有无限多个Manger,但是我们的AssociationsHashMap只有一个,都是通过manger来获取的

  • 第一个map对应的是每一个类都作为一个key拥有这不同的一个ObjectAssiciationMap,每个类维护属于自己的那一个关联对象,就好比person类维护person类的,teacher维护teacher类的,.

  • 第二个ObjectAssiciationMap的key的类型为const void 再加上我们对于objc_setAssociatedObject(<id _Nonnull object>, <const void * _Nonnull key>, <id _Nullable value>, <objc_AssociationPolicy policy>)这个就相当于我们前面设置的那个字符串.value的类型是ObjcAssociation

  • 最后的value由两个成员变量,一个是内存管理的一个策略,一个是一个value.

这里就是这里我们发现,我们的key是很重要的,我们不可以把多个value关联到同一个key中,所以我们对于key有以下几个用法:

  • 采用getter方法关联:
    • objc_setAssociatedObject(obj, @selector(getter), value, OBJC_ASSOCIATION_RETAIN_NONATOMIC)
  • 采用属性名
    • objc_setAssociatedObject(self, @“name”, name, OBJC_ASSOCIATION_COPY_NONATOMIC);
  • 使用指针名
    • static void *MyKey = &MyKey; objc_setAssociatedObject(obj, MyKey, value, OBJC_ASSOCIATION_RETAIN_NONATOMIC)
    • static void *MyKey = &MyKey; 创建一个静态的 void* 指针变量 MyKey,并将其地址赋给自身。这种写法通过静态变量的内存地址唯一性保证键值的全局唯一性
  • 使用static字符作为key
    • static char MyKey; objc_setAssociatedObject(obj, &MyKey, value, OBJC_ASSOCIATION_RETAIN_NONATOMIC)’

大致工作流程

set
  • 调用 objc_setAssociatedObject。
  • AssociationsManager 查找或创建与目标对象相关的 ObjectAssociationMap。
  • 在 ObjectAssociationMap 中查找或创建对应的 ObjcAssociation。
  • 将关联值和存储策略设置到 ObjcAssociation 中
get
  • 调用 objc_getAssociatedObject。
  • AssociationsManager 查找或创建与目标对象相关的 ObjectAssociationMap。
  • 在 ObjectAssociationMap 中查找或创建对应的 ObjcAssociation。
  • 返回从ObjcAssociation返回的value
remove
  • 调用 objc_removeAssociatedObjects 或 objc_setAssociatedObject 设置为 nil。
  • AssociationsManager 查找与目标对象相关的 ObjectAssociationMap。
  • 从 ObjectAssociationMap 中移除对应的 ObjcAssociation。
  • 如果 ObjectAssociationMap 为空,可能会移除整个映射以释放资源。

关联对象的释放时机

对象销毁的时候会调用一个dealloc函数,这个函数会执行下面几个方法:

  • 1、C++函数释放 :objc_cxxDestruct
  • 2、移除关联属性:_object_remove_assocations
  • 3、将弱引用自动设置nil:weak_clear_no_lock(&table.weak_table, (id)this);
  • 4、引用计数处理:table.refcnts.erase(this)
  • 5、销毁对象:free(obj)

所以这里会自动移除这里的关联对象的属性.

所以我们不需要手动释放这里的关联对象.

总结

这里我们就大致明白了我们关联对象的一个set过程,get过程其实也是类似在这个二层hash表中进行一个检索.这里笔者就不多讲述细节了.直接用一张图来总结一下:

关联对象的底层调用流程

相关文章:

  • Spring AI 实战:第九章、Spring AI MCP之万站直通
  • 聊聊对Mysql的理解
  • 每日c/c++题 备战蓝桥杯(洛谷P1015 [NOIP 1999 普及组] 回文数)
  • 从头训练小模型: 4 lora 微调
  • 性能优化实践:内存优化技巧
  • LeetCode 热题 100 994. 腐烂的橘子
  • 宏任务与微任务
  • 高等数学第三章---微分中值定理与导数的应用(3.4~3.5)
  • 【前端】【总复习】HTML
  • 互联网大厂Java面试:从基础到实战
  • 运算放大器的主要技术指标
  • 33.降速提高EMC能力
  • SpringBoot中接口签名防止接口重放
  • 前端面经-VUE3篇(三)--vue Router(二)导航守卫、路由元信息、路由懒加载、动态路由
  • Java后端开发day40--异常File
  • 【QT】QT中http协议和json数据的解析-http获取天气预报
  • express 怎么搭建 WebSocket 服务器
  • Linux | 了解Linux中的任务调度---at与crontab 命令
  • 调试Cortex-M85 MCU启动汇编和链接命令文件 - 解题一则
  • 基于多策略混合改进哈里斯鹰算法的混合神经网络多输入单输出回归预测模型HPHHO-CNN-LSTM-Attention
  • 张建华评《俄国和法国》|埃莲娜·唐科斯的俄法关系史研究
  • 澎湃读报丨央媒头版头条集中刊发:大国应有的样子
  • 燕子矶:物流网络中的闪亮节点|劳动者的书信②
  • 阿根廷发生5.6级地震,震源深度30公里
  • 王毅谈金砖国家开展斡旋调解的经验和独特优势
  • 《探秘海昏侯国》数字沉浸特展亮相首届江西文化旅游产业博览交易会