源码

Category的本质三关联对象

Category的本质<一>

Category的本质<二>load,initialize方法

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

我们首先创建一个类Person类继承自NSObject,给这个类声明一个属性name:

@property (nonatomic, strong)NSString *name;

我们声明了这句话之后,实际是做了三件事:

  • 1.声明了一个成员变量_name。

NSString *_name;
  • 2.声明了set和get方法:

- (void)setName:(NSString *)name;
- (NSString *)name;
  • 3.在.m文件中实现set和get方法:

- (void)setName:(NSString *)name{
    
    _name = name;
}

- (NSString *)name{
    
    return _name;
}

以上是给一个类添加属性。下面给一个分类添加属性:

我们创建一个Person类的分类Test1,然后给这个分类添加一个height属性:

@property (nonatomic, assign)int height;

这样只会申明set和get方法,而不会申明成员变量和实现set,get方法:

- (void)setHeight:(int)height;
- (int)height;

既然系统没有帮我们声明成员变量和实现set和get方法,那么我们能不能自己去声明一下呢?我们尝试一下:

出现了报错Instance variables may not be placed in categories,意思就是成员变量不能声明在分类中。所以我们得出结论,分类中不能添加成员变量。

我们从分类的结构的角度来考虑一下分类中为什么不能添加成员变量:

通过分类的底层结构我们可以看到,分类中可以存放实例方法,类方法,协议,属性,但是没有存放成员变量的地方。

既然分类中不能添加成员变量,那么我们给分类添加属性时,它的功能不是完整的,比如说我们分别给Person类的name和height这两个属性赋值,然后打印读出这两个属性:

Person *person = [[Person alloc] init];
person.name = @"dongdong";
person.height = 180;
    
NSLog(@"name: %@, height : %d", person.name, person.height);

程序崩溃了,崩溃原因是:-[Person setHeight:]: unrecognized selector sent to instance 0x60400020a0d0,意思就是给这个person对象发送了没有实现的消息:setHeight:,这应该是在我们的预料之中,为什么呢?因为我们在分类中声明age这个属性的时候,不像在类中声明属性一样,系统只会声明set和get方法,而不会在.m中去实现set和get方法,因此导致了程序崩溃。因此我们在分类的.m文件中去实现set和get方法:

//Person+Test1.m文件
- (void)setHeight:(int)height{
    
}

- (int)height{
    
    return 0;
}

再次运行代码,这次程序不崩溃了,打印结果是:

Category[9030:308848] name: dongdong, height : 0

我们看到name属性赋值成功了,而height属性显然没有赋值成功。

person.height = 180;

这句代码显然是调用了set方法,但是在分类中的set方法什么也没有实现,没有存储下这个设置的值180。

NSString *_name;

0

实则是调用了get方法,由于不能保存传递过来的height值,所以上面的代码中我们返回固定值0。

而name属性能够赋值和读取成功,是因为在其set方法中用_name这个成员变量保存的赋的值:

NSString *_name;

1

在其get方法中利用_name成员变量返回存储的值:

NSString *_name;

2

所以如果我们在分类的.m文件中保存传递过来的值,然后在取值的时候返回存储的值,那么应该也能实现属性的完整功能。

方法一 全局变量

第一种方法是使用全局变量来存储传递进来的值:

NSString *_name;

3

然后我们运行一下程序:

NSString *_name;

4

这次好像是赋值成功了,返回也对。我们再把height改成190试试:

NSString *_name;

5

这次打印的也是对的,那么这样是不是就真的可以完全实现属性的功能呢?

问题在于,height_是全局变量,所有的对象共用这一个全局变量,如果有个对象的height值变了,其他的对象的height值也会跟着改变,也是不符合我们的需求的,我们可以测试一下:

NSString *_name;

6

打印结果:

NSString *_name;

7

所以这种方法就被pass掉了。

方法二 字典

第一种方法全局变量失败的原因就是不能做到每个对象和自己的height值一一对应。这就让我们想到了一个数据结构-字典。假如我们通过键值对的形式存放height值,这样是否可以呢?我们使用person对象指向的地址作为键,将height值作为值存储在字典中:

NSString *_name;

8

NSString *_name;

9

打印结果:

- (void)setName:(NSString *)name;
- (NSString *)name;

0

所以采用字典这种方式是完全可行的。

使用字典存在的问题:

  • 1.非线程安全

    由于这个字典是全局的,所有的对象的height属性值都是存储在这个全局字典里面,当不同的对象在不同的线程同时访问这个全局字典时,这个时候就容易产生线程安全问题,需要去加线程锁,有些复杂。

  • 2.需要创建多个全局字典

    刚才已经看到了,我们需要为分类中的每一个属性值创建一个全局字典,这是非常麻烦又复杂的事。

方法三 关联对象

关联对象使用的是runtime的API:

- (void)setName:(NSString *)name;
- (NSString *)name;

1

- (void)setName:(NSString *)name;
- (NSString *)name;

2

我们再给Person类的分类声明一个属性:

- (void)setName:(NSString *)name;
- (NSString *)name;

3

然后我们使用关联对象的方法给sex这个属性赋值和取值:

- (void)setName:(NSString *)name;
- (NSString *)name;

4

- (void)setName:(NSString *)name;
- (NSString *)name;

5

打印结果:

- (void)setName:(NSString *)name;
- (NSString *)name;

6

我们发现打印结果是正确的。

但是这里存在一个问题就是我们设置的key没有赋值,也即是sexKey相当于NULL,假如我们再给height属性设置一个key为heightKey,那么这个heightKey也是NULL,那么在get方法中通过key值来取得值时,由于属性的key都是一样的,所以就很容易出错。

  • 方法一

因此我们需要给这个sexKey赋值一个独一无二的值:

- (void)setName:(NSString *)name;
- (NSString *)name;

7

这句话就是直接将sexKey这个指针的地址值赋给自己。对于height:

- (void)setName:(NSString *)name;
- (NSString *)name;

8

由于这两个指针分类在不同的内存地址中,所以heightKey和sexKey可以保证是不相同的,这样就能在get方法中取出正确的值。

  • 方法二

上面这种方式实在是非常啰嗦又累赘,我们要声明指针,初始化指针,下面介绍一种更简单的方法:

- (void)setName:(NSString *)name;
- (NSString *)name;

9

我们直接把@"sex"这个字符串传进去作为key,这样就不用声明指针又初始化了。有人就有疑问了,这里的key明明要求是指针类型的,我们传进一个字符串可以吗?我们分析一下下面这句代码:

- (void)setName:(NSString *)name{
    
    _name = name;
}

- (NSString *)name{
    
    return _name;
}

0

这里name变量是一个指针变量。那么我们为什么能用一个字符串去初始化一个指针变量呢?原因就是这里传进去的是@"dongdong"这个字符串的地址。这样我们就能明白,上面@"sex"其实传进去的也是这个字符串的地址。

为了防止误写,我们还可以把字符串抽成宏:

- (void)setName:(NSString *)name{
    
    _name = name;
}

- (NSString *)name{
    
    return _name;
}

1

  • 方法三

第二种方法已经非常简便了,但是为了方便准确我们还要把字符串抽成宏。有没有更加简便的方法呢?我们可以尝试传进一个方法的地址作为key,比如说set或get方法:

- (void)setName:(NSString *)name{
    
    _name = name;
}

- (NSString *)name{
    
    return _name;
}

2

这里传进去的key是@selector(sex),也就是sex这个get方法的地址。当然我们也可以传进set方法的地址作为key。最后我们还可以更进一步的简化:

- (void)setName:(NSString *)name{
    
    _name = name;
}

- (NSString *)name{
    
    return _name;
}

3

这里在get方法里把@selector(sex)换成了_cmd,这是因为我们使用的key是sex这个方法的地址,在这个方法内部,我们可以直接使用_cmd获取本方法。那这样就非常方便简洁了。

关联对象的原理

set方法

objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key, id _Nullable value, objc_AssociationPolicy policy)方法

我们直接去runtime的源码中去查看关联对象的具体实现,直接搜索objc_setA,

  • 1.选择objc-runtime.mm这个文件中的实现:

- (void)setName:(NSString *)name{
    
    _name = name;
}

- (NSString *)name{
    
    return _name;
}

4

  • 2.点进_object_set_associative_reference(object, (void *)key, value, policy);这个真实的实现函数:

- (void)setName:(NSString *)name{
    
    _name = name;
}

- (NSString *)name{
    
    return _name;
}

5

这个函数的实现看起来非常复杂,都是C++的语法,对于不了解C++的人来说非常困难,不过没关系,即便我们看不懂上面的代码,通过下面的分析,我们也能明白关联对象的原理:

实现关联对象技术的核心对象有:

  • AssociationsManager

  • AssociationsHashMap

  • ObjectAssociationMap

  • ObjectAssociation

这里面经常出现Map这个东西,这其实和我们Objective-c中的字典是一样的,我们可以把它当字典来看待。在第二种方法里面我们是用字典去实现的,这里又出现了和字典相似的结构,那它们的实现会不会相似呢?

在上面的一大段源码中,我们在开头的位置找到这一句:

- (void)setName:(NSString *)name{
    
    _name = name;
}

- (NSString *)name{
    
    return _name;
}

6

我们点进AssociationsManager查看其结构:

前面讲了Map类型是字典,那么什么是key,什么是value呢?然后我们继续点进AssociationsHashMap:

我们前面也讲了,ObjectAssociationMap这个结构也是字典,那么这个字典里面装的是什么呢?我们点进去看看:

那这个ObjcAssociation又是什么东西呢?我们进去看看:

总结一下上面四个核心对象的结构:

下面这张图总结的是这四个核心对象之间的联系:

那么问题来了,objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key, id _Nullable value, objc_AssociationPolicy policy)中的四个参数分别对应上面结构中的哪个结构呢?

下图就展示了它们的对应关系:

拿我们之前写的作为例子:

- (void)setName:(NSString *)name{
    
    _name = name;
}

- (NSString *)name{
    
    return _name;
}

7

这句代码中,self也就是person对象被赋给了AssociationHashMap的key,而@selector(sex)的地址被赋给了AssociationMap的key,策略OBJC_ASSOCIATION_COPY_NONATOMIC被赋值给了ObjectAssociation的policy,传递进来的值sex被赋值给了ObjectAssociation的value。

这种设计的巧妙之处就在于:

当一个person对象不光有一个属性值要关联时,比如我们要关联height和sex这两个属性时,我们以person对象作为key,然后值是AssociationMap这个字典类型,在这个字典类型中,分别使用@selector(sex)和@selector(height)作为key,然后分别利用sex属性的policy和传递进来的value和height属性的policy和传递进来的value生成ObjectAssociation作为value。而如果有多个person对象需要关联时,我们只需要在AssociationHashMap中创造更多的键值对就可以解决这个问题。

通过这个过程我们也能明白:

关联对象的值它不是存储在自己的实例对象的结构中,而是维护了一个全局的结构AssociationManager

get方法

objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key)方法

经过了上面的分析,基本上就对set方法的原理比较清楚了,下面我们直接看一下get方法的源码:

  • 1.在runtime的源码中找到这个函数:

- (void)setName:(NSString *)name{
    
    _name = name;
}

- (NSString *)name{
    
    return _name;
}

8

  • 2.点进_object_get_associative_reference(object, (void *)key);

回答面试题

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

答:不能直接给Category添加成员变量,但是可以间接实现Category有成员变量的效果。我们可以使用runtime的API,objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key, id _Nullable value, objc_AssociationPolicy policy)和objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key)这两个来实现。

作者:雪山飞狐_91ae

链接:https://www.jianshu.com/p/4b463169a84a

(1)

本文由 投稿者 创作,文章地址:https://blog.isoyu.com/archives/categorydebenzhisanguanlianduixiang.html
采用知识共享署名4.0 国际许可协议进行许可。除注明转载/出处外,均为本站原创或翻译,转载前请务必署名。最后编辑时间为:8 月 13, 2018 at 07:52 下午

热评文章

发表回复

[必填]

我是人?

提交后请等待三秒以免造成未提交成功和重复