程序员社区

OC-关联对象的原理

面试题 - Category

Category能否添加成员变量?如果可以,如何给Category添加成员变量?

学习内容

OC-关联对象的原理插图
image-20210421123509653
OC-关联对象的原理插图1
image-20210421123528192
OC-关联对象的原理插图2
image-20210421123555533

一. 如何给分类添加成员变量

思考一下:分类中能否添加属性?能否添加成员变量?

我们知道,如果在类中添加如下属性,

@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'
OC-关联对象的原理插图3
image-20210421124820651

会发现找不到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
OC-关联对象的原理插图4
image-20210421125247053

连编译都不成功:

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_值了。

OC-关联对象的原理插图5
image-20210421130048188

上面没有做到一对一,如果使用字典就能做到一对一了,可以实现需求吗?

#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,也是可以实现,但是这样也有问题:

  1. 字典一直在内存中,内存泄漏问题
  2. 不同的对象可能会在不同的线程同时访问这个字典,线程安全问题
  3. 比较麻烦

二. 使用关联对象给分类添加成员变量-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_setAssociatedObjectAPI中的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在哪呢?观察下图:

OC-关联对象的原理插图6
关联对象原理

现在我们就明白了,通过关联对象技术,间接的给类添加成员变量时,并不会添加到类本身的空间,而是通过一个类似全局字典的东西在存储,我们结合一下源码配上注释再看看:

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对象/关联对象/多线程/内存管理/性能优化,相关图片素材均取自课程中的课件。如有侵权,请联系我删除,谢谢!

赞(0) 打赏
未经允许不得转载:IDEA激活码 » OC-关联对象的原理

一个分享Java & Python知识的社区