本篇文章解决的问题
- Category 的实现原理
- Category 和 Extension 的区别是什么?
- Category 中有 load 方法吗?load 方法时什么时候调用的?load 方法能继承吗?
- load、initialize 方法的区别是什么?他们在 category 中的调用的顺序?以及出现继承时他们之间的调用过程?
- Category为什么只能加方法不能加属性?
- Category 能否添加成员变量?如果可以,如何给 Category 添加成员变量?
1. Category 的实现原理
答案:
分类在编译之后的结构是一个 _category_t
类型的结构体,它里面存储着分类的类名、对象方法、类方法、属性、协议的相关信息。在运行过程中,runtime
会将这些信息合并到类对象与元类对象中去。合并之后分类的信息会插入到原来信息的前边。
分析:
假设我们现在有如下的一个类和分类
@interface Person@end@implmentation- (void)run;@end**************************************************@interface Person (test)@end@implementation Person (test)- (void)testInstanceMethod;+ (void)testClassMethod;@end复制代码
我们回想一下,当我们的一个 Person 对象调用 run
方法时:
Person *person = [[Person alloc] init]; [person run];复制代码
实际上是编译器将代码 [person run]
转成 objc_msgSend(person, @selector(run))
,系统给 person 发送消息,找到 person 的 isa,通过 isa 找到 Person class 对象,在 Person class 对象的方法列表中去找到 run
的实现并进行调用。
现在使用 person
调用分类的方法:
[person test];复制代码
实际上的流程也还是和上面是一样的,系统会先找到 person 的 isa,通过 isa 找到 Person class 对象,在 Person class 的方法列表中找到 test 的实现并进行调用。在程序运行的过程中,系统通过 runtime 动态地将分类中的对象方法添加进了 Person class 对象的方法列表中。
如果使用 Person 调用分类的类方法,情况也是类似的:
[Person test1];复制代码
在程序运行的过程中,系统通过 runtime 动态地将分类的类方法添加到了 Person 的 meta-class 对象的方法列表中去。调用方法的时候,系统先找到 Person class 的 isa,然后找到 Person meta-class 对象,在其方法列表中找到 test1
的实现并进行调用。
我们来看一下分类的底层结构。
先创建一个工程,创建一个Person
类,在创建一个 Person+test
的分类,在里面添加好我们上面说到的方法。
进入 Person+test.m
所在的目录,执行命令:
$ xcrun -sdk iphoneos clang -arch i386 -rewrite-objc Person+test.m
将 Person+test.m
的代码转成 c/c++ 代码,然后打开它。 我们还可以找到这样一段代码
Person
,另一个是对象方法列表 &_OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_test
。 我们来看一下 &_OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_test
这个东西是什么: - (void)test;
这段代码中我们也看见了有"test"的相关描述,所以我们可以判断出 &_OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_test
就是描述分类的对象方法信息的一个东西。 综上,我们可以判断出分类在编译之后确实是编译成了 _category_t
这个类型。
我们现在得出的结论是,添加分类后:在编译时,编译器将分类编译成了 _category_t
类型的结构体,它里面存储的有分类中的类名、对象方法、类方法、属性信息、协议信息。在运行的过程中,Runtime 将所有分类对应的结构体中的对象方法列表合并到类对象的方法列表中,将类方法合并到元类对象的方法列表中。
如果阅读源码的话,还可以发现:合并之后,Category 的数据,会插入到原来数据的前面。 再联想到对象调用方法的流程,现在我们就可以解释为什么如果类和分类中都有某个方法时,总是会先调用分类中的方法了。如果多个分类中都有这个方法,调用哪个分类中的方法,和编译顺序有关。
2. Category 和 Extension 的区别是什么?
答案:
Extension 中的数据,在编译的时候就已经合并到类里边去了。Category 中在编译的时候会编译成一个结构体,在运行时才会合并到类里边去。(Extension 的作用是可以把一些需要私有化的属性、方法写在 .m 文件中去。)
3. Category 中有 load 方法吗?load 方法时什么时候调用的?load 方法能继承吗?
答案:
Category
中有load
方法。- 在
runtime
加载类、分类的时候调用,每个类、分类的load
方法,在程序运行的过程中只调用一次。 load
方法可以继承。但是一般情况不会主动去调用load
方法,都是让系统自动调用。
分析:
新创建一个工程。
创建一个类Person
,再创建其分类 Person+test
。 在 Person
和 Person+test
的实现中都加上如下代码: + (void)load{ NSLog(@"Person +load");// NSLog(@"Person (test) +load");}复制代码
然后点击运行。
我们发现了以下这些现象:
- 我们还没有在任何地方调用
+ (void)load;
方法,甚至没有在任何地方导入过类或者分类的头文件。 + (void)load;
在main函数
之前执行,因为main函数
里面的hello world
先打印。- 之前我们学习的:如果分类和类中都有某个方法,那么只会执行分类中的方法,这里却都执行了
+ (void)load;
方法。
解释这个现象需要看一下源码:
objc-os.mm
文件中,可以找到这段源码,可以看出 _objc_init
是在程序启动之初被调用的。 最后一行有一个 load_images
,点进去看一下,看到这段代码: load
方法的相关处理,最后还调用了 call_load_methods()
。因为这段代码是在程序启动时调用的,所以 load
方法是在程序启动时调用的,用官方的原话来说是: load
方法在 runtime 加载类、分类的时候调用。这也就解释了前两个现象。 继续来看 call_load_methods
做了什么:
load
方法,然后调用了类中的 load
方法: call_class_loads();
,然后又通过同样的方法直接调用了分类的 load
方法: call_categry_loads();
。它不像 objc_msgSend()
方法那样会有找 isa 、找元类对象等操作,系统会自己调用 load
方法。这解释了第三个现象。 再深入看源码的话可以发现,如果有多种类和分类,调用顺序是这样的: 先按照编译顺序调用所有类的 load
方法,再按照调用所有子类的 load
方法,最后再按照编译顺序调用所有分类的 load
方法。
4. load、initialize 方法的区别是什么?他们在 category 中的调用的顺序?以及出现继承时他们之间的调用过程?
答:
load
方法是在runtime
加载类和分类的时候,通过函数指针找到每个类和分类的load
方法,直接进行调用;initialize
是类在第一次接收消息的时候,会使用消息机制objc_msgSend(类, @selector(initialize))
去给类发送消息进行调用。- 当有分类时:类和分类的
load
方法都会被调用,因为load
的原理是找到每个类和分类的函数指针直接调用,先调用父类的load
方法,然后在按照编译顺序先后调用所有分类的load
方法;而initialize
它通过消息机制objc_msgSend()
进行调用的,所以它会通过类对象的isa
找到元类对象的方法列表,在列表中去找initialize
的实现并进行调用,如果分类中有initialize
方法,那么先找到的是分类的initialize
方法,就会先调用它。 - 出现继承时:系统会先调用所有类的
load
方法,然后再调用所有子类的load
方法,最后再调用所有分类的load
方法。当第一次给子类对象发送消息时,会先给父类发送消息objc_msgSend(父类,@selector(initialize))
,让父类去调用initialize
,然后给子类发送消息objc_msgSend(子类, @selector(initialize))
,让子类去调用initialize
;
分析:
initialize
的原理是在类第一次接受到消息的时候进行调用。比如说在第一次执行代码 [NSObject alloc]
、[Class alloc]
的时候,都向类发送了消息,这个时候都会调用。也就是说,如果我们使用了这个类,那么 initialize
就会被调用,如果我们从来都没有使用到这个类,那么 initialize
就永远都不会被调用。可以创建一个类,在类的.m文件中实现 + (void)initialize
去进行验证。
我们在在类、它的分类或者多个分类中都实现 + (void)initialize
方时,我们发现第一次个类发送消息时,只会调用一个 initialize
方法,调用的是分类中的方法,而 load
方法是:在 runtime
加载类的时候会调用所有的 load
方法。我们可以猜想 initialize
的调用是使用的消息机制 objc_msgSend()
来进行调用的,通过 Class 的 isa 找到元类对象,在元类对象的方法列表中找 initialize
方法,由于分类中的 initialize
方法已经被合并到了元类对象方法列表的前面,所以调用的其实是分类中的 initialize
方法。
再写出一个子类进行测试,可以发现:在调用子类的 initialize
之前,会先调用父类的 initialize
(如果父类的已经调用过了,就不调用了)。 我们可以猜测,在编译后肯定是调用了这样的代码:
objc_msgSend(父类, @selector(initialize));objc_msgSend(子类, @selector(initialize));复制代码
我们已经知道,当类接收到这消息的时候,它会通过类对象的 isa 找到元类对象,找到元类对象的方法列表,然后在里面查找方法。
我们可以在源码中找到这一段:
lookUpImpOrNil
-> lookUpImpOrForward
-> _class_initialize
的顺序点进去。然后我们主要注意这个函数的两个部分: 我们先来看一下 callInitialize()
点进去:
callInitialize(cls)
简单的理解成 objc_msgSend(cls, @selector(initialize))
。 接下来把刚才找到的这一大段代码再结合前一段lookUpImpOrForward
中的大吗用伪代码做一简化,只留下我们关注的部分,得到如下的样子:
if (cls 没有被初始化) { if (有父类 && 父类没有初始化) { objc_msgSend(父类,@selector(initialize)); 初始化父类; } objc_msgSend(cls, @selector(initialize)); 初始化 cls;}复制代码
这段伪代码就完全说明白了 + (void)initialize
方法的根本实现。
所以上边测试子类的现象就可以理解了。
父类的 initialize
方法可能会被调用很多次,我们来看一个例子。
@interface Person : NSObject@end@implementation Person+ (void)initialize{ NSLog(@"Person - initialize");}@end**************************************************@interface Person (test)@end@implementation Person (test)+ (void)initialize{ NSLog(@"Person test - initialize");}@end**************************************************@interface Student : Person@end@implementation Student@end**************************************************@interface Teacher : Student@end@implementation Teacher@end复制代码
然后调用下列方法:
[Student alloc]; [Teacher alloc];复制代码
你可以试着猜测一下打印出来的语句是什么。
5. Category为什么只能加方法不能加属性?
答:
分类能添加方法是因为分类编译之后的结构体 _category_t
中有可以存储方法列表的成员变量。
分类其实也是可以添加属性的,因为 _category_t
中也有可以存储属性的成员变量。
分类不能直接添加成员变量,因为 _category_t
不能存储成员变量。
分析:
再看一下分类编译之后的结构体:
instance_methods
与 class_methods
,所以分类是可以添加方法的。 分类其实是可以添加属性的,因为 _category_t
中有 properties
,它是用来存储属性的
Person (test)
分类中添加属性 @property (nonatomic, copy) NSString *name;
之后,还是可以编译成功的。 一般我们在类中添加 @property
之后,系统会自动生成成员变量,在 @interface
中添加 setter
、getter
方法声明,在 @implementation
中实现 setter
和 getter
。
在分类中添加 @property
,系统只会在 @interface
中添加setter
和 getter
的方法声明。
6. Category 能否添加成员变量?如果可以,如何给 Category 添加成员变量?
答:
不能直接添加,可以间接使用关联对象实现 `Category` 有成员变量的效果。分析:
Category 中是不能直接添加成员变量
关联对象提供了一下 API [6. 你使用过 runtime 中的哪些方法?]:
- 添加关联对象
/* 参数 object 表示的是要给哪个对象进行关联,一般传实例对象; 第二个参数 key ,它的类型是 void *,指针,也就是一个地址; 第三个参数 value ,表示要关联的是哪个值,把 value 和 object 通过 key 关联起来; 第四个参数 policy ,关联策略,设置它就类似于设置 @property 后边的修饰符,它有以下选项: OBJC_ASSOCIATION_ASSIGN = 0, OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, OBJC_ASSOCIATION_COPY_NONATOMIC = 3, OBJC_ASSOCIATION_RETAIN = 01401, OBJC_ASSOCIATION_COPY = 01403*/void objc_setAssociatedObject(id object, const void * key, id value, objc_AssociationPolicy policy)复制代码
- 获得关联对象
/* 取出与 object 通过 key 关联的值 value*/id objc_getAssociatedObject(id object, const void * key)复制代码
- 移除所有的关联对象
id objc_removeAssociatedObjects(id object)
参数 key 的三种简便的设置方式:
第一种方式
Person+test.h@interface Person (test)@property (nonatomic, assign) int weight;@endPerson+test.mconst int WeightKey;@implementation Person (test) - (void)setWeight:(int)weight{ objc_setAssociatedObject(self, &WeightKey, @(weight), OBJC_ASSOCIATION_RETAIN_NONATOMIC); //因为 value 是 id 类型,所以把它转成 NSNumber}- (int)weight{ return [objc_getAssociatedObject(self, &WeightKey) intValue];}@end复制代码
通过这种方式确实可以实现想要的效果。但是存在两个问题:
a. 我们定义的全局变量可以被外部的文件访问。可以在前面用static
修饰,限制它的作用域。 static const int WeightKey;复制代码
b. 它只是作为一个 key,如果用 int 类型,需要占用 4 个字节。可以把它改成 char 类型的,就只用占 1 个字节了。
static const char WeightKey;复制代码
第二种方式,在 .m 中这样设置 key :
- (void)setWeight:(int)weight{ objc_setAssociatedObject(self, @"weight", @(weight), OBJC_ASSOCIATION_RETAIN_NONATOMIC);}- (int)weight{ return objc_getAssociatedObject(self, @"weight");}复制代码
key 的类型是指针,将 @"weight"
传进去实际上是将这个字符串的地址传进去。
那么,这里产生了一个疑问,这里用了两个 @"weight"
,他们的地址是一样的吗?
是一样的。在 iOS 中我们这种直接写字符串的方式,字符串是存储在常量区的。 存储在常量区中的数据一经初始化就不能再修改,程序结束后由系统释放。所以不论我们写多少遍 @"weight"
,它其实都是存放在常量区的一个 @"weight"。
这里可以延伸到另个一面试题:判断两个字符串字面量是否相同,为什么要用 isEqualToString
, 而不能用 ==
来判断?在以后的问题里会有讲到。
第三种比较好的方式,使用 @selector()
:
- (void)setWeight:(int)weight{ objc_setAssociatedObject(self, @selector(weight), @(weight), OBJC_ASSOCIATION_RETAIN_NONATOMIC);}- (int)weight{ return [objc_getAssociatedObject(self, @selectory(weight)) intValue]; }复制代码
测试一下,可以发现 @selector(weight)
的地址也都是一样的:
NSLog(@"%p %p %p", @selector(weight), @selector(weight), @selector(weight));复制代码
上面介绍了可以采用关联对象的方式间接实现分类中添加成员变量的效果和参数 key 的三种常用的方式。
关于 AssociatedObjects
的实现原理等,可以参考和文章。