OC进阶 - 给分类添加成员变量 | 关联对象实现原理 <objc4-818.2>
▶ 给分类添加成员变量
关于分类很多说法是只能添加方法接口、属性且不会生成成员变量、协议等。它并不是绝对不能添加成员变量,虽然在分类结构体中是没有成员变量列表的,但如想要实现也非难事:通过关联对象添加成员变量
在了解系统如何给分类添加成员变量前,我们先用 3种 方式尝试为 Animal+Pet 添加成员变量
// - Animal+Pet.h
#import "Animal.h" @interface Animal (Pet) @property(nonatomic,assign)int age; @end
// - Animal+Pet.m
1 #import "Animal+Pet.h" 2 #import <objc/runtime.h> 3 // 方式一:使用全局变量 4 // int _age01; 5 6 // 方式二:使用全局字典 7 // NSMutableDictionary *ageDic; 8 9 10 // 方式三:关联对象 11 // 首先需要一个 key值,这里指向自己的内存地址,可达到节省内存的目的 12 // 我们肯定考虑使用全局变量 13 // 全局变量容易暴露隐私,其他文件可以使用 extern关键字 直接取出使用 14 // 在这里我们提出简单的几种优化方案 15 // const void *ageKey = &ageKey; 16 17 18 // 优化A:使用静态全局变量 19 // 其实这种方式也不太友好,因为参数要求是 const void *型 20 // static const void *ageKey = &ageKey; 21 22 23 // 优化 B 24 // 我们完全可以只定义一个变量,而且不需要赋值。这里使用字符型 25 // static const char ageKey; 26 27 // 优化C:不实用全局变量!直接使用字符串字面量 28 // 因为字符串字面量在内存中处于常量区,不论你书写多少个,它内存唯独一份,内存地址一样的 29 30 // 优化D:就是针对 优化C 的一种改进,直接搞一个宏,毕竟可以使代码更容易阅读 31 // #define age_key @"age" 32 33 // 优化E:使用 @selector。也是个人推荐的方式 34 35 @implementation Animal (Pet) 36 37 // 重写 setter/getter,实现分类添加成员变量的目的 38 39 //----------------- 方式一:使用全局变量 ----------------- 40 // 存在的问题:参见 main.m文件 中的示例 41 //- (void)setAge:(int)age{ 42 // _age01 = age; 43 //} 44 //-(int)age{ 45 // return _age01; 46 //} 47 48 //---------------- 方式二:使用全局字典 ------------------- 49 // 存在的问题:参见 main.m文件 中的示例 50 // 重写 load方法,创建字典 51 //+ (void)load{ 52 // ageDic = [[NSMutableDictionary alloc] initWithCapacity:0]; 53 //} 54 //- (void)setAge:(int)age{ 55 // 56 // // 这里我们可以使用 self 的地址充当键 57 // NSString *insKey = [NSString stringWithFormat:@"%p",self]; 58 // ageDic[insKey] = @(age); 59 //} 60 //- (int)age{ 61 // NSString *insKey = [NSString stringWithFormat:@"%p",self]; 62 // return [ageDic[insKey] intValue]; 63 //} 64 65 //---------------- 方式三:关联对象 ------------------- 66 - (void)setAge:(int)age{ 67 68 // // 优化A 69 // objc_setAssociatedObject(self, ageKey, @(age), OBJC_ASSOCIATION_RETAIN_NONATOMIC); 70 71 // // 优化B:注意第二个参数传进的是地址 72 // objc_setAssociatedObject(self, &ageKey, @(age), OBJC_ASSOCIATION_RETAIN_NONATOMIC); 73 74 // // 优化C:这种方式看起来更直观,和属性名一样 75 // objc_setAssociatedObject(self, @"age", @(age), OBJC_ASSOCIATION_RETAIN_NONATOMIC); 76 // // 可以把字面量的地址打印出来做下验证 77 // NSLog(@"%p",@"age"); // 0x100001030 78 79 // // 优化D 80 // objc_setAssociatedObject(self, age_key, @(age), OBJC_ASSOCIATION_RETAIN_NONATOMIC); 81 82 // 优化E:直接传入 setter/getter的方法地址,建议使用 getter,方便操作方便 83 // 好处就是可读性高,而且输入有提示,帮助排错 84 objc_setAssociatedObject(self, @selector(age), @(age), OBJC_ASSOCIATION_RETAIN_NONATOMIC); 85 } 86 87 - (int)age{ 88 89 // // 优化A 90 // return [objc_getAssociatedObject(self, ageKey) intValue]; 91 92 // // 优化B 93 // return [objc_getAssociatedObject(self, &ageKey) intValue]; 94 95 // // 优化C:内存地址一样一样的 96 // NSLog(@"%p",@"age"); // 0x100001030 97 // return [objc_getAssociatedObject(self, @"age") intValue]; 98 99 // // 优化D 100 // return [objc_getAssociatedObject(self, age_key) intValue]; 101 102 103 // 优化E :我们知道每个方法都有两个隐藏参数 (id)self 和 _cmd:(SEL)_cmd 104 // _cmd = @selector(age) 105 return [objc_getAssociatedObject(self, _cmd) intValue]; 106 // 等同与 return [objc_getAssociatedObject(self, @selector(age)) intValue]; 107 } 108 109 @end
// - main.m
#import <Foundation/Foundation.h> #import "Animal+Pet.h" int main(int argc, const char * argv[]) { //------------------- 方式一:全局变量 -------------------- // Animal *an1A = [[Animal alloc] init]; // an1A.age = 10; // NSLog(@"%d",an1A.age); // // 貌似解决了 赋值/取值 的问题 // // 但是我们要知道,实例对象的成员变量是人手一份的 // // 使用全局变量人手一份的基本要求就无法实现!且伴有内存泄露(生命周过长)、线程安全等问题 // // Animal *an1B = [[Animal alloc] init]; // an1B.age = 18; // NSLog(@"%d",an1B.age); // 18 // NSLog(@"%d",an1A.age); // 18 // // 造成问题:两个实例对象 的值是一样的 //------------------------ 方式二:字典 ------------------------ // // 好处:利用键值对儿保证实例对象的唯一性 // Animal *an2A = [[Animal alloc] init]; // an2A.age = 10; // Animal *an2B = [[Animal alloc] init]; // an2B.age = 18; // // NSLog(@"%d",an2A.age); // 10 // NSLog(@"%d",an2B.age); // 18 // // 但是依旧存在内存泄露(全局变量)、线程安全问题 //--------------------- 方式三:关联对象 ------------------------ Animal *an3A = [[Animal alloc] init]; an3A.age = 100; Animal *an3B = [[Animal alloc] init]; an3B.age = 180; NSLog(@"%d",an3A.age); // 100 NSLog(@"%d",an3B.age); // 180 return 0; }
▶ 关联对象的底层实现
A. 在 runtime源码中关联对象是使用 objc_setAssociatedObjec函数,下面我们一步步进行窥视
该函数的内部调用了 _object_set_associative_reference函数
我们需要关注的是 AssociationsManager、AssociationsHashMap、ObjectAssociationMap、ObjcAssociation 四个
它们的工作原理如下:关联的对象并不是存储在被关联对象的本身内存之中,而是存储在全局统一的一个 AssociationsManager 里面
B. 移除关联对象
单独移除一个实例对象的关联对象:其实就是给关联对象置 nil。下面我们在 Animal+Pet 中添加新的成员变量 _name,关联对象后把它移出
// - Animal+Pet.h
@property(nonatomic,copy)NSString *name;
// - Animal+Pet.m
- (void)setName:(NSString *)name{ objc_setAssociatedObject(self, @selector(name), name, OBJC_ASSOCIATION_COPY_NONATOMIC); } - (NSString *)name{ return objc_getAssociatedObject(self, _cmd); }
// - main.m
// 关联对象 Animal *an3C = [[Animal alloc] init]; an3C.age = 1022; an3C.name = @"tudou"; NSLog(@"%@",an3C.name); // 移除关联对象 an3C.name = nil;// 同 setter方法 中传进了 nil,实际执行如下 // objc_setAssociatedObject(self, @selector(name), nil, OBJC_ASSOCIATION_COPY_NONATOMIC); NSLog(@"%@",an3C.name); // null
这里重点要关注还是看 _object_set_associative_reference 的底层实现!以下是局部截图(移出关联对象的代码)
通过对源码的解读,单独移除一个实例对象的关联对象实质上就是把 ObjectAssociationMap 中所关联的对象 _name 擦除
那么对于 objc_removeAssociatedObjects:移除所有关联对象又是如何实现 ?
找到底层实现是 _object_remove_assocations
其实质是从源头上的 AssociationHashMap 中抹掉了 Animal实例对象
▶ objc_AssociationPolicy
关联策略我们见名知义,无需多讲。需要注意的是 ASSIGN 是没有 weak 特性的,下面进行简单验证
1 int main(int argc, const char * argv[]) { 2 3 Animal *ani3D = [Animal new]; 4 // 作用域 R 5 { 6 Animal *an3E = [[Animal alloc] init]; 7 8 // an3E 作为 ani3D 的关联对象 9 objc_setAssociatedObject(ani3D, @"an3E", an3E, OBJC_ASSOCIATION_ASSIGN);// 使用 assign 10 } 11 12 // 实例对象 an3E 出了 作用域R 就会自动销毁,如果关联策略 assign 有 weak 功能,那么 an3E 销毁则置 nil,程序很安全 13 // 然而执行到下行代码就崩掉了,说明它的确只是保留了 assign 特性 14 NSLog(@"%@", objc_getAssociatedObject(ani3D, @"an3E")); 15 // 报错 message sent to deallocated instance 0x102a04860 16 17 return 0; 18 19 }
下面是关联策略所对应的修饰符
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 没有源码,如何修改代码逻辑?
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· .NET10 - 预览版1新功能体验(一)
2017-05-05 UI定制 - 高仿百度外卖顶部波浪效果
2017-05-05 UI定制 - 水波纹效果(中心向外扩散)