Category 面试题总结

Category(分类)这一Object-C 2.0之后添加的语言特性,在日常开发中使用频率非常高。而且面试时Category基本上是都会涉及到的一个知识点。下面罗列一下面试中经常会提出的问题,基本上涵盖了这个知识点:

  1. Category和Extension的区别。
  2. Category底层实现原理
  3. Category的加载处理过程
  4. Category中 + load方法的调用
  5. Category中 + initialize方法的调用
  6. Category中load和initialize方法的区别
  7. 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;
}

1529889-7ae9c8740f8b171b.png

People class method list

发现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中查看一下编译文件排序:

1529889-2492a725f2279dae.png

TARGETS - Compile Sources.png

发现 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;
}

类的编译顺序是:

1529889-12f94a8d28595b6f.png

编译顺序

按照之前的思路,打印的顺序应该是:
student、jump、animal、eat、People、run
或者是:
run、People、eat、animal、jump、student

但是打印结果不是这样,打印出结果:

1529889-7c44130bb3ab8381.png

+ load method result

原因是调用+load 方法不是通过消息发送机制(objc_msgSend),而是根据内存中函数地址直接调用。而且是在runtime加载类、分类时调用。

+load方法调用顺序总结如下:

  • +load方法时在runtime加载类、分类的时候调用。
  • 每个类、分类的+load方法在程序运行中只调用一次
  1. 先调用类的+load方法
    1.1 调用类的+load方法时,按照编译先后顺序调用(先调用Student再调用Animal)
    1.2 调用子类的+load方法时,先调用父类的+load方法(调用Student时,先调用People,再调用Student)
    于是调用顺序是:People、Student、Animal
  2. 再调用分类的+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;
}

编译的顺序是:

1529889-8749c212caca165d.png

initialize类编译顺序

// 打印结果
[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 方法的区别总结如下:

调用方式

  1. +load是根据方法函数的内存地址直接调用
  2. +initialize是通过objc_msgSend调用

调用时刻

  1. +load是runtime加载类、分类时调用(只会调用一次)
  2. +initialize是类第一次接收消息时调用,每一个类只会初始化(initialize)一次,但是父类的+ initialize方法可能会调用多次。

调用顺序

  1. +load
    1.1 先调用类的+load方法
    编译越早,调用越早
    调用子类的+load方法时,先调用父类的+load方法
    1.2 再调用分类的+load方法
    编译越早,调用越早

  2. +initialize
    2.1 先初始化父类
    2.2 再初始化子类,若子类没有实现+initialize方法,最终还是会调用父类的+initialize方法
    2.3 如果分类实现了+initialize方法,会覆盖类的+initialize方法。编译越晚,调用越早。


7. Category中添加成员变量的实现

一个类中如果写一个属性的话,编译器会自动做3件事情:

  1. 生成一个成员变量
  2. 生成成员变量的getter、setter声明
  3. 生成getter和setter的实现

但是如果在一个分类中写一个属性,编译器只会做1件事情:

  1. 生成getter和setter的声明

根据分类的结构,不能直接给分类添加一个成员变量,但是可以间接实现分类有成员变量的效果:使用关联对象(Association Object)。

关联对象是runtime中的方法,使用时需要引入<objc/runtime.h>

关联对象主要的方法有3个:

  1. 设置关联对象
    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
  1. 获取关联对象
    OBJC_EXPORT id _Nullable
    objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key)

返回类型为 id,其中有2个参数:
id _Nonnull object : 获取哪一个对象的关联对象
const void * _Nonnull key :传入一个指针进去,接收的是地址值

  1. 移除关联对象
    OBJC_EXPORT void
    objc_removeAssociatedObjects(id _Nonnull object)

返回类型为 void,其中有1个参数:
id _Nonnull object : 移除哪一个对象的所有关联对象

其他3个参数比较明了,说一下key这个参数的用法,一般key的常见用法有4种:

  1. 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];
}
  1. 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];
}
  1. 直接使用属性名作为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];
}
  1. 使用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;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值