1、能否在分类中添加成员变量

如果在分类中直接添加成员变量

  1. @interface Person (Test1) {
  2. int _weight;
  3. }

系统会报错:
Instance variables may not be placed in categories (实例变量不能放在类别中)
说明分类中不能直接添加成员变量。
如果在分类中添加属性:

  1. @interface Person (Test1)
  2. @property (nonatomic, assign) int weight;
  3. @end

当调用set或者get方法时,控制台会报错:

  1. ~:-[Person setWeight:]: unrecognized selector sent to instance 0x1007160e0
  2. ~:-[Person weight]: unrecognized selector sent to instance 0x105011920

说明分类可以添加属性,但是添加属性后,只会自动添加set和get方法声明,不会实现set和get方法。

2、利用字典给分类添加成员变量

  1. 声明一个全局字典,用于保存属性
  2. NSMutableDictionary *dic_;
  3. + (void)load {
  4. dic_ = [[NSMutableDictionary alloc] init];
  5. }
  6. - (void)setWeight:(int)weight {
  7. NSString *key = [NSString stringWithFormat:@"%p",self];
  8. dic_[key] = @(weight);
  9. }
  10. - (int)weight {
  11. NSString *key = [NSString stringWithFormat:@"%p",self];
  12. return [dic_[key] intValue];
  13. }

通过分类中的字典可以保存分类里添加属性的值,但是有以下几个缺点
1、内存泄漏,字典是全局变量,会一直保存在内存中。
2、线程安全,不同的对象在不同的线程同时访问分类中的成员变量,可能会导致读写错误。
3、代码量比较大,实现起来比较麻烦。

3、关联对象

3.1、基本用法

关联对象主要方法有:

  1. // 添加关联对象
  2. objc_setAssociatedObject(id object, const void * key,
  3. id value, objc_AssociationPolicy policy);
  4. // 读取关联对象
  5. objc_getAssociatedObject(id object, const void * key);
  6. // 移除所有关联对象
  7. objc_removeAssociatedObjects(id _Nonnull object);

关联策略对应关系:
image.png
代码举例:

  1. - (void)setName:(NSString *)name {
  2. objc_setAssociatedObject(self, @"key", name, OBJC_ASSOCIATION_COPY);
  3. }
  4. - (NSString *)name {
  5. return objc_getAssociatedObject(self, "key");
  6. }

3.2、key的用法

3.2.1、使用变量地址作为key

  1. static const void *NameKey = &NameKey;
  2. - (void)setName:(NSString *)name {
  3. objc_setAssociatedObject(self, NameKey, name, OBJC_ASSOCIATION_COPY);
  4. }
  5. - (NSString *)name {
  6. return objc_getAssociatedObject(self, NameKey);
  7. }

缺点:声明一个全局指针变量,占用8个字节,且书写麻烦。

3.2.2、使用char类型变量地址作为key

  1. static const char NameKey;
  2. - (void)setName:(NSString *)name {
  3. objc_setAssociatedObject(self, &NameKey, name, OBJC_ASSOCIATION_COPY);
  4. }
  5. - (NSString *)name {
  6. return objc_getAssociatedObject(self, &NameKey);
  7. }

优点:char类型只占用1个字节
缺点:书写麻烦

3.2.3、使用字符串作为key

  1. #define NameKey @"name"
  2. - (void)setName:(NSString *)name {
  3. objc_setAssociatedObject(self, NameKey, name, OBJC_ASSOCIATION_COPY);
  4. }
  5. - (NSString *)name {
  6. return objc_getAssociatedObject(self, NameKey);
  7. }

缺点:书写麻烦

3.2.4、使用方法地址作为key(推荐)

  1. - (void)setName:(NSString *)name {
  2. objc_setAssociatedObject(self, @selector(name), name, OBJC_ASSOCIATION_COPY);
  3. }
  4. - (NSString *)name {
  5. return objc_getAssociatedObject(self, _cmd);
  6. }

优点:可读性较高、如果方法名写错系统会有提示,书写简单。
其中_cmd == @selector(name),OC方法有两个隐式参数,一个是self,一个是_cmd,比如name函数可能是:

  1. - (NSString *)nameSelf:(id)self cmd:(SEL):_cmd {
  2. }

4、关联对象原理

查看 objc源码 objc-references.mm中的_object_set_associative_reference方法和_object_get_associative_reference方法:

  1. _object_get_associative_reference(id object, const void *key)
  2. _object_set_associative_reference(id object, const void *key, id value, uintptr_t policy){}

可以看出关联对象过程中主要用到一下几个数据结构:
AssociationsManager、AssociationsHashMap、ObjectAssociationMap、ObjcAssociation

  1. typedef DenseMap<const void *, ObjcAssociation> ObjectAssociationMap;
  2. typedef DenseMap<DisguisedPtr<objc_object>, ObjectAssociationMap> AssociationsHashMap;
  3. class AssociationsManager {
  4. ......
  5. AssociationsHashMap &get() {
  6. return _mapStorage.get();
  7. }
  8. ......
  9. };
  10. class ObjcAssociation {
  11. uintptr_t _policy;
  12. id _value;
  13. ......
  14. }

它们的结构关系如下:
image.png
所有实例对象的关联对象都由一个AssociationsManager管理,其内部有一个AssociationsHashMap对象,AssociationsHashMap内部保存着实例对象(object)和存储着实例对象对应自己关联对象的ObjectAssociationMap,ObjectAssociationMap内部存储着关联对象的key和对应关联对象的value和策略。

5、总结

1、关联对象并不是存储在被关联对象本身
2、关联对象存储在全局统一的一个AssociationsManager中
3、移除关联对象将关联对象设置nil即可(通过查看源码可知,当value==nil时会调用erase方法)
4、移除所有关联对象,调用objc_removeAssociatedObjects(id _Nonnull object)
5、如果被关联对象销毁,对应的关联属性也都会销毁