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

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

类的编译顺序是:

编译顺序

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

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

+ 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;
}

编译的顺序是:

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;
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,723评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,080评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,604评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,440评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,431评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,499评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,893评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,541评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,751评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,547评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,619评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,320评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,890评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,896评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,137评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,796评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,335评论 2 342

推荐阅读更多精彩内容