【iOS】——分类拓展关联对象

莱莱  金牌会员 | 2025-1-10 07:40:17 | 显示全部楼层 | 阅读模式
打印 上一主题 下一主题

主题 876|帖子 876|积分 2628

分类

OC的动态特性答应使用种别为现有的类添加新方法而且不必要创建子类,不必要访问原有类的源代码。通过使用种别即可动态为现有的类添加新方法,而且可以将类界说模块化分布到多个相干文件。


  • 分类是 Objective-C 中的一种语言特性,它答应你为现有类添加新的方法,而无需修改原始类的源代码。
  • 分类就像一个“扩展”,你可以用它来添加新的功能,而不必创建子类。
  • 分类不能添加新的实例变量,只能添加方法。
  • 分类也可以把framework私有方法公开化,通过在分类中声明类的实现部分的私有方法即可通过分类来调用类中的某个私有方法
  • 分类中添加的属性并没有主动生成成员变量,也没有实现set和get方法,只是生成了set和get方法的声明
示例代码如下:
  1. @interface NSString (MyCategory)
  2. - (NSString *)reverseString;
  3. @end
  4. @implementation NSString (MyCategory)
  5. - (NSString *)reverseString {
  6.     NSMutableString *reversedString = [[NSMutableString alloc] init];
  7.     for (NSInteger i = self.length - 1; i >= 0; i--) {
  8.         [reversedString appendString:[self substringWithRange:NSMakeRange(i, 1)]];
  9.     }
  10.     return reversedString;
  11. }
  12. @end
复制代码
分类应用场景:


  • 扩展体系类功能: 例如,为 NSString 类添加 reverseString 方法来反转字符串。
  • 模块化代码: 将相干的方法分组到分类中,例如将全部与网络相干的操纵方法放到一个名为 Networking 的分类中。
  • 扩展第三方库功能: 为第三方库的类添加新方法,例如为 AFNetworking 添加一个方法来处理特定类型的 API 哀求。
  • 实现协议方法: 分类可以用来实现协议方法,而无需子类化。
  • 委托模式: 分类可以用来实现委托方法,而无需子类化。
  • 耽误加载: 分类可以用来实现耽误加载,例如将一些耗时的操纵放到分类方法中,并在必要时才加载。
分类的界说如下:
  1. struct category_t {
  2.     // 分类名称
  3.     const char *name;
  4.     // 分类所属的类
  5.     classref_t cls;
  6.     // 实例方法列表
  7.     struct method_list_t *instanceMethods;
  8.     // 类方法列表
  9.     struct method_list_t *classMethods;
  10.     // 协议列表
  11.     struct protocol_list_t *protocols;
  12.     // 实例属性列表
  13.     struct property_list_t *instanceProperties;
  14.     // 类属性列表(可能不存在)
  15.     struct property_list_t *_classProperties;
  16.     // 获取实例方法或类方法列表
  17.     method_list_t *methodsForMeta(bool isMeta) {
  18.         if (isMeta) return classMethods;
  19.         else return instanceMethods;
  20.     }
  21.     // 获取实例属性或类属性列表
  22.     property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
  23. };
复制代码
从布局体可以看出,分类能


  • 给类添加实例方法 (instanceMethod)
  • 给类添加类方法 (classMethod)
  • 实现协议 (protocol)
  • 添加属性 (instancePropertie)
但是不能添加实例变量,即无法主动生成实例变量的setter和getter方法
扩展

扩展与种别相似,偶然候被称为匿名分类。但是两者实质上不是一个内容。
扩展是在编译阶段与该类同时编译的,是类的一部分。扩展中声明的方法只能在该类的@implementation中实现。所以这也就意味着我们无法对体系的类使用扩展。
同时与分类不同,扩展不但可以声明方法,还可以声明成员变量,这是分类所做不到的。
示例代码如下:
  1. #import "Car1.h"
  2. @interface Car1 ()
  3. @property (nonatomic, copy) NSString *color;
  4. - (void)drive:(NSString *)owner;
  5. @end
  6. #import "Car1.h"
  7. #import "Car1+drive.h"
  8. @implementation Car1
  9. - (void)drive {
  10.     NSLog (@"%@汽车在路上跑", self);
  11. }
  12. - (void)drive:(NSString*)owner {
  13.     NSLog(@"%@正在驾驶%@汽车在路上跑", owner, self);
  14. }
  15. - (NSString*)description {
  16.     return [NSString stringWithFormat:@"<Car[_brand = %@, _model = %@, _color = %@]>",self.brand, self.model, self.color];
  17. }
  18. @end
复制代码
两者区别



  • 分类原则上只能增加方法,但是也可以通过关联属性增加属性
  • 拓展可以增加方法和成员变量,都是私有的,实现部分在类中。
  • 扩展只能在自身类中使用,而不是子类大概其他地方。
  • 扩展是在编译阶段添加到类中,而分类是在运行时添加到类中
关联对象

关联对象答应你为一个对象添加额外的属性,即使这个对象自己没有界说这些属性。
因此,可以使用关联对象给分类添加属性
下面是关联对象的API
  1. //添加关联对象
  2. void objc_setAssociatedObject(id object, const void * key, id value, objc AssociationPolicy policy)
  3. //获得关联对象
  4. id objc_getAssociatedObject(id object, const void * key)
  5. //移除所有的关联对象
  6. void objic_removeAssociatedObjects(id object)
复制代码


  • object: 要添加属性的对象。
  • key: 用于标识属性的键,通常使用一个 NSString 对象。
  • value: 要添加的属性值。
  • policy: 关联战略,用于指定属性的生命周期和访问权限。
关联战略如下:
  1. typedef OBJC ENUM(uintptr_t, objc_AssociationPolicy) {
  2.         OBJC_ ASSOCIATION_ASSIGN = 0, //指定个弱引用相关联的对象
  3.         OBJC_ ASSOCIATION_RETAIN_NONATOMIC = 1; //指定相关对象的强引用, 非原子性
  4.         OBJC_ ASSOCIATION_COPY_NONATOMIC = 3; //指定相关的对象被复制, 非原子性
  5.         OBJC_ ASSOCIATION_RETAIN = 01401; //指定相关对象的强引用,原子性
  6.         OBJC_ ASSOCIATION_COPY = 01403; //指定相关的对象被复制, 原子性
  7. };
复制代码
给key设置值一般来说有三种方法

  • 针对每个属性,界说-个全局的key名, 然后取其地点,这一定是唯一的加上static,只在文件内部有效
  1. static const void *NameKey = &NameKey;
  2. static const void *WeightKey = &WeightKey;
复制代码

  • 针对每个属性,由于类中的属性名是唯一的,直接拿属性名作为key
  1. #define NameKey = @"name";
  2. #define WeightKey = @"weight";
复制代码

  • 使用@selector作为key
  1. @selector(name)//直接用属性名对应的get方法的selector,有提示不容易写错。并且get方法隐藏参数cmd 可以直接用,看上去就会更加简洁
复制代码
下面是示例代码:
  1. #import <UIKit/UIKit.h>
  2. #import "objc/runtime.h"
  3. NS_ASSUME_NONNULL_BEGIN
  4. @interface UIView (defaultColor)
  5. @property (nonatomic, strong)UIColor* defaultColor;
  6. @end
  7. NS_ASSUME_NONNULL_END
  8. #import "UIView+defaultColor.h"
  9. @implementation UIView (defaultColor)
  10. @dynamic defaultColor;
  11. static char kDefaultColorKey;
  12. - (void)setDefaultColor:(UIColor *)defaultColor {
  13.     //objc_setAssociatedObject(self, &kDefaultColorKey, defaultColor, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
  14.     objc_setAssociatedObject(self, @selector(defaultColor), defaultColor, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
  15. }
  16. - (id)defaultColor {
  17.     //return objc_getAssociatedObject(self, &kDefaultColorKey);
  18.     return objc_getAssociatedObject(self, _cmd);
  19. }
  20. @end
复制代码
通过上面的关联对象我们就给体系提供的UIView设置了默认颜色的属性

   _cmd 是一个特别的参数,它代表当前正在执行的方法的选择器(selector)。选择器是一个字符串,它标识了方法的名称。
  _cmd 可以让方法在执行过程中获取自身的信息,例如方法名称
  关联对象底层探索

实现关联对象技术的核心对象有

  • AssociationsManager
  • AssociationsHashMap
  • ObjectAssociationMap
  • ObjcAssociation
objc_setAssociatedObject

起首来看创建关联对象的函数,源码如下:
  1. void
  2. _object_set_associative_reference(id object, const void *key, id value, uintptr_t policy)
  3. {
  4.     //isa有一位信息为禁止关联对象,如果设置了,直接报错
  5.     if (!object && !value) return;
  6.     // 判断runtime版本是否支持关联对象
  7.     if (object->getIsa()->forbidsAssociatedObjects())
  8.         _objc_fatal("objc_setAssociatedObject called on instance (%p) of class %s which does not allow associated objects", object, object_getClassName(object));
  9.     // 将 object 封装成 DisguisedPtr 目的是方便底层统一处理
  10.     DisguisedPtr<objc_object> disguised{(objc_object *)object};
  11.     // 将 policy和value 封装成ObjcAssociation,目的是方便底层统一处理
  12.     ObjcAssociation association{policy, value};
  13.     // (如果有新值)保留锁外的新值。
  14.     // retain the new value (if any) outside the lock.
  15.     // 根据传入的缓存策略,创建一个新的value对象
  16.     association.acquireValue();
  17.     bool isFirstAssociation = false;
  18.     {
  19.     //调用构造函数,构造函数内加锁操作
  20.         AssociationsManager manager;
  21.         // 创建一个管理对象管理单例,类AssociationsManager管理一个锁/哈希表单例对。分配一个实例将获得锁
  22.         // 并不是全场唯一,构造函数中加锁只是为了避免重复创建,在这里是可以初始化多个AssociationsManager变量的
  23.         
  24.         //获取全局的HasMap
  25.         // 全场唯一
  26.         AssociationsHashMap &associations(manager.get());
  27.         
  28.         if (value) {
  29.             //去关联表中找对象对应的关联对象表,如果没有内部会重新生成一个
  30.             
  31.             auto refs_result = associations.try_emplace(disguised, ObjectAssociationMap{});
  32.             //如果没有找到
  33.             if (refs_result.second) {
  34.                 /* it's the first association we make */
  35.                 // 这是我们建立的第一个关联
  36.                 //说明是第一次设置关联对象,把是否关联对象设置为YES
  37.                 isFirstAssociation = true;
  38.             }
  39.             // 建立或替换关联
  40.             /* establish or replace the association */
  41.             // 获取ObjectAssociationMap中存储值的地址
  42.             auto &refs = refs_result.first->second;
  43.             // 移除之前的关联,根据key
  44.             // 将需要存储的值存放在关联表中存储值的地址中
  45.             // 同时会根据key去查找,如果查找到`result.second` = false ,如果找不到就创建`result.second` = true
  46.             // 创建association时,当(association的个数+1)超过3/4,就会进行两倍扩容
  47.             auto result = refs.try_emplace(key, std::move(association));
  48.             if (!result.second) {
  49.                 // 交换association和查询到的`association`
  50.                 // 其实可以理解为更新查询到的`association`数据,新值替换旧值
  51.                 association.swap(result.first->second);
  52.             }
  53.         } else {
  54.             // 这里相当于传入的nil,移除之前的关联
  55.             // 到AssociationsHashMap找到ObjectAssociationMap,将传入key对应的值变为空。
  56.             // 查找disguised 对应的ObjectAssociationMap
  57.             auto refs_it = associations.find(disguised);
  58.             // 如果找到对应的 ObjectAssociationMap 对象关联表
  59.             if (refs_it != associations.end()) {
  60.                 // 获取 refs_it->second 里面存放了association类型数据
  61.                 auto &refs = refs_it->second;
  62.                 // 根据key查询对应的association
  63.                 auto it = refs.find(key);
  64.                 if (it != refs.end()) {
  65.                     // 如果找到,更新旧的association里面的值
  66.                     association.swap(it->second);
  67.                     refs.erase(it);
  68.                     if (refs.size() == 0) {
  69.                         // 如果该对象关联表中所有的关联属性数据被清空,那么该对象关联表会被释放
  70.                         associations.erase(refs_it);
  71.                     }
  72.                 }
  73.             }
  74.         }
  75.     }
  76.     // 在锁外面调用setHasAssociatedObjects,因为如果对象有一个,这个//将调用对象的noteAssociatedObjects方法,这可能会触发initialize,这可能会做任意的事情,包括设置更多的关联对象。
  77.     if (isFirstAssociation)
  78.         object->setHasAssociatedObjects();
  79.     // release the old value (outside of the lock).
  80.     // 释放旧的值(在锁外部)
  81.     association.releaseHeldValue();
  82. }
复制代码


  • 起首就是举行参数检查和举行安全处理(检查 object 和 value 是否都为 nil,检查 object 的类是否答应关联对象)
  • 接着封装数据类型,便于底层处理(将 object 封装成 DisguisedPtr<objc_object> 类型,将 policy 和 value 封装成 ObjcAssociation 类型)
  • 接着使用 association.acquireValue() 保留新值,确保新值不会被开释
  • 然后创建一个 AssociationsManager 对象,并通过它来获取全局的 AssociationsHashMap 对象
  • 在 AssociationsHashMap 中查找 object 对应的关联表 ObjectAssociationMap
  • 如果没有找到ObjectAssociationMap,则创建一个新的 ObjectAssociationMap,并将其插入到 AssociationsHashMap 中。
  • 如果找到ObjectAssociationMap,则在ObjectAssociationMap接着查找 key 对应的关联信息
  • 如果找到,则使用新值替换旧值。
  • 如果没有找到,则创建一个新的关联信息,并将新值存储在 ObjectAssociationMap 中。
  • 如果 value 为 nil,则表示要移除关联。体系会查找 ObjectAssociationMap 中 key 对应的关联信息,并将其移除。
objc_getAssociatedObject

获取关联对象的源码如下:
  1. id
  2. _object_get_associative_reference(id object, const void *key)
  3. {
  4.     ObjcAssociation association{};//创建空的关联对象
  5.     {
  6.         AssociationsManager manager;//创建一个AssociationsManager管理类
  7.         AssociationsHashMap &associations(manager.get());//获取全局唯一的静态哈希map
  8.         AssociationsHashMap::iterator i = associations.find((objc_object *)object);//找到迭代器,即获取buckets
  9.         if (i != associations.end()) {//如果这个迭代查询器不是最后一个 获取
  10.             ObjectAssociationMap &refs = i->second; //找到ObjectAssociationMap的迭代查询器获取一个经过属性修饰符修饰的value
  11.             ObjectAssociationMap::iterator j = refs.find(key);//根据key查找ObjectAssociationMap,即获取bucket
  12.             if (j != refs.end()) {
  13.                 association = j->second;//获取ObjcAssociation
  14.                 association.retainReturnedValue();
  15.             }
  16.         }
  17.     }
  18.     return association.autoreleaseReturnedValue();//返回value
  19. }
复制代码


  • 起首创建AssociationsManager对象,接着通过它来获取全局的AssociationsHashMap
  • 在AssociationsHashMap 中查找object对应的ObjectAssociationMap
  • 如果找到ObjectAssociationMap,则在ObjectAssociationMap中接着查找key对应的关联信息并赋值给value
  • 最后返回value
objc_removeAssociatedObjects

移除关联对象的源码如下:
  1. // 与设置/获取关联引用不同,此函数对性能敏感,因为原始isa对象(如OS对象)不能跟踪它们是否有关联对象。
  2. void
  3. _object_remove_assocations(id object, bool deallocating)
  4. {
  5.     ObjectAssociationMap refs{};
  6.     {
  7.         AssociationsManager manager;
  8.         AssociationsHashMap &associations(manager.get());
  9.         AssociationsHashMap::iterator i = associations.find((objc_object *)object);
  10.         if (i != associations.end()) {
  11.             refs.swap(i->second);
  12.             // If we are not deallocating, then SYSTEM_OBJECT associations are preserved.
  13.             //如果我们没有回收,那么SYSTEM_OBJECT关联会被保留。
  14.             bool didReInsert = false;
  15.             if (!deallocating) {
  16.                 for (auto &ref: refs) {
  17.                     if (ref.second.policy() & OBJC_ASSOCIATION_SYSTEM_OBJECT) {
  18.                         i->second.insert(ref);
  19.                         didReInsert = true;
  20.                     }
  21.                 }
  22.             }
  23.             if (!didReInsert)
  24.                 associations.erase(i);
  25.         }
  26.     }
  27.     // Associations to be released after the normal ones.
  28.     // 在正常关联之后释放关联。
  29.     SmallVector<ObjcAssociation *, 4> laterRefs;
  30.     // release everything (outside of the lock).
  31.     // 释放锁外的所有内容。
  32.     for (auto &i: refs) {
  33.         if (i.second.policy() & OBJC_ASSOCIATION_SYSTEM_OBJECT) {
  34.             // If we are not deallocating, then RELEASE_LATER associations don't get released.
  35.             //如果我们不是在释放,那么RELEASE_LATER关联不会被释放
  36.             if (deallocating)
  37.                 laterRefs.append(&i.second);
  38.         } else {
  39.             i.second.releaseHeldValue();
  40.         }
  41.     }
  42.     for (auto *later: laterRefs) {
  43.         later->releaseHeldValue();
  44.     }
  45. }
复制代码


  • 起首使用 AssociationsManager 获取全局的 AssociationsHashMap,并查找 object 对应的关联表 ObjectAssociationMap。
  • 如果找到关联表,则将其复制到 refs 变量中,以便在锁外举行操纵。
  • 如果对象正在被开释,则全部关联对象都会被移除,但如果对象只是被修改,则体系关联对象会被保留
  • 如果没有保留任何关联对象,则从 AssociationsHashMap 中移除 object 对应的关联表
  • 遍历 refs 中的全部关联对象,并根据关联对象的战略举行开释。
  • 如果关联对象的战略包含 OBJC_ASSOCIATION_SYSTEM_OBJECT,则将其添加到 laterRefs 列表中,以便在全部其他关联对象开释后举行开释。
  • 如果关联对象的战略不包含 OBJC_ASSOCIATION_SYSTEM_OBJECT,则立即开释关联对象。
  • 最后遍历 laterRefs 中的全部关联对象,并开释它们
总结



  • 分类可以用来为类动态的添加方法,通过关联对象还能动态添加属性
  • 分类默认只能声明属性不会生成成员变量和对应的get和set方法
  • 分类也用于模块化设计
  • 分类是在运行期生成,扩展是类的一部分在编译期生成
  • 关联对象的API的实现都是通过操纵AssociationsManager、AssociationsHashMap、ObjectAssociationMap、ObjcAssociation来实现
Category的方法会“覆盖”掉原来类的同名方法?



  • Category的方法没有“完全替换掉”原来类已经有的方法,也就是说如果Category和原来类都有methodA,那么Category附加完成之后,类的方法列表里会有两个methodA
  • Category的方法被放到了新方法列表的前面,而原来类的方法被放到了新方法列表的背面,这也就是我们平常所说的Category的方法会“覆盖”掉原来类的同名方法,这是由于运行时在查找方法的时候是顺着方法列表的序次查找的,它只要一找到对应名字的方法,就返回了,不会在理会背面的同名方法
关联对象被存储在什么地方,是不是存放在被关联对象自己的内存中?

关联对象存放在名为ObjectAssociationMap的哈希表中,存放关联对象的哈希表又被存放在名为AssociationsHashMap的哈希表中,通过AssociationsManager来管理。也就是说全部对象的关联对象都存在一个全局map内里。而map的的key是这个对象的指针地点,而这个map的value又是别的一个全局map,内里保存了关联对象的key。
关联对象的生命周期是怎样的,什么时候被开释,什么时候被移除?

关联对象的开释时机与移除时机并不总是同等。关联对象的生命周期取决于关联战略和目的对象的生存期。弱引用战略的关联对象会随着目的对象的开释而被开释,而强引用战略和复制引用战略的关联对象会继续存在,直到它们的引用计数降为 0。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

莱莱

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表