Category 里面的关联对象

439 阅读3分钟

我们知道分类编译完以后,它里面是一个这样的数据结构:

struct _category_t {
	const char *name;                                // 类的名字
	struct _class_t *cls;                            // 指向的类
	const struct _method_list_t *instance_methods;   // 实例对象方法列表
	const struct _method_list_t *class_methods;      // 类方法列表
	const struct _protocol_list_t *protocols;        // 协议列表
	const struct _prop_list_t *properties;           // 属性列表
};

从它的数据结构可以看出它可以添加方法,属性, 实现协议等,但是没有成员变量的内存结构。但是在实际的使用中,我们还是能够使用 关联对象 实现类似成员变量的功能。

关联对象的实现

在分类中使用关联对象功能主要有下面方法:

  • 设置关联对象:
// object:一般为传入的关联的实例对象
//   key:一个指针地址作为关联数据的key(获取数据的时候需要)
// value: 具体的数据
// policy:存储策略
objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key, id _Nullable value, objc_AssociationPolicy policy)
  • 获取关联对象:
objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key)
  • 移除关联对象:
objc_removeAssociatedObjects(id _Nonnull object)

一个例子

添加测试代码:

// ------------------------- Cat -------------------------

@interface Cat : NSObject

@property (nonatomic, assign) NSInteger age;

@end

@implementation Cat

@end

// ------------------------- Cat (Name) -------------------------

@interface Cat (Name)

/*
 在分类中添加的属性,只会有 get 和 set 方法的声明,不会生成 get 和 set 方法的实现 (当然也不会有对应的实例变量)。
 如果不自己添加 get 和 set 方法,使用的时候会运行崩溃。
 错误信息:Terminating app due to uncaught exception 'NSInvalidArgumentException',
         reason: '-[Cat setName:]: / -[Cat name] unrecognized selector sent to instance 0xxxxxxxxxxxxx
 */
@property (nonatomic, copy) NSString *name;

@end

@implementation Cat (Name)

// 设置名字
- (void)setName:(NSString *)name {
    // 在例子中使用 @selector(name) 方法的指针地址作为key
    objc_setAssociatedObject(self, @selector(name), name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

// 获取名字
- (NSString *)name {
    return objc_getAssociatedObject(self, @selector(name));
}

@end

里面有类 Cat,分类 Cat (Name)

测试:

#import <Foundation/Foundation.h>
#import "Cat+Name.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Cat *cat = [[Cat alloc] init];
        cat.age = 7;
        cat.name = @"Tom";
    
        NSLog(@"%@'s age is %ld",  cat.name, (long)cat.age);
    }
    return 0;
}

输出:

2021-01-05 17:42:46.940627+0800 AssociatedObject[15920:3977195] Tom's age is 7

可以看到在分类 Cat (Name) 里面添加的 name 属性现在也能像在类 Cat 里的属性一样使用了。

runtime 文件里面可以看到有如下存储策略:

/* Associative References */

/**
 * Policies related to associative references.
 * These are options to objc_setAssociatedObject()
 */
typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
    OBJC_ASSOCIATION_ASSIGN = 0,           /**< Specifies a weak reference to the associated object. */
    OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, /**< Specifies a strong reference to the associated object. 
                                            *   The association is not made atomically. */
    OBJC_ASSOCIATION_COPY_NONATOMIC = 3,   /**< Specifies that the associated object is copied. 
                                            *   The association is not made atomically. */
    OBJC_ASSOCIATION_RETAIN = 01401,       /**< Specifies a strong reference to the associated object.
                                           *   The association is made atomically. */
    OBJC_ASSOCIATION_COPY = 01403          /**< Specifies that the associated object is copied.
                                            *   The association is made atomically. */
};

在实际情况可如下使用:

OBJC_ASSOCIATION_ASSIGNassign

OBJC_ASSOCIATION_RETAIN_NONATOMICnonatomic, strong

OBJC_ASSOCIATION_COPY_NONATOMICnonatomic, copy

OBJC_ASSOCIATION_RETAINatomic, strong

OBJC_ASSOCIATION_COPYatomic, copy

关联对象的内部数据结构

objc 源码下载地址

首先到苹果objc源码官网下载一个最新的包,这里下载的是 objc4-781.tar

直接搜索 objc_setAssociatedObject 方法,发现它的接口在 objc-runtime.mm 文件中,但是实现在 objc-references.mm 文件中。

接口:

实现:

objc_getAssociatedObjectobjc_removeAssociatedObjects 方法也可以通过类似方式找到)

通过查找可以发现是大致这样一个结构:

AssociationsManager:

class AssociationsManager {
   static AssociationsHashMap *hashMap;
};

AssociationsHashMap:

// DisguisedPtr 这个是不同的关联对象的 key 的包装
typedef DenseMap<DisguisedPtr<objc_object>, ObjectAssociationMap> AssociationsHashMap;

ObjectAssociationMap:

// const void * 是某个关联对象里面属性的 key
typedef DenseMap<const void *, ObjcAssociation> ObjectAssociationMap;

ObjcAssociation:

class ObjcAssociation {
    uintptr_t _policy;  // 存放策略
    id _value;          // 存放具体的对象
 }

画个粗糙的关系图大概是这样的:

总结

以例子中的 setName 为例:

// 设置名字
- (void)setName:(NSString *)name {
    objc_setAssociatedObject(self, @selector(name), name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
  • 一个实例对象的关联数据不是在自己的内存结构中的;

  • 它是在一个统一全局的 AssociationsManager 里面,然后里面有一个 AssociationsHashMap 保存实际的数据(可以把 AssociationsHashMap 看成类似一个字典的数据结构);

  • AssociationsHashMap 里面以实例对象的内存地址为 key (调用objc_setAssociatedObject 里面的 self), valueObjectAssociationMap 存储所有和这个对象相关的关联数据;

  • ObjectAssociationMap 里面可以有很多个这个实例对象的关联数据,key 为设置 objc_setAssociatedObject 方法时传入的 key, valueObjcAssociation 存放具体数据;

  • ObjcAssociation 里面存放了 _policy(存储策略, 为调用objc_setAssociatedObject 里面的 OBJC_ASSOCIATION_COPY_NONATOMIC), _value(具体的数据, 为调用objc_setAssociatedObject 里面的 name)。