由问题 category能否添加成员变量,如果可以,如何给category添加成员变量?
答案:不可以直接给category添加成员变量,但是可以通过关联对象实现添加成员变量的效果
关联对象的本质
关联对象由AssociationsManager管理并在AssociationsHashMap存储
所有对象的关联内容都在同一容器中
...
我们在一个MJPerson类里用@property声明一个属性
@property (assign, nonatomic) int age;
比如上句
其实做了三件事
@interface MJPerson : NSObject
{
int _age; //第一步声明_age成员变量
}
- (void)setAge:(int)age; //第二步声明age的set和get方法
- (int)age;
@end
@implementation MJPerson
- (void)setAge:(int)age //第三步实现age的set和get方法
{
_age = age;
}
- (int)age
{
return _age;
}
@end
如果是在MJPerson的分类里用@property声明一个属性
@property (assign, nonatomic) int age;
比如上句,只会做下面事
- (void)setAge:(int)age; //只会声明age的set和get方法
- (int)age;
既然分类的属性不帮我们实现成员变量和age的set和get方法的实现,那么我们能不能自己来实现呢。首先在分类里定义成员变量系统会报错,不允许。那么我们还是先把set和get方法实现,如下。
@implementation MJPerson
- (void)setAge:(int)age
{
//_age = age; //分类里成员变量不允许生成,所以这句话报废了
}
- (int)age
{
//return _age; //分类里成员变量不允许生成,所以这句话也报废了
}
@end
由此发现我们自己实现的set和get方法实现都是空架子,怎么办呢?
下面一步步来改造
新建MJPerson类
#import
@interface MJPerson : NSObject
@property (nonatomic , assign) int age;
@end
#import "MJPerson.h"
@implementation MJPerson
@end
新建MJPerson分类
#import "MJPerson.h"
@interface MJPerson (Category)
@property (nonatomic , assign) int num;
@end
#import "MJPerson+Category.h"
@implementation MJPerson (Category)
int num_;//新建一个全局变量,既然分类不给我实现成员变量,那么我就自己找个变量中转下。在@implementation里声明的变量,其getter/setter 方法只在该类中可以。
- (void)setNum:(int)num {
num_ = num;
}
- (int)num {
return num_;
}
@end
然后控制器里如下调用
- (void)viewDidLoad {
[super viewDidLoad];
MJPerson *person = [[MJPerson alloc]init];
person.age = 1;
person.num = 2;
NSLog(@"person的age = %d,person的num = %d", person.age, person.num);
MJPerson *person1 = [[MJPerson alloc]init];
person1.age = 3;
person1.num = 4;
NSLog(@"person的age = %d,person的num = %d", person.age, person.num);
NSLog(@"person1的age = %d,person1的num = %d", person1.age, person1.num);
}
看到打印
2019-11-27 10:02:40.820360+0800 Test[1445:35155] person的age = 1,person的num = 2
2019-11-27 10:02:40.820468+0800 Test[1445:35155] person的age = 1,person的num = 4
2019-11-27 10:02:40.820542+0800 Test[1445:35155] person1的age = 3,person1的num = 4
虽然可以实现分类里的num的保存,但是可以看到因为num_是全局变量,所以person和person1的num用了同一份变量,显然这种方法不太靠谱,下面接着改造
新建MJPerson类
#import
@interface MJPerson : NSObject
@property (nonatomic , assign) int age;
@end
#import "MJPerson.h"
@implementation MJPerson
@end
新建MJPerson分类
#import "MJPerson.h"
@interface MJPerson (Category)
@property (nonatomic , assign) int num;
@end
#import "MJPerson+Category.h"
@implementation MJPerson (Category)
NSMutableDictionary *dic;
+(void)load{
dic = [NSMutableDictionary dictionary];
}
- (void)setNum:(int)num {
NSString *key = [NSString stringWithFormat:@"%p",self];
dic[key] = @(num);
}
- (int)num {
NSString *key = [NSString stringWithFormat:@"%p",self];
return [dic[key] intValue];
}
@end
然后控制器里如下调用
- (void)viewDidLoad {
[super viewDidLoad];
MJPerson *person = [[MJPerson alloc]init];
person.age = 1;
person.num = 2;
NSLog(@"person的age = %d,person的num = %d", person.age, person.num);
MJPerson *person1 = [[MJPerson alloc]init];
person1.age = 3; //3是存储在person1的对象内部
person1.num = 4; //4是存储在全局的字典对象里面
NSLog(@"person的age = %d,person的num = %d", person.age, person.num);
NSLog(@"person1的age = %d,person1的num = %d", person1.age, person1.num);
}
看到打印
2019-11-27 10:08:57.343514+0800 Test[1551:38984] person的age = 1,person的num = 2
2019-11-27 10:08:57.343618+0800 Test[1551:38984] person的age = 1,person的num = 2
2019-11-27 10:08:57.343700+0800 Test[1551:38984] person1的age = 3,person1的num = 4
由此看到实现了num的保存,且互不干扰,但是这种做法有一定的线程安全问题,同时如果分类里属性过多,需要很多字典,异常复杂。
关联对象(将值和对象关联起来,所以叫关联对象)
假设分类里有个age属性,可以把set和get方法改造成下列形式
const void *ageKey = &ageKey; //ageKey指向的内存存的是自己的地址,定义为const void *是因为关联方法的key是这种类型的
- (void)setAge:(int)age
//objc_setAssociatedObject添加关联对象
//四个值
//object :对象
//key: 唯一值
//value: 值
//objc_AssociationPolicy:关联模式,可选copy,retain,assin。没有weak,没有weak意味着如果释放对象是直接销毁,不是置nil
objc_setAssociatedObject(self, ageKey, @(age), OBJC_ASSOCIATION_ASSIGN);
}
- (int)age
return [objc_getAssociatedObject(self, ageKey) intValue];
}
由此,完成了关联对象
如果新增分类属性name是string类型
const void *nameKey = &nameKey;
- (void)setName:(NSString *)name
{
objc_setAssociatedObject(self, nameKey, name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (NSString *)name
{
return objc_getAssociatedObject(self, nameKey);
}
上面代码定义为const全局变量了,能被外界访问,不太好(比如控制器里用extern来声明一个变量extern const void *nameKey,那么通过NSLog(@"%p",nameKey);就能打印出nameKey里面存的值(就是&nameKey))
改进
在const前个 static 让其变为局部变量
static const void *nameKey = &nameKey;
- (void)setName:(NSString *)name
{
objc_setAssociatedObject(self, nameKey, name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (NSString *)name
{
return objc_getAssociatedObject(self, nameKey);
}
因为key是void *类型,也就是只要是指针就行,所以key还可以更简单
static const char nameKey; //还保留const是因为key里面有const,所以为了一致性
然后key需要为&nameKey,因为不关心nameKey指针里存了什么,而是指针地址值
static const char nameKey;
-(void)setName:(NSString *)name {
objc_setAssociatedObject(self, &nameKey, name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (NSString *)name {
return objc_getAssociatedObject(self, &nameKey);
}
还有一种很简单的方法,连key值都不用定义了
- (void)setName:(NSString *)name
{
objc_setAssociatedObject(self, @"name", name, OBJC_ASSOCIATION_COPY_NONATOMIC); //写@"name"也行的原因是因为key接收的是指针,@"name"等同于将NSString *str = @"name"的str传进去了,所以是可行的;
}
- (NSString *)name
{
//return objc_getAssociatedObject(self, @selector(name));
// get方法里 _cmd == @selector(name),_cmd代指当前方法,所以可以用_c md替代
return objc_getAssociatedObject(self, @"name");
}
还有一种连key值都不用定义了的很简单的方法
- (void)setName:(NSString *)name
{
objc_setAssociatedObject(self, @selector(name), name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (NSString *)name
{
//return objc_getAssociatedObject(self, @selector(name));
// get方法里 _cmd == @selector(name),_cmd代指当前方法,所以可以用_cmd替代
return objc_getAssociatedObject(self, _cmd);
}
上述论证很精彩,那么关联对象原理呢?
看源码
搜索objc_setAssociatedObject可以在objc-runtime.mm文件里看到这个方法
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy) {
_object_set_associative_reference(object, (void *)key, value, policy);
}
点击_object_set_associative_reference可以在objc-references.mm文件看到方法_object_set_associative_reference
void _object_set_associative_reference(id object, void *key, id value, uintptr_t policy) {
// This code used to work when nil was passed for object and key. Some code
// probably relies on that to not crash. Check and handle it explicitly.
// rdar://problem/44094390
if (!object && !value) return;
assert(object);
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));
// retain the new value (if any) outside the lock.
ObjcAssociation old_association(0, nil);
id new_value = value ? acquireValue(value, policy) : nil; //根据传进来的value以及策略生成一个new_value,基本也就是原值,额外匹配了内存管理的意思。
{
//关联对象管理类,C++实现的一个类
AssociationsManager manager;
//全局容器
AssociationsHashMap &associations(manager.associations());
//DISGUISE函数对传进来的object地址进行位运算,然后作为容器的key
disguised_ptr_t disguised_object = DISGUISE(object);
if (new_value) {
// break any existing association.
//根据disguised_object指针查找对应的一个ObjectAssociationMap
AssociationsHashMap::iterator i = associations.find(disguised_object);
if (i != associations.end()) { //找到了ObjectAssociationMap
// 获取ObjectAssociationMap
ObjectAssociationMap *refs = i->second;
//根据key去查找ObjcAssociation
ObjectAssociationMap::iterator j = refs->find(key);
if (j != refs->end()) {//查找到了ObjcAssociation
old_association = j->second;
//生成新的ObjcAssociation替换掉ObjectAssociationMap里key对应的 的value
j->second = ObjcAssociation(policy, new_value);
} else {//没有查到ObjcAssociation
//生成新的ObjcAssociation作为ObjectAssociationMap的value
(*refs)[key] = ObjcAssociation(policy, new_value);
}
} else {//没找到ObjectAssociationMap,即该object第一次关联对象
// 创建新的ObjectAssociationMap (first time).
ObjectAssociationMap *refs = new ObjectAssociationMap;
//ObjectAssociationMap作为disguised_object对应的值
associations[disguised_object] = refs;
//然后根据策略和new_value组装一个ObjcAssociation,作为ObjectAssociationMap里的key对应的 value
(*refs)[key] = ObjcAssociation(policy, new_value);
//设置这个object是有了关联对象的
object->setHasAssociatedObjects();
}
} else { //如果value是空
//根据disguised_object指针查找对应的一个ObjectAssociationMap
AssociationsHashMap::iterator i = associations.find(disguised_object);
if (i != associations.end()) {
ObjectAssociationMap *refs = i->second;
//根据key去查找ObjcAssociation
ObjectAssociationMap::iterator j = refs->find(key);
if (j != refs->end()) {
old_association = j->second;
//找到了并擦除ObjcAssociation。所以如果想取消已关联对象的一个值,可以传这个值为nil
refs->erase(j);
}
}
}
}
// release the old value (outside of the lock).
if (old_association.hasValue()) ReleaseValue()(old_association);
}
点击AssociationsManager看到如下AssociationsManager定义如下:
class AssociationsManager {
// associative references: object pointer -> PtrPtrHashMap.
static AssociationsHashMap *_map;
public:
AssociationsManager() { AssociationsManagerLock.lock(); }
~AssociationsManager() { AssociationsManagerLock.unlock(); }
AssociationsHashMap &associations() {
if (_map == NULL)
_map = new AssociationsHashMap();
return *_map;
}
};
在看这段代码之前,先看一下几个数据结构以及它们之间的关系。
实现关联对象技术的核心对象有 (看到map可以理解为字典)
AssociationsManager
AssociationsHashMap
ObjectAssociationMap
ObjcAssociation
1.AssociationsManager 是顶级的对象,维护了一个从 spinlock_t 锁到 AssociationsHashMap 哈希表的单例键值对映射;
2.AssociationsHashMap 是一个无序的哈希表,维护了从对象地址到 ObjectAssociationMap 的映射;
3.ObjectAssociationMap 是一个 C++ 中的 map ,维护了从 key 到 ObjcAssociation 的映射,即关联记录;
4.ObjcAssociation 是一个 C++ 的类,表示一个具体的关联结构,主要包括两个实例变量,_policy 表示关联策略,_value 表示关联对象。
每一个对象地址对应一个 ObjectAssociationMap 对象,而一个 ObjectAssociationMap 对象保存着这个对象的若干个关联记录。
上述核心对象关系简言之就是:
首先objc_setAssociatedObject会创建一个AssociationsManager;AssociationsManager里面维护了一个AssociationsHashMap 哈希表的单例键值对映射,由AssociationsHashMap来存储所有的关联对象(这相当于把所有对象的关联对象都存在一个全局map里面),这个 map的的key是传进来的这个对象的指针地址(任意两个不同对象的指针地址一定是不同的),而这个map的value对应ObjectAssociationMap。ObjectAssociationMap里面维护了从 key 到ObjcAssociation的映射,即关联记录,其中key就是关联对象里面自己定义的key值,value对应ObjcAssociation。ObjcAssociation表示一个具体的关联结构,主要包括两个实例变量,_policy 表示关联策略,_value 表示关联对象。
上述过程比如有MJPerson类,并为其创建分类,那么初始化MJPerson的实例对象时,初始化几个实例对象,每个实例对象都分别调用分类的属性,那么这几个实例对象的指针地址分别会在AssociationsHashMap的key里占有一席之位。
AssociationsHashMap里就会存储如下
MJPerson的实例对象1的指针 :ObjectAssociationMap
MJPerson的实例对象2的指针 :ObjectAssociationMap
...
上述_object_set_associative_reference源码的大致意思就是
根据传进来的value以及策略生成一个new_value(看源码基本也就是原值,额外匹配了内存管理的意思)。
如果new_value为空,那么根据对象地址找到ObjectAssociationMap,然后擦除
如果new_value不为空,那么根据对象地址获取ObjectAssociationMap,如果ObjectAssociationMap不存在,那么会创建一个新的ObjectAssociationMap关联对象。获取ObjectAssociationMap后,根据外界传进来的key查找ObjcAssociation,如果ObjcAssociation不存在会新建并直接存入新的ObjcAssociation。如果ObjcAssociation存在,会持有旧的ObjcAssociation,存入新的ObjcAssociation,然后释放旧的ObjcAssociation。
看懂了 objc_setAssociatedObject 函数后,objc_getAssociatedObject 函数对我们来说就是小菜一碟了。这个函数先根据对象地址在 AssociationsHashMap 中查找其对应的 ObjectAssociationMap 对象,如果能找到则进一步根据 key 在 ObjectAssociationMap 对象中查找这个 key 所对应的关联结构 ObjcAssociation ,如果能找到则返回 ObjcAssociation 对象的 value 值,否则返回 nil。
关联对象提供了以下API
添加关联对象
void objc_setAssociatedObject(id object, const void * key,
id value, objc_AssociationPolicy policy)
获得关联对象
id objc_getAssociatedObject(id object, const void * key)
移除所有的关联对象
void objc_removeAssociatedObjects(id object)
如何定义 Weak Associated Object
然而有时候,安全起见,我们需要在对象和 associated object 之间建立 weak 关系,而非 assign 关系,如何实现呢?
首先定义一个简单对象,该对象提供一个被 weak 修饰的属性:
@interface WeakAssociatedObjectWrapper : NSObject
@property (nonatomic, weak) id object;
@end
@implementation WeakAssociatedObjectWrapper
@end
其次
@interface UIView (ViewController)
@property (nonatomic, weak) UIViewController *vc;
@end
@implementation UIView (ViewController)
- (void)setVc:(UIViewController *)vc {
WeakAssociatedObjectWrapper *wrapper = [WeakAssociatedObjectWrapper new];
wrapper.object = vc;
objc_setAssociatedObject(self, @selector(vc), wrapper, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (UIViewController *)vc {
WeakAssociatedObjectWrapper *wrapper = objc_getAssociatedObject(self, _cmd);
return wrapper.object;
}
@end
提醒
关联对象并不是存储在被关联对象本身内存中
关联对象存储在全局的统一的一个AssociationsManager中
设置关联对象为nil,就相当于是移除关联对象 // 类(分类).属性 = nil