面试题 - Category
Category能否添加成员变量?如果可以,如何给Category添加成员变量?
学习内容
一. 如何给分类添加成员变量
思考一下:分类中能否添加属性?能否添加成员变量?
我们知道,如果在类中添加如下属性,
@property (assign, nonatomic) int age;
编译器会自动帮我们做下面三件事:
1.生成_开头的成员变量
{
int _age;
}
2.生成set、get方法的声明
- (void)setAge:(int)age;
- (int)age;
3.生成set、get方法的实现
- (void)setAge:(int)age
{
_age = age;
}
- (int)age
{
return _age;
}
在OC-Category实现的原理中,我们知道Category的底层数据结构:
struct category_t {
const char *name;
classref_t cls;
struct method_list_t *instanceMethods; //对象方法列表
struct method_list_t *classMethods; //类方法列表
struct protocol_list_t *protocols; //协议列表
struct property_list_t *instanceProperties; //属性列表
// Fields below this point are not always present on disk.
struct property_list_t *_classProperties;
method_list_t *methodsForMeta(bool isMeta) {
if (isMeta) return classMethods;
else return instanceMethods;
}
property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
};
可以看出,分类也可以添加属性,但是在分类中添加属性只会生成set、get方法的声明,不会生成_开头的成员变量,也不会生成set、get方法的实现。
直接上代码验证一下:创建一个Person
类和它的分类Person + Test1
,然后在Person + Test1
中添加属性@property (assign, nonatomic) int weight
;
@interface MJPerson : NSObject
@property (assign, nonatomic) int age;
@end
#import "MJPerson.h"
@implementation MJPerson
@end
//MJPerson+Test.h
#import "MJPerson.h"
@interface MJPerson (Test)
@property (assign, nonatomic) int weight;
@property (copy, nonatomic) NSString* name;
@end
#import "MJPerson+Test.h"
#define MJKey [NSString stringWithFormat:@"%p", self]
@implementation MJPerson (Test)
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
MJPerson *person = [[MJPerson alloc] init];
person.age = 10;
person.weight = 40;
MJPerson *person2 = [[MJPerson alloc] init];
person2.age = 20; // 20是存储在peron2对象内部
person2.weight = 50; // 50是存放在全局的字典对象里面
NSLog(@"person - age is %d, weight is %d", person.age, person.weight);
NSLog(@"person2 - age is %d, weight is %d", person2.age, person2.weight);
}
return 0;
}
RUN>
2021-04-21 12:47:33.842094+0800 Interview01-Category的成员变量[2266:107137] -[MJPerson setWeight:]: unrecognized selector sent to instance 0x100413a90 2021-04-21 12:47:33.843029+0800 Interview01-Category的成员变量[2266:107137] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[MJPerson setWeight:]: unrecognized selector sent to instance 0x100413a90'
会发现找不到setWeight
方法,我们给一个类添加属性,实际上是做了3个事情:
1: 添加
_weight
成员变量;
2: 添加setter,getter
方法声明;
3: 添加setter,getter
方法实现;
我们自己手动添加不就好了,我们在分类中添加如下代码:
#import "MJPerson.h"
@interface MJPerson (Test)
{
int _weight;
}
@property (assign, nonatomic) int weight;
@property (copy, nonatomic) NSString* name;
- (void)setWeight:(int)weight;
- (int)weight;
@end
连编译都不成功:
Instance variables may not be placed in categories
现在我们知道了,分类中不能直接添加成员变量,其实从分类的底层结构也能看出来,分类并没有存放成员变量的地方。
分类中可以添加属性,但是分类中添加属性只会生成set、get方法的声明,分类中又不能直接添加成员变量,但是可以使用关联对象间接实现Category有成员变量的效果。
set、get方法的实现我们可以自己写,这个好解决,成员变量怎么办呢?首先你肯定会想到用全局变量
@implementation MJPerson (Test)
int weight_;
- (void)setWeight:(int)weight
{
weight_ = weight;
}
- (int)weight
{
return weight_;
}
@end
RUN>
2021-04-21 12:57:54.719306+0800 Interview01-Category的成员变量[2353:112565] person - age is 10, weight is 50 2021-04-21 12:57:54.719779+0800 Interview01-Category的成员变量[2353:112565] person2 - age is 20, weight is 50
但是,不能使用全局变量,使用全局变量所有的person对象使用的都是一个weight_值了。
上面没有做到一对一,如果使用字典就能做到一对一了,可以实现需求吗?
#define MJKey [NSString stringWithFormat:@"%p", self]
@implementation MJPerson (Test)
NSMutableDictionary *names_;
NSMutableDictionary *weights_;
+ (void)load
{
weights_ = [NSMutableDictionary dictionary];
names_ = [NSMutableDictionary dictionary];
}
- (void)setWeight:(int)weight
{
weights_[MJKey] = @(weight);
}
- (int)weight
{
return [weights_[MJKey] intValue];
}
@end
RUN>
2021-04-21 13:02:44.317810+0800 Interview01-Category的成员变量[2394:115475] person - age is 10, weight is 40 2021-04-21 13:02:44.318341+0800 Interview01-Category的成员变量[2394:115475] person2 - age is 20, weight is 50
可以发现,使用字典把当前对象的指针当做key,也是可以实现,但是这样也有问题:
- 字典一直在内存中,内存泄漏问题
- 不同的对象可能会在不同的线程同时访问这个字典,线程安全问题
- 比较麻烦
二. 使用关联对象给分类添加成员变量-key的常见用法
因为分类底层结构的限制,不能添加成员变量到分类中,但可以通过关联对象来间接实现。
关联对象提供了以下API:
添加关联对象:
/**
设置关联对象
@param object 传入的对象,会和value关联起来,最后一个对象对应一个value
@param key 传入一个指针作为key,取值的时候用
@param value 关联的值,一个value关联一个object
@param policy 关联策略
*/
void objc_setAssociatedObject(id object, const void * key,
id value, objc_AssociationPolicy policy)
关联策略:
typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
OBJC_ASSOCIATION_ASSIGN = 0, //相当于assign
OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, //相当于strong, nonatomic
OBJC_ASSOCIATION_COPY_NONATOMIC = 3, //相当于copy, nonatomic
OBJC_ASSOCIATION_RETAIN = 01401, //相当于strong, atomic
OBJC_ASSOCIATION_COPY = 01403 //相当于copy, atomic
};
获取关联对象:
/**
获取关联对象
@param object 和value关联的对象
@param key 根据传入的指针key取值
@return 取值返回的就是value
*/
id objc_getAssociatedObject(id object, const void * key)
移除所有的关联对象:
/**
移除某个对象所有的关联对象
*/
void objc_removeAssociatedObjects(id object)
关于key,这个key要求传入一个指针,设置关联对象传入的key和获取关联对象传入的key要一样,下面有四种方式设置key:
static void *MyKey = &MyKey;// MyKey存的是自己的地址值,而且没必要赋值,因为我们只用地址
objc_setAssociatedObject(obj, MyKey, value, OBJC_ASSOCIATION_RETAIN_NONATOMIC)
objc_getAssociatedObject(obj, MyKey)
static char MyKey; //char类型的MyKey只占一个字节,内存占用更小
objc_setAssociatedObject(obj, &MyKey, value, OBJC_ASSOCIATION_RETAIN_NONATOMIC)
objc_getAssociatedObject(obj, &MyKey)
//使用属性名作为key
//直接将字符串字面量传进去,就相当于将字符串的地址值传进去,因为NSString *p = @"property"
objc_setAssociatedObject(obj, @"property", value, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
objc_getAssociatedObject(obj, @"property");
//使用get方法的@selecor作为key,就相当于将SEL对象的地址值传进去 (推荐)
objc_setAssociatedObject(obj, @selector(getter), value, OBJC_ASSOCIATION_RETAIN_NONATOMIC)
objc_getAssociatedObject(obj, @selector(getter))
-
key
的第一种写法: -指针变量
objc_setAssociatedObject
API中的key
的类型是:const void * _Nonnull key
,我们也可以创建这种类型的指针变量:
@implementation MJPerson (Test)
static const void *MJNameKey = &MJNameKey;
static const void *MJWeightKey = &MJWeightKey;
- (void)setName:(NSString *)name
{
objc_setAssociatedObject(self, MJNameKey, name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (NSString *)name
{
return objc_getAssociatedObject(self, MJNameKey);
}
- (void)setWeight:(int)weight
{
objc_setAssociatedObject(self, MJWeightKey, @(weight), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (int)weight
{
return [objc_getAssociatedObject(self, MJWeightKey) intValue];
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
MJPerson *person = [[MJPerson alloc] init];
person.age = 10;
person.name = @"jack";
person.weight = 30;
MJPerson *person2 = [[MJPerson alloc] init];
person2.age = 20;
person2.name = @"rose";
person2.weight = 50;
NSLog(@"person - age is %d, name is %@, weight is %d", person.age, person.name, person.weight);
NSLog(@"person2 - age is %d, name is %@, weight is %d", person2.age, person2.name, person2.weight);
}
return 0;
}
RUN>
2021-04-21 13:41:07.159749+0800 Interview01-Category的成员变量[2509:123335] person - age is 10, name is jack, weight is 30 2021-04-21 13:41:07.160194+0800 Interview01-Category的成员变量[2509:123335] person2 - age is 20, name is rose, weight is 50
key
的第二种写法: - 使用变量地址
既然key
的类型是void *
类型,也就是说key
可以是任何类型的指针对象,所以我们可以声明char nameKey , char cityKey
变量,然后再取出变量的地址(之所以是char
类型是因为char
类型只占2个字节,当然也可以是int
类型)
static const char MJNameKey;
static const char MJWeightKey;
- (void)setName:(NSString *)name
{
objc_setAssociatedObject(self, &MJNameKey, name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (NSString *)name
{
return objc_getAssociatedObject(self, &MJNameKey);
}
- (void)setWeight:(int)weight
{
objc_setAssociatedObject(self, &MJWeightKey, @(weight), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (int)weight
{
return [objc_getAssociatedObject(self, &MJWeightKey) intValue];
}
run>
2021-04-21 13:43:29.907380+0800 Interview01-Category的成员变量[2530:124825] person - age is 10, name is jack, weight is 30 2021-04-21 13:43:29.907773+0800 Interview01-Category的成员变量[2530:124825] person2 - age is 20, name is rose, weight is 50
key
的第三种写法: - 直接使用字符串
#define MJNameKey @"name"
#define MJWeightKey @"weight"
- (void)setName:(NSString *)name
{
objc_setAssociatedObject(self, MJNameKey, name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (NSString *)name
{
return objc_getAssociatedObject(self, MJNameKey);
}
- (void)setWeight:(int)weight
{
objc_setAssociatedObject(self, MJWeightKey, @(weight), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (int)weight
{
return [objc_getAssociatedObject(self, MJWeightKey) intValue];
}
RUN>
2021-04-21 13:44:59.889910+0800 Interview01-Category的成员变量[2552:126019] person - age is 10, name is jack, weight is 30 2021-04-21 13:44:59.890345+0800 Interview01-Category的成员变量[2552:126019] person2 - age is 20, name is rose, weight is 50
为什么字符串在这里也可以呢?因为字符串存储在内存中的常量区,在内存中只存储一份,他的地址始终是一样的.我们平常写的
NSString *str = @"name"
,实际上就是把@"name"
的地址赋值给str
.所以这里直接写@"name"
,也相当于写地址.
key
的第四种写法: - @selector
- (void)setName:(NSString *)name
{
objc_setAssociatedObject(self, @selector(name), name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (void)setWeight:(int)weight
{
objc_setAssociatedObject(self, @selector(weight), @(weight), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (NSString *)name
{
return objc_getAssociatedObject(self, @selector(name));
}
- (int)weight
{
return [objc_getAssociatedObject(self, @selector(weight)) intValue];
}
RUN>
2021-04-21 13:48:24.265249+0800 Interview01-Category的成员变量[2579:128048] person - age is 10, name is jack, weight is 30 2021-04-21 13:48:24.265714+0800 Interview01-Category的成员变量[2579:128048] person2 - age is 20, name is rose, weight is 50
@selector
同一个方法在内存中的地址都是一样的,并且@selector
拼写的时候还有提示,毫无疑问这种写法是最好的.补充1:
上面的object是id类型的,所以理论上给什么对象添加关联都可以,上面我们是给实例对象添加关联对象,因为每个实例对象都不一样。如果给类对象添加关联对象,也是可以的,但是由于类对象只有一个,添加的关联对象也是唯一的,这样做没什么意义(MJExtension中有给MJProperty类对象添加关联对象)。
补充2:关于_cmd
_cmd是当前方法的@selector,就是@selector(当前方法名)。
其实每个方法都有两个隐式参数:self和_cmd,比如上面的name方法也可以写成:- (int)weight { // _cmd == @selector(weight) return [objc_getAssociatedObject(self, _cmd) intValue]; } - (NSString *)name { // 隐式参数 // _cmd == @selector(name) return objc_getAssociatedObject(self, _cmd); }
那么现在我们就实现了间接的往类中添加成员变量了,那么这些成员变量添加到哪里了呢?
三. 关联对象的原理
关联对象不是存储在原来实例对象和类对象里面。
关联对象是通过Runtime实现的,想要知道它内部是怎么实现的可以直接查看源码,打开objc4,搜索“ objc_setAssociatedObject(”
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
void _object_set_associative_reference(id object, void *key, id value, uintptr_t policy) {
// retain the new value (if any) outside the lock.
ObjcAssociation old_association(0, nil);
id new_value = value ? acquireValue(value, policy) : nil;
{
AssociationsManager manager;
AssociationsHashMap &associations(manager.associations());
//根据传入的object生成一个key
disguised_ptr_t disguised_object = DISGUISE(object);
if (new_value) {
// break any existing association.
AssociationsHashMap::iterator i = associations.find(disguised_object);
if (i != associations.end()) {
// secondary table exists
ObjectAssociationMap *refs = i->second;
ObjectAssociationMap::iterator j = refs->find(key);
if (j != refs->end()) {
old_association = j->second;
j->second = ObjcAssociation(policy, new_value);
} else {
(*refs)[key] = ObjcAssociation(policy, new_value);
}
} else {
// create the new association (first time).
ObjectAssociationMap *refs = new ObjectAssociationMap;
associations[disguised_object] = refs;
(*refs)[key] = ObjcAssociation(policy, new_value);
object->setHasAssociatedObjects();
}
} else {
// setting the association to nil breaks the association.
AssociationsHashMap::iterator i = associations.find(disguised_object);
if (i != associations.end()) {
ObjectAssociationMap *refs = i->second;
ObjectAssociationMap::iterator j = refs->find(key);
if (j != refs->end()) {
old_association = j->second;
refs->erase(j);
}
}
}
}
// release the old value (outside of the lock).
if (old_association.hasValue()) ReleaseValue()(old_association);
}
可以发现,关联对象就是通过上面这个函数实现的,我们发现实现关联对象技术的核心对象有:
AssociationsManager 关联对象管理者
AssociationsHashMap
ObjectAssociationMap
ObjcAssociation
我们就先梳理他们四个之间的关系:
首先进入AssociationsManager,发现里面有一个AssociationsHashMap
class AssociationsManager {
// associative references: object pointer -> PtrPtrHashMap.
static AssociationsHashMap *_map;
......
}
进入AssociationsHashMap,发现里面又有一个map,其中key是disguised_ptr_t,value是ObjectAssociationMap
class AssociationsHashMap : public unordered_map<disguised_ptr_t, ObjectAssociationMap *>
进入ObjectAssociationMap,发现里面也有一个map,其中key是void *,value是ObjcAssociation
class ObjectAssociationMap : public std::map<void *, ObjcAssociation>
进入ObjcAssociation,发现里面只有_policy和_value两个值:
class ObjcAssociation {
uintptr_t _policy;
id _value;
}
可以发现,我们传进去的_policy和_value,存放在ObjcAssociation里面,并没有在原来实例对象和类对象里面。
那么我们传进去的object和key在哪呢?观察下图:
现在我们就明白了,通过关联对象技术,间接的给类添加成员变量时,并不会添加到类本身的空间,而是通过一个类似全局字典的东西在存储,我们结合一下源码配上注释再看看:
void _object_set_associative_reference(id object, void *key, id value, uintptr_t policy) {
// retain the new value (if any) outside the lock.
ObjcAssociation old_association(0, nil);
id new_value = value ? acquireValue(value, policy) : nil;
{
AssociationsManager manager;
//从 manager 中获取 AssociationsHashMap类型的 associations
AssociationsHashMap &associations(manager.associations());
//通过传入的 objc 通过位移 生成key : disguised_object
disguised_ptr_t disguised_object = DISGUISE(object);
if (new_value) {
// break any existing association.
// 把 disguised_object 当做 key ,从 associations 中得到 AssociationsHashMap类型的 i
AssociationsHashMap::iterator i = associations.find(disguised_object);
if (i != associations.end()) {
// secondary table exists
// 通过 i 找到 ObjectAssociationMap 类型的 refs
ObjectAssociationMap *refs = i->second;
// 根据传进来的 key ,从 refs 中找到 ObjectAssociationMap 类型的 j
ObjectAssociationMap::iterator j = refs->find(key);
if (j != refs->end()) {
old_association = j->second;
// 把传递传递进来的 policy ,new_value 保存到 j 中
j->second = ObjcAssociation(policy, new_value);
} else {
(*refs)[key] = ObjcAssociation(policy, new_value);
}
} else {
// create the new association (first time).
ObjectAssociationMap *refs = new ObjectAssociationMap;
associations[disguised_object] = refs;
(*refs)[key] = ObjcAssociation(policy, new_value);
object->setHasAssociatedObjects();
}
} else {
// 当传入的 value 为 nil 时
// setting the association to nil breaks the association.
AssociationsHashMap::iterator i = associations.find(disguised_object);
if (i != associations.end()) {
ObjectAssociationMap *refs = i->second;
ObjectAssociationMap::iterator j = refs->find(key);
if (j != refs->end()) {
old_association = j->second;
// 移除所关联的对象
refs->erase(j);
}
}
}
}
// release the old value (outside of the lock).
if (old_association.hasValue()) ReleaseValue()(old_association);
}
当传递进来的关联对象为
nil
时,源码中有一个移除的操作,相当于删除关联对象.
objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key)
的源码也是按照类似取值的:
id _object_get_associative_reference(id object, void *key) {
id value = nil;
uintptr_t policy = OBJC_ASSOCIATION_ASSIGN;
{
AssociationsManager manager;
//从 manager 中 取出 AssociationsHashMap 类型的 associations
AssociationsHashMap &associations(manager.associations());
//把传递进来的被关联对象 通过 位移,得到一个 disguised_object 作为 key
disguised_ptr_t disguised_object = DISGUISE(object);
// 把 作为 key ,从 associations 中 得到 AssociationsHashMap i
AssociationsHashMap::iterator i = associations.find(disguised_object);
if (i != associations.end()) {
// 从 遍历器 i 中 得到 ObjectAssociationMap 类型的 refs
ObjectAssociationMap *refs = i->second;
// 从 ObjectAssociationMap 中 得到 ObjectAssociationMap 类型的 j
ObjectAssociationMap::iterator j = refs->find(key);
if (j != refs->end()) {
// 从 ObjectAssociationMap 中取值
ObjcAssociation &entry = j->second;
value = entry.value();
policy = entry.policy();
if (policy & OBJC_ASSOCIATION_GETTER_RETAIN) {
objc_retain(value);
}
}
}
}
if (value && (policy & OBJC_ASSOCIATION_GETTER_AUTORELEASE)) {
objc_autorelease(value);
}
return value;
}
总结:
- 分类中可以添加属性,但是系统只会给这个属性添加
setter,getter
方法的声明,不会添加成员变量和setter,getter
的实现. - 如果直接在分类中添加成员变量会报错,我们可以通过
关联对象
这种技术,间接的给分类添加成员变量. - 通过
关联对象
间接给分类添加的成员变量,并没有添加到类本身的内存结构中,而是由runtime
生成了一个类似全局变量的字典来管理,也就是AssociationsManager
. - 设置关联对象为
nil
,相当于移除对象. - 移除所有的关联对象用objc_removeAssociatedObjects
面试题 - Category
Category能否添加成员变量?如果可以,如何给Category添加成员变量?
直接在分类中添加成员变量会报错,我们可以通过
关联对象
这种技术,间接的给分类添加成员变量通过
关联对象
间接给分类添加的成员变量,并没有添加到类本身的内存结构中,而是由runtime
生成了一个类似全局变量的字典来管理,也就是AssociationsManager
.
特别备注
本系列文章总结自MJ老师在腾讯课堂iOS底层原理班(下)/OC对象/关联对象/多线程/内存管理/性能优化,相关图片素材均取自课程中的课件。如有侵权,请联系我删除,谢谢!