前言
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 - 关联策略所队形的修饰符