我们知道分类编译完以后,它里面是一个这样的数据结构:
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_ASSIGN 是 assign
OBJC_ASSOCIATION_RETAIN_NONATOMIC 是 nonatomic, strong
OBJC_ASSOCIATION_COPY_NONATOMIC 是 nonatomic, copy
OBJC_ASSOCIATION_RETAIN 是 atomic, strong
OBJC_ASSOCIATION_COPY 是 atomic, copy
关联对象的内部数据结构
首先到苹果objc源码官网下载一个最新的包,这里下载的是 objc4-781.tar。
直接搜索 objc_setAssociatedObject 方法,发现它的接口在 objc-runtime.mm 文件中,但是实现在 objc-references.mm 文件中。
接口:
实现:
(objc_getAssociatedObject 和 objc_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),value为ObjectAssociationMap存储所有和这个对象相关的关联数据; -
ObjectAssociationMap里面可以有很多个这个实例对象的关联数据,key为设置 objc_setAssociatedObject 方法时传入的 key,value为ObjcAssociation存放具体数据; -
ObjcAssociation里面存放了_policy(存储策略, 为调用objc_setAssociatedObject 里面的 OBJC_ASSOCIATION_COPY_NONATOMIC),_value(具体的数据, 为调用objc_setAssociatedObject 里面的 name)。