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 }
复制代码

下面是关联策略所对应的修饰符

 

posted on   低头捡石頭  阅读(200)  评论(0编辑  收藏  举报

编辑推荐:
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 没有源码,如何修改代码逻辑?
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· .NET10 - 预览版1新功能体验(一)
历史上的今天:
2017-05-05 UI定制 - 高仿百度外卖顶部波浪效果
2017-05-05 UI定制 - 水波纹效果(中心向外扩散)
< 2025年3月 >
23 24 25 26 27 28 1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31 1 2 3 4 5

导航

统计

点击右上角即可分享
微信分享提示