首页 > 技术文章 > OC笔记 - 分类添加成员变量 | 关联对象底层实现 <objc4-818.2>

self-epoch 2021-05-05 23:16 原文

前言

1 - 关于分类很多说法是只能添加方法、属性、协议等,而不能添加成员变量。其实这种说法是不严谨的,并不是绝对不能添加成员变量

2 - 分类的结构体中是没有成员变量列表的,如想要添加成员变量也非难事:我们完全可以通过 runtime手动添加实现我们想要的效果

分类添加成员变量

1 - 使用三种方式为分类 Animal+Pet添加成员变量方式三是关联对象)

// - Animal.h

1 #import <Foundation/Foundation.h>
2 @interface Animal : NSObject
3 
4 @end

// - Animal.m

1 #import "Animal.h"
2 
3 @implementation Animal
4 
5 @end

// - Animal+Pet.h

 1 #import "Animal.h"
 2 @interface Animal (Pet)
 3 
 4 @property(nonatomic,assign)int age;
 5 
 6 // 分类中的属性
 7 // 不会生成成员变量 _age;会帮助声明 setter\getter接口,但不会实现
 8 // - (void)setAge:(int)age;
 9 // - (int)age;
10 
11 @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 // 方式三:关联对象
 12 // 设定一个 key值,这里我们自己指向自己的内存地址,以达到节省内存的目的
 13 // const void *ageKey = &ageKey;
 14 // 全局变量容易暴露隐私,其他文件可以使用 extern关键字直接取出使用
 15 // 在这里我们提出简单的几种优化方案
 16 
 17 // 优化 ①:使用静态全局变量
 18 // static const void *ageKey = &ageKey;
 19 // 其实这种方式也不太友好,因为参数要求是 const void *型
 20 // 我们完全可以只定义一个变量,而且不需要赋值。这里使用字符型(节约内存空间)
 21 
 22 // 优化 ②
 23 // static const char ageKey;
 24 
 25 // 优化 ③:直接使用字符串字面量
 26 // 因为字符串字面量在内存中处于常量区,不论你书写多少个,它内存就独一份,内存地址一样的
 27 
 28 // 优化 ④:就是对优化 ③的一种改进,直接搞一个宏
 29 // #define age_key @"age"
 30 
 31 // 优化 ⑤:使用 @selector。也是个人推荐的方式
 32 
 33 @implementation Animal (Pet)
 34 
 35 // 重写 setter\getter,实现分类添加成员变量的目的
 36 
 37 //-----------------------------------
 38 // 方式一:使用全局变量
 39 //- (void)setAge:(int)age{
 40 //
 41 //    _age01 = age;
 42 //}
 43 //
 44 //-(int)age{
 45 //
 46 //    return _age01;
 47 //}
 48 
 49 
 50 //-----------------------------------
 51 // 我们在 load方法中创建字典
 52 //+ (void)load{
 53 //
 54 //    ageDic = [[NSMutableDictionary alloc] initWithCapacity:0];
 55 //}
 56 //
 57 //// 方式二:使用字典
 58 //- (void)setAge:(int)age{
 59 //
 60 //    NSString *insKey = [NSString stringWithFormat:@"%p",self];
 61 //    ageDic[insKey] = @(age);
 62 //
 63 //}
 64 //
 65 //-(int)age{
 66 //
 67 //    NSString *insKey = [NSString stringWithFormat:@"%p",self];
 68 //    return [ageDic[insKey] intValue];
 69 //}
 70 
 71 
 72 //-----------------------------------
 73 // 方式三:关联对象
 74 - (void)setAge:(int)age{
 75 
 76     // 优化 ①
 77     // objc_setAssociatedObject(self, ageKey, @(age), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
 78     
 79     
 80     // 优化 ②:注意第二个参数传进的是地址
 81     // objc_setAssociatedObject(self, &ageKey, @(age), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
 82     
 83     
 84     // 优化 ③:这种方式看起来更直观,和属性名一样
 85     // objc_setAssociatedObject(self, @"age", @(age), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
 86     // NSLog(@"%p",@"age"); // 0x100001030
 87     
 88     // 优化 ⑤:直接传入 setter\getter的方法地址,建议 getter,操作方便
 89     // 好处就是可读性高,而且输入有提示,帮助排错
 90     objc_setAssociatedObject(self, @selector(age), @(age), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
 91 }
 92 
 93 - (int)age{
 94     
 95     // 优化 ②
 96     // return [objc_getAssociatedObject(self, &ageKey) intValue];
 97     
 98     
 99     // 优化 ③:内存地址一样一样的
100     // NSLog(@"%p",@"age");  // 0x100001030
101     // NSLog(@"%p",age_key); // 0x100001030
102     // return [objc_getAssociatedObject(self, age_key) intValue];
103     
104     
105      // 优化 ⑤ :我们知道每个方法都有两个隐藏参数 (id)self和 _cmd:(SEL)_cmd
106      // _cmd = @selector(age)
107      return [objc_getAssociatedObject(self, _cmd) intValue];
108      // 同 return [objc_getAssociatedObject(self, @selector(age)) intValue];
109 }
110 
111 @end

// - main.m

 1 #import <Foundation/Foundation.h>
 2 #import "Animal.h"
 3 #import "Animal+Pet.h"
 4 int main(int argc, const char * argv[]) {
 5     
 6 //    //-------------------------------------------------------------
 7 //    // 方式一:全局变量
 8 //    Animal *an1A = [[Animal alloc] init];
 9 //    an1A.age = 10;
10 //    NSLog(@"%d",an1A.age);
11 //    // 貌似解决了设值\取值的问题
12 //    // 但是实例对象的成员变量是人手一份
13 //    // 使用全局变量独一份的基本要求就无法实现,并且生命周过长(内存泄露);还有线程安全问题
14 //
15 //    Animal *an1B = [[Animal alloc] init];
16 //    an1B.age = 18;
17 //    NSLog(@"%d",an1B.age); // 18
18 //    NSLog(@"%d",an1A.age); // 18
19 
20     
21 //    //-----------------------------------------------------------
22 //    // 方式二:使用字典,利用键值对保证实例对象的唯一性
23 //    Animal *an2A = [[Animal alloc] init];
24 //    an2A.age = 10;
25 //    Animal *an2B = [[Animal alloc] init];
26 //    an2B.age = 18;
27 //
28 //    NSLog(@"%d",an2A.age); // 10
29 //    NSLog(@"%d",an2B.age); // 18
30 //    // 但是依旧存在内存泄露(全局变量)、线程安全问题
31     
32     
33     //-----------------------------------------------------------
34     // 方式三:关联对象
35     Animal *an3A = [[Animal alloc] init];
36     an3A.age = 100;
37     Animal *an3B = [[Animal alloc] init];
38     an3B.age = 180;
39     NSLog(@"%d",an3A.age); // 100
40     NSLog(@"%d",an3B.age); // 180
41 
42     
43     return 0;
44     
45 }

关联对象底层实现

1 - objc_setAssociatedObjec

① 内部调用了 _object_set_associative_reference函数

② 关联对象的技术对象有 AssociationsManager、AssociationsHashMap、ObjectAssociationMap、ObjcAssociation四个

通过对底层代码分析,关联对象工作原理如图示:关联的对象并不是存储在被关联对象的本身内存中,而是存储在全局统一的一个 AssociationsManager中

2 - 移除关联对象

① 单独移除一个实例对象:给关联对象置 nil就相当于移除了关联对象。我们在分类 Animal+Pet中添加新属性 name并实现方法

@property(nonatomic,copy)NSString *name;
1 - (void)setName:(NSString *)name{
2     
3     objc_setAssociatedObject(self, @selector(name), name, OBJC_ASSOCIATION_COPY_NONATOMIC);
4 }
5 
6 - (NSString *)name{
7     
8     return objc_getAssociatedObject(self, _cmd);
9 }

// - main.m

 1     // 方式三:关联对象
 2     Animal *an3C = [[Animal alloc] init];
 3     an3C.age = 1022;
 4     an3C.name = @"tudou";
 5     NSLog(@"%@",an3C.name); // tudou
 6     
 7     // 移除关联对象
 8     an3C.name = nil;// 就相当于在 setter方法中传进了 nil
 9     // objc_setAssociatedObject(self, @selector(name), nil, OBJC_ASSOCIATION_COPY_NONATOMIC);
10     NSLog(@"%@",an3C.name); // null

② 进入 _object_set_associative_reference源码分析

就是把关联对象 name从 AssociationMap移除

③ 移除所有关联对象 objc_removeAssociatedObjects

打开 _object_remove_assocations

其实质是莫掉了整个实例对象

objc_AssociationPolicy

1 - 关联策略中的 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     // crash: 实例对象出了作用域 R就会自动销毁,如果关联策略 assign有 weak功能
13     // 那么 an3E销毁时会置 nil,但是下面代码就崩掉了,说明它的确只是 assign
14     NSLog(@"%@", objc_getAssociatedObject(ani3D, @"an3E"));
15     // 报错 message sent to deallocated instance 0x102a04860
16     
17     return 0;
18     
19 }

2 - 关联策略所队形的修饰符

 

 

推荐阅读