前言

Tip5 - 谈对象了,关联对象

相关目标是用来为分类增加成员变量时运用的,那么为什么分类需求运用相关目标来增加成员变量呢?那肯定是由于正常的增加成员变量的方式在分类中不能用。

一般咱们在类中声明一个特点,代码是这样的:

@interface Animal: NSObject
@property (nonatomic, copy) NSString *name;
@end

然后编译器会帮咱们生成如下办法:

@implementation Animal {
    NSString *_name;
}
- (NSString *)name {
    return _name;
}
- (void)setName:(NSString *)name {
    _name = name;
}

便是:

  • 生成一个实例变量 _name
  • 生成 getter 办法
  • 生成 setter 办法

编译器帮咱们生成了一个实例变量,然后存到在类结构中。

当咱们尝试往一个分类中去增加一个特点:

@interface Animal (Category)
@property (nonatomic, copy) NSString *gender;
@end

咱们会得到以下警告:

Tip5 - 谈对象了,关联对象

Property 'gender' requires method 'gender' to be defined - use @dynamic or provide a method implementation in this category

意思便是 gender 特点的存取办法需求自己去手动完成,或许运用 @dynamic 在运行时完成这些办法。

由于在分类中,虽然能够经过 @property 来增加特点,可是不会主动生成私有成员变量,也不会生成 setget 办法,只会生成 setget 的声明,详细的办法完成需求咱们自己去完成。

从 Category 讲起

至于为什么不主动给分类生成私有成员变量,最直接的原因便是分类不支持,先来看看类的结构,它在源码中是一个名为 objc_class 的结构体:

struct objc_class {
    Class _Nonnull isa;
    struct objc_ivar_list * _Nullable ivars; // 成员变量列表
    struct objc_method_list * _Nullable * _Nullable methodLists; // 办法列表
    struct objc_protocol_list * _Nullable protocols; // 协议列表
    ...
}

其间有三个成员:

  • ivars:成员变量列表
  • methodLists:办法列表
  • protocols:协议列表

分类在源码中的结构如下,它是一个名为 category_t 的结构体:

struct category_t {
    const char *name; // 对应的类名
  WrappedPtr<method_list_t, method_list_t::Ptrauth> instanceMethods; // 实例办法列表
  WrappedPtr<method_list_t, method_list_t::Ptrauth> classMethods; // 类办法列表
  struct protocol_list_t *protocols; // 协议列表
  struct property_list_t *instanceProperties; // 特点列表
    ...
}

它包含:

  • instanceMethods:实例办法列表
  • protocols:协议列表
  • instanceProperties:特点列表

能够看到它是没有成员变量列表的,这便是为什么分类不允许增加成员变量的最直接的原因,从结构规划上就不支持。

为什么要这么规划呢?

其实得问苹果为什么要这么规划,我个人觉得规划成能够增加成员变量好像也没毛病?虽然我没想出来是为啥,不过还是放一份别人的答案,看懂的同学能够指点下:

总得来说便是,Category 是在运行时才会被运行时库(也便是 Runtime)加载到内存中,而类的内存布局在编译时就已经确定了,不能够再更改。

所以不允许增加成员变量,是由于增加成员变量会影响到 “类实例” 的内存布局,所谓 “类实例”,它便是咱们创立出来的类目标,它是一块包含 isa 指针和一切的成员变量的内存区域,咱们不能在运行时再去改变它,而成员变量的增加会直接影响到它的内存的,所以不允许在运行时再增加成员变量。

而办法/协议/特点不属于 “类实例” 这个概念,它们归类管,也便是 objc_class,不论如何增删,都不会影响到 “类实例” 的内存,所以能够随意增删。

详细的原因我还没想明白,可是不论咋说,从源码上看,苹果它就不支持你去在分类中增加成员变量,假如咱们想达到向正常类那样去运用一个特点(会主动生成实例变量和 set/get 办法),那么咱们能够借助相关目标(主角进场的有点晚)。

当然,相关目标和正常的成员变量在底层是大不相同的,不过运用相关目标完成的特点和正常的特点在运用上并无二致。

运用相关目标

Animal 的分类中增加一个 age 特点:

#import "objc/runtime.h"
@interface Animal (Category)
@property (nonatomic, copy) NSString *categoryName;
@end
@implementation Animal (Category)
- (void)setCategoryName:(NSString *)categoryName {
  objc_setAssociatedObject(self, @"categoryName", categoryName, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (NSString *)categoryName {
  return objc_getAssociatedObject(self, @"categoryName");
}
@end

运用时:

Animal *animal = [[Animal alloc] init];
animal.categoryName = @"Tom";
NSLog(@"%@", animal.categoryName);

控制台输出:

Tom

就像正常特点相同进行运用即可。

相关目标的完成原理

经过上面的比如,能够看到两个关键的 api,objc_setAssociatedObjectobjc_getAssociatedObject,一个是设置,一个是获取,咱们到源码中看看它们做了什么。

void
objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
{
  _object_set_associative_reference(object, key, value, policy);
}

老样子,又是调了另一个办法:

void
_object_set_associative_reference(id object, const void *key, id value, uintptr_t policy)
{
    DisguisedPtr<objc_object> disguised{(objc_object *)object}; // (1)
  ObjcAssociation association{policy, value}; // (2)
    AssociationsManager manager; (3)
  AssociationsHashMap &associations(manager.get()); (4)
    auto refs_result = associations.try_emplace(disguised, ObjectAssociationMap{});
    auto &refs = refs_result.first->second;
  auto result = refs.try_emplace(key, std::move(association));
    ...
    // 开释旧值
    association.releaseHeldValue();
}

这儿省掉了部分代码,咱们需求留意里面的几个类和数据结构,在详细剖析代码之前,需求先了解它们的作用。

  • DisguisedPtr
  • ObjcAssociation
  • AssociationsManager
  • AssociationsHashMap
  • ObjectAssociationMap

DisguisedPtr 是一个 class

class DisguisedPtr {
  uintptr_t value;
    ...
}

它只要一个成员,经过 (1) 咱们能够看到,它传入的是 object,对应的便是 objc_setAssociatedObject(self, @"categoryName", categoryName, OBJC_ASSOCIATION_COPY_NONATOMIC); 中的 self

也便是将 self 放到了一个类中。

再看 ObjcAssociation

class ObjcAssociation {
  uintptr_t _policy;
  id _value;
    ...
}

ObjcAssociation 只要两个成员,_policyobjc_AssociationPolicy,它是一个枚举:

typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
  OBJC_ASSOCIATION_ASSIGN = 0,
  OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1,
  OBJC_ASSOCIATION_COPY_NONATOMIC = 3,
  OBJC_ASSOCIATION_RETAIN = 01401,
  OBJC_ASSOCIATION_COPY = 01403
};

value 便是相关目标对应的值。

DisguisedPtr 相似,而且经过 (2) 能够看出,是将相关目标的 OBJC_ASSOCIATION_COPY_NONATOMIC 和相关目标的 value,也便是 categoryName 一起放入了这个目标。

接下来便是 AssociationsManager

class AssociationsManager {
  using Storage = ExplicitInitDenseMap<DisguisedPtr<objc_object>, ObjectAssociationMap>;
  static Storage _mapStorage;
public:
  AssociationsManager()  { AssociationsManagerLock.lock(); }
  ~AssociationsManager() { AssociationsManagerLock.unlock(); }
  AssociationsHashMap &get() {
    return _mapStorage.get();
  }
  static void init() {
    _mapStorage.init();
  }
};

其间的 AssociationsManagerLock 是一个自旋锁:

spinlock_t AssociationsManagerLock;

所以它有一个 spinlock_t(自旋锁)和 AssociationsHashMap 单例,在 &get 办法中回来的是一个大局的 AssociationsHashMap 单例,然后 AssociationsManager 经过持有一个 spinlock_t 来确保对 AssociationsHashMap 的操作是线程安全的。

(4) 中便是经过 &get 办法,拿到了 AssociationsHashMap

AssociationsHashMap 的界说为:

typedef DenseMap<DisguisedPtr<objc_object>, ObjectAssociationMap> AssociationsHashMap;

是一个 [DisguisedPtr : ObjectAssociationMap] 的字典,ObjectAssociationMap 的界说如下:

typedef DenseMap<const void *, ObjcAssociation> ObjectAssociationMap;

是一个 [void * : ObjcAssociation] 的字典,而 ObjcAssociation 咱们前面提到过,它存放了相关目标的修饰符和相关目标的值,也能够说它便是咱们所说的 “相关目标” 实践在内存中的结构。

看到这儿,咱们能够得到相关目标的一个存储结构,以咱们在 Animal 中的 categoryName 为例,假如将 categoryName 设置为 Tom,它在内存中是这么存储的:

Tip5 - 谈对象了,关联对象

所以相关目标是经过一个大局的单例类来办理的,存储的结构也是一个哈希表的结构,所以咱们能够幻想出来,当需求一个目标增加了相关目标,它会以线程安全的方式(加锁)存储到一个大局的表中,key 是目标的 id(地址),而 value 是相关目标的修饰符和值。

那么开释的机遇咱们也很简单揣度出来,只需求在目标的析构办法中,去这个大局的表中以目标的 id 为 key 移除去与它相关的相关目标即可。

dealloc 办法中去找:

dealloc -> rootDealloc -> object_dispose -> objc_destructInstance -> _object_remove_assocations_object_remove_assocations 便是开释相关目标的办法,界说如下:

void
_object_remove_assocations(id object, bool deallocating)
{
    ...
}

能够看到传入了析构目标自身(object),然后再根据 object 去大局的表中查找该 object 所对应的值,然后移除即可。

源码

剖析完原理之后,咱们来看一下 objc_setAssociatedObjectobjc_getAssociatedObject 两个办法的详细完成。

void
_object_set_associative_reference(id object, const void *key, id value, uintptr_t policy)
{
  if (!object && !value) return;
  if (object->getIsa()->forbidsAssociatedObjects())
    _objc_fatal("objc_setAssociatedObject called on instance (%p) of class %s which does not allow associated objects", object, object_getClassName(object));
  DisguisedPtr<objc_object> disguised{(objc_object *)object};
  ObjcAssociation association{policy, value};
  association.acquireValue();
  bool isFirstAssociation = false;
  {
    AssociationsManager manager;
    AssociationsHashMap &associations(manager.get());
    if (value) {  // a
      auto refs_result = associations.try_emplace(disguised, ObjectAssociationMap{});
      if (refs_result.second) {
        isFirstAssociation = true;
      }
      auto &refs = refs_result.first->second;
      auto result = refs.try_emplace(key, std::move(association));
      if (!result.second) {
        association.swap(result.first->second);
      }
    } else {
      auto refs_it = associations.find(disguised);
      if (refs_it != associations.end()) {
        auto &refs = refs_it->second;
        auto it = refs.find(key);
        if (it != refs.end()) {
          association.swap(it->second);
          refs.erase(it);
          if (refs.size() == 0) {
            associations.erase(refs_it);
          }
        }
      }
    }
  }
  if (isFirstAssociation)
    object->setHasAssociatedObjects();
  association.releaseHeldValue();
}

提个留意点便是 // a 处,分两种情况:

  • value != nil,设置/更新相关目标的值
  • value == nil,删除相关目标

将相关目标的值设置为 nil 的话,会去删除这个相关目标。

objc_getAssociatedObject 办法内部调用了 _object_get_associative_reference

id
_object_get_associative_reference(id object, const void *key)
{
  ObjcAssociation association{};
  {
    AssociationsManager manager;
    AssociationsHashMap &associations(manager.get());
    AssociationsHashMap::iterator i = associations.find((objc_object *)object);
    if (i != associations.end()) {
      ObjectAssociationMap &refs = i->second;
      ObjectAssociationMap::iterator j = refs.find(key);
      if (j != refs.end()) {
        association = j->second;
        association.retainReturnedValue();
      }
    }
  }
  return association.autoreleaseReturnedValue();
}

只放了源码,没有详细到每一条的剖析,个人觉得看个大约,了解原理就能够了。

总结

  • 相关目标在源码中其实便是 ObjcAssociation 目标。
  • ObjcAssociation 存放在 ObjectAssociationMap 中,然后以目标的指针为 keyObjectAssociationMapvalue,存放在 AssociationsHashMap 中。
  • AssociationsHashMap 是一个哈希表,由 AssociationsManager 办理,AssociationsManager 是一个大局的单例,持有 AssociationsHashMapAssociationsHashMap 也是大局仅有的一张表。
  • 目标在析构函数中,经过 has_assoc 标记位判别目标是否有相关目标,有的话会调用 _object_remove_assocations 办法移除相关相关目标。

参考

相关目标 AssociatedObject 彻底解析