Category(分类)这一Object-C 2.0之后添加的语言特性,在日常开发中使用频率非常高。而且面试时Category基本上是都会涉及到的一个知识点。下面罗列一下面试中经常会提出的问题,基本上涵盖了这个知识点:
- Category和Extension的区别。
- Category底层实现原理
- Category的加载处理过程
- Category中 + load方法的调用
- Category中 + initialize方法的调用
- Category中load和initialize方法的区别
- Category中添加成员变量的实现
1. Category和Extension的区别。
- Category是在程序运行的时候,runtime会将Category的数据合并到类信息汇中。
- Class Extension 是在编译的时候,就已经将数据包含在类信息中。
2. Category底层实现原理
Category编译之后的底层结构是 struct category_t ,里面存储着分类的对象方法,类方法,属性,协议信息。
3. Category的加载处理过程
下面创建了4个类,一个People类和3个People类的分类(Run、Jump、Eat)。
这4个类都实现了 - instanceMethod这个实例方法。
调用People的这个实例方法,查看打印结果。
@interface People : NSObject
- (void)instanceMethod;
@end
@implementation People
- (void)instanceMethod
{
NSLog(@"people instanceMethod");
}
@end
@interface People (Run)
- (void)instanceMethod;
@end
@implementation People (Run)
- (void)instanceMethod
{
NSLog(@"people run instanceMethod");
}
@end
@interface People (Jump)
- (void)instanceMethod;
@end
@implementation People (Jump)
- (void)instanceMethod
{
NSLog(@"people jump instanceMethod");
}
@end
@interface People (Eat)
- (void)instanceMethod;
@end
@implementation People (Eat)
- (void)instanceMethod
{
NSLog(@"people eat instanceMethod");
}
@end
查看People类中的方法列表:
#import "People.h"
#import <objc/runtime.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
unsigned int count;
Method *methodList = class_copyMethodList([People class], &count);
for (int i = 0; i < count; i ++) {
Method method = methodList[I];
NSLog(@"%@",NSStringFromSelector(method_getName(method)));
}
free(methodList);
}
return 0;
}
发现People类中有4个instanceMethod方法,分类中的instanceMethod也在People类中。而且这时没有调用People的实例方法,是在runtime运行中加载了People类之后,Category的所有数据插入到了People类中。
下面调用一下People类的实例方法:
#import <Foundation/Foundation.h>
#import "People.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
People *people = [[People alloc] init];
[people instanceMethod]; // 打印结果为:people run instanceMethod
}
return 0;
打印结果为 people run instanceMethod
从结果来看,调用People的实例方法时调用了分类的方法,也就是所有分类的方法都合并到一个数组中,然后插入到原有类的前面,但是为什么是People (Run)分类覆盖了实例方法,而不是其他两个?
在TARGETS中查看一下编译文件排序:
发现 People+Run.m是最后编译的。也就是说编译顺序在最后的方法会排在方法列表的最前面。
所以Category的加载处理过程是:
1. 通过runtime加载某个类的所有的Category数据。
2. 将所有的Category数据(方法、属性、协议)合并成到一个大数组中。这些数据后面参与编译的Category数据,会保存在数组的前面。
3. 将合并后的分类数据(方法、属性、协议)插入到类的原来的数据的前面。
4. Category中 + load方法的调用
- Category有load方法。
- load方法在Runtime加载类、分类时就会调用。
- 每个类、分类在程序运行过程中,只调用一次load方法。
创建6个类,之间的关系是:
Animal : NSObject
People : NSObject
Student : People
People Category : People+Run , People+Jump , People+Eat
Animal 、 People 继承自 NSObject;
Student 继承自People
People+Run , People+Jump , People+Eat 是People的分类
分别实现一下load方法:
@implementation Animal
+ (void)load
{
NSLog(@"animal load method");
}
@end
@implementation People
+ (void)load
{
NSLog(@"people load method");
}
@end
@interface Student : People
@end
@implementation Student
+ (void)load
{
NSLog(@"student load method");
}
@end
@implementation People (Run)
+ (void)load
{
NSLog(@"people run load method");
}
@end
@implementation People (Jump)
+ (void)load
{
NSLog(@"people jump load method");
}
@end
@implementation People (Eat)
+ (void)load
{
NSLog(@"people eat load method");
}
@end
然后在main.m中不引入类的头文件:
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
}
return 0;
}
类的编译顺序是:
按照之前的思路,打印的顺序应该是:
student、jump、animal、eat、People、run
或者是:
run、People、eat、animal、jump、student
但是打印结果不是这样,打印出结果:
原因是调用+load 方法不是通过消息发送机制(objc_msgSend),而是根据内存中函数地址直接调用。而且是在runtime加载类、分类时调用。
+load方法调用顺序总结如下:
- +load方法时在runtime加载类、分类的时候调用。
- 每个类、分类的+load方法在程序运行中只调用一次
- 先调用类的+load方法
1.1 调用类的+load方法时,按照编译先后顺序调用(先调用Student再调用Animal)
1.2 调用子类的+load方法时,先调用父类的+load方法(调用Student时,先调用People,再调用Student)
于是调用顺序是:People、Student、Animal - 再调用分类的+load方法
2.1 调用分类+load方法时,按照编译先后顺序调用
PS. 如果是手动调用 load方法,则会触发消息机制(objc_msgSend)调用。按照消息机制调用顺序执行。但是一般不会手动调用load方法。
5. Category中+ initialize方法的调用
+initialize是在类第一次接收消息时调用的。
创建几个类,他们之间的关系是:
People : NSObject
Student : People
People Category : People+Run , People+Jump , People+Eat
People 继承自 NSObject;
Student 继承自People
People+Run , People+Jump , People+Eat 是People的分类
分别实现 + initialize 方法:
@interface People : NSObject
@end
@implementation People
+(void)initialize
{
NSLog(@"people initialize");
}
@end
@interface Student : People
@end
@implementation Student
+(void)initialize
{
NSLog(@"student initialize");
}
@end
@implementation People (Run)
+(void)initialize
{
NSLog(@"people run initialize");
}
@end
@implementation People (Jump)
+(void)initialize
{
NSLog(@"people jump initialize");
}
@end
@implementation People (Eat)
+(void)initialize
{
NSLog(@"people eat initialize");
}
@end
分别调用People的alloc方法和Student的alloc方法:
int main(int argc, const char * argv[]) {
@autoreleasepool {
[People alloc];
}
return 0;
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
[Student alloc];
}
return 0;
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
// 分别调用People和Student的alloc
[People alloc];
[Student alloc];
}
return 0;
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
// 调用一次People allocation,三次Student allocation
[People alloc];
[Student alloc];
[Student alloc];
[Student alloc];
}
return 0;
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
// 调用三次Student allocation
[Student alloc];
[Student alloc];
[Student alloc];
}
return 0;
}
编译的顺序是:
// 打印结果
[People alloc];
--> people run initialize
[Student alloc];
--> people run initialize
--> student initialize
[People alloc];
[Student alloc];
--> people run initialize
--> student initialize
[People alloc];
[Student alloc];
[Student alloc];
[Student alloc];
--> people run initialize
--> student initialize
[Student alloc];
[Student alloc];
[Student alloc];
--> people run initialize
--> student initialize
发现有几个现象:
- 调用People alloc时打印的是People分类Run的 initialize方法
- 调用Student alloc时打印的是People分类Run的initialize方法和Student initialize方法
- 调用People 和 Student的alloc时打印的还是和调用Student alloc一样的结果
- 多次调用Student alloc时打印的结果和调用一次Student alloc的一样
所以得出以下几个结论:
- +initialize是类第一次接收消息的时候调用
- +initialize是通过objc_msgSend(消息机制)调用,所以分类方法会覆盖类方法
- 调用子类(Student)的+initialize方法时底层会先调用父类(People)的+initialize方法,再调用子类的方法
objc_msgSend([People class], @selector(initialize));
objc_msgSend([People class], @selector(initialize)); - 每个类只会初始化一次(只调用一次initialize),多次接收消息只调用一次+initialize方法
因为+ initialize是通过objc_msgSend调用的,所以会有以下特点:
如果子类没有实现 + initialize方法,会调用父类的 + initialize方法。所以当多个子类都没有实现 + initialize方法的话,会多次调用父类 + initialize方法。
当分类实现了 + initialize方法,会覆盖类本身的 + initialize方法调用。因为Category的加载过程是将所有的Category的方法、属性、协议信息合成一个大数组,再将这个大数组插入到类信息的前面。Category中编译越靠后越优先调用。
6. Category中load和initialize方法的区别
Category 中 + load 和 + initialize 方法的区别总结如下:
调用方式
- +load是根据方法函数的内存地址直接调用
- +initialize是通过objc_msgSend调用
调用时刻
- +load是runtime加载类、分类时调用(只会调用一次)
- +initialize是类第一次接收消息时调用,每一个类只会初始化(initialize)一次,但是父类的+ initialize方法可能会调用多次。
调用顺序
+load
1.1 先调用类的+load方法
编译越早,调用越早
调用子类的+load方法时,先调用父类的+load方法
1.2 再调用分类的+load方法
编译越早,调用越早+initialize
2.1 先初始化父类
2.2 再初始化子类,若子类没有实现+initialize方法,最终还是会调用父类的+initialize方法
2.3 如果分类实现了+initialize方法,会覆盖类的+initialize方法。编译越晚,调用越早。
7. Category中添加成员变量的实现
一个类中如果写一个属性的话,编译器会自动做3件事情:
- 生成一个成员变量
- 生成成员变量的getter、setter声明
- 生成getter和setter的实现
但是如果在一个分类中写一个属性,编译器只会做1件事情:
- 生成getter和setter的声明
根据分类的结构,不能直接给分类添加一个成员变量,但是可以间接实现分类有成员变量的效果:使用关联对象(Association Object)。
关联对象是runtime中的方法,使用时需要引入<objc/runtime.h>
关联对象主要的方法有3个:
- 设置关联对象
OBJC_EXPORT void
objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key, id _Nullable value, objc_AssociationPolicy policy)
返回类型为 void,其中有4个参数:
id _Nonnull object : 给哪一个对象添加关联对象
const void * _Nonnull key :传入一个指针进去,接收的是地址值
id _Nullable value :关联什么值
objc_AssociationPolicy policy :关联的策略
关联策略:
objc_AssociationPolicy :
// 给关联对象指向一个弱引用
OBJC_ASSOCIATION_ASSIGN = 0, /**< Specifies a weak reference to the associated object. */
// 给关联对象指向一个强引用,这个关联对象是非原子性
OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, /**< Specifies a strong reference to the associated object.
* The association is not made atomically. */
// 给关联对象指向copy,这个关联对象是非原子性
OBJC_ASSOCIATION_COPY_NONATOMIC = 3, /**< Specifies that the associated object is copied.
* The association is not made atomically. */
// 给关联对象指向一个强引用,这个关联对象是原子性
OBJC_ASSOCIATION_RETAIN = 01401, /**< Specifies a strong reference to the associated object.
* The association is made atomically. */
// 给关联对象指向copy,这个关联对象是非原子性
OBJC_ASSOCIATION_COPY = 01403 /**< Specifies that the associated object is copied.
* The association is made atomically. */
};
// 关联对象策略对应的修饰符:
// 关联对象策略中没有weak修饰符,没有弱引用这种效果
OBJC_ASSOCIATION_ASSIGN === assign
OBJC_ASSOCIATION_RETAIN_NONATOMIC === strong,nonatomic
OBJC_ASSOCIATION_COPY_NONATOMIC === copy,nonatomic
OBJC_ASSOCIATION_RETAIN === strong,atomic
OBJC_ASSOCIATION_COPY === copy,atomic
- 获取关联对象
OBJC_EXPORT id _Nullable
objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key)
返回类型为 id,其中有2个参数:
id _Nonnull object : 获取哪一个对象的关联对象
const void * _Nonnull key :传入一个指针进去,接收的是地址值
- 移除关联对象
OBJC_EXPORT void
objc_removeAssociatedObjects(id _Nonnull object)
返回类型为 void,其中有1个参数:
id _Nonnull object : 移除哪一个对象的所有关联对象
其他3个参数比较明了,说一下key这个参数的用法,一般key的常见用法有4种:
- static void *myKey = &myKey;
- (void)setAge:(int)age
{
objc_AssociationPolicy policy = OBJC_ASSOCIATION_ASSIGN;
objc_setAssociatedObject(self, myKey, @(age), policy);
}
- (int)age
{
return [objc_getAssociatedObject(self, myKey) intValue];
}
- static char myKey;
- (void)setAge:(int)age
{
objc_AssociationPolicy policy = OBJC_ASSOCIATION_ASSIGN;
objc_setAssociatedObject(self, &myKey, @(age), policy);
}
- (int)age
{
return [objc_getAssociatedObject(self, &myKey) intValue];
}
- 直接使用属性名作为key
使用属性名可以防止名称冲突,而且每一个不同的字符串的地址不一样
- (void)setAge:(int)age
{
objc_AssociationPolicy policy = OBJC_ASSOCIATION_ASSIGN;
objc_setAssociatedObject(self, @"age", @(age), policy);
}
- (int)age
{
return [objc_getAssociatedObject(self, @"age") intValue];
}
- 使用get方法的@selector作为key
- (void)setAge:(int)age
{
objc_AssociationPolicy policy = OBJC_ASSOCIATION_ASSIGN;
objc_setAssociatedObject(self, @selector(age), @(age), policy);
}
- (int)age
{
return [objc_getAssociatedObject(self, @selector(age)) intValue];
}
// 在getter中可以使用隐式参数_cmd,_cmd对应当前方法的selector
- (void)setAge:(int)age
{
objc_AssociationPolicy policy = OBJC_ASSOCIATION_ASSIGN;
objc_setAssociatedObject(self, @selector(age), @(age), policy);
}
- (int)age
{
return [objc_getAssociatedObject(self, _cmd) intValue];
}
这样就可以在分类中实现有成员变量的效果:
int main(int argc, const char * argv[]) {
@autoreleasepool {
People *people = [[People alloc] init];
people.age = 10;
NSLog(@"age = %d",people.age); // age = 10
}
return 0;
}