iOS-总结Category
Category这个知识点,相信有了解过的同学们都知道,用到的地方虽然不多,但是相对比较集中,一般一个类的内容信息过于庞大,导致里面代码信息过于混乱,我们就可以试着用category去管理这个类。其实举个简单的例子,就好像“人”这个物种,现在不就是有各类人吗,这也是一种分类,而每一类人体现出来的特点或者说是优点等等都是不一样的。
言归正传,我们还是通过面试题来入手,从而一步一步了解Category,这里总共有四个面试题,如下:
问题1:Category的实现原理
问题2:Category和Class Extension的区别是什么
问题3:Category中有load方法吗?什么时候调用?load方法能继承吗?
问题4:load、initialize方法的区别是什么?它们在Category中调用的顺序?以及在出现继承的时候,它们之间的调用过程
这里,同样我们还是通过用代码入手,这里笔者定义了一个BLPerson类和它的BLPerson+Test的分类,以及通过xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc BLPerson+Test.m -o BLPerson+Test.cpp这个命令行(这个命令行笔者就不解释了)得到的BLPerson+Test.m的底层c++实现文件BLPerson+Test.cpp(代码链接在最底部)。这时我们可以在BLPerson+Test.cpp文件中看到对应分类的m文件的实现,当然我们主要关心的是这个Category对应的实现具体是什么样子的,其实就是如下代码:
struct _class_t {
struct _class_t *isa;
struct _class_t *superclass;
void *cache;
void *vtable;
struct _class_ro_t *ro;
};
struct _category_t {
const char *name;
struct _class_t *cls;
const struct _method_list_t *instance_methods;
const struct _method_list_t *class_methods;
const struct _protocol_list_t *protocols;
const struct _prop_list_t *properties;
};
Category在底层就是一个_category_t结构体,其实对于这个结果,相信看了笔者之前iOS-Objective-C的本质这篇文章的都知道,一个类的本质其实就是一个结构体。在_category_t这个结构体中,我们可以很清楚的看到,它里面包含了类的名称、对象方法列表、类方法列表、协议列表和属性列表,而_class_t这个结构体里面包含的就是这个类的isa、父类等信息
这里在分类中加入了两个方法:
@interface BLPerson (Test)
- (void)instanceTest;
+ (void)classTest;
@end
#import "BLPerson+Test.h"
@implementation BLPerson (Test)
- (void)instanceTest {
NSLog(@"BLPerson (Test) instanceTest");
}
+ (void)classTest {
NSLog(@"BLPerson (Test) classTest");
}
@end
在cpp文件中,我们可以看到以下代码:
static struct _category_t _OBJC_$_CATEGORY_BLPerson_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) =
{
"BLPerson",
0, // &OBJC_CLASS_$_BLPerson,
(const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_BLPerson_$_Test,
(const struct _method_list_t *)&_OBJC_$_CATEGORY_CLASS_METHODS_BLPerson_$_Test,
0,
0,
};
从这个代码中我们可以看到_category_t结构体中对应的instance_methods和class_methods就是上面所展示:
(const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_BLPerson_$_Test,
(const struct _method_list_t *)&_OBJC_$_CATEGORY_CLASS_METHODS_BLPerson_$_Test,
接下来,笔者在BLPerson的本类中添加同样的两个方法:
@interface BLPerson : NSObject
- (void)instanceTest;
+ (void)classTest;
@end
#import "BLPerson.h"
@implementation BLPerson
- (void)instanceTest {
NSLog(@"BLPerson instanceTest");
}
+ (void)classTest {
NSLog(@"BLPerson classTest");
}
@end
然后笔者在main函数中调用BLPerson的这个两个方法:
#import <Foundation/Foundation.h>
#import "BLPerson.h"
#import "BLPerson+Test.h"
int main(int argc, const char * argv[]) {
BLPerson *person = [[BLPerson alloc] init];
[person instanceTest];
[BLPerson classTest];
return 0;
}
控制台打印如下:
![](https://img.haomeiwen.com/i10378163/d8aa99650ed8aca8.png)
接下来,我们把分类去掉:
#import <Foundation/Foundation.h>
#import "BLPerson.h"
int main(int argc, const char * argv[]) {
BLPerson *person = [[BLPerson alloc] init];
[person instanceTest];
[BLPerson classTest];
return 0;
}
控制台打印如下:
![](https://img.haomeiwen.com/i10378163/a40a47d74c0173ab.png)
所以我们得到,不管分类是否导入,只要分类中有一样的方法,则就会优先调用分类中的方法。
这是为什么呢?大家都知道,方法的调用,其实就是runtime中的消息发送,先发送消息子,然后在方法列表中查找对应的方法;而且当方法名称一样时,肯定是优先调用先找到的那个。所以从这一点上面,我们可以看出,分类中的同名方法应该是排在本类的前面,只有这样,在调用这个方法的时候,才会先调用分类中的方法。事实就是如此,其实我们还是需要从源代码入手,这里笔者只摘录最直接的部分代码,如下:
//将本类中原有的方法列表地址向后移动一定位置,移动的位置个数就是分类中的方法列表的个数
memmove(array()->lists + addedCount, array()->lists,
oldCount * sizeof(array()->lists[0]));
// 将所有分类的方法列表,拷贝到经过移动之后的本类的方法列表中
memcpy(array()->lists, addedLists,
addedCount * sizeof(array()->lists[0]));
这样,大家就应该明白,为什么会先调用分类的方法,因为在系统底层的现实上就是将分类的方法放在本类之前。
至此,问题1的答案已经很明白了:
Category编译之后的底层结构是struct category_t,里面存储着分类的对象方法、类方法、属性、协议信息;在程序运行的时候,runtime会将Category的数据,合并到类信息中(类对象、元类对象中)。
至于第二个问题,笔者在这里就直接给出答案,也不做具体分析了:
Class Extension是在编译的时候就已经把数据包含在本类中了,而Category是在运行时,才会将数据合并到本类的信息中。
接下来,我们再来分析第三个和第四个问题,首先,在本类和分类中都添加load和initialize这两个方法,代码如下:
// BLPerson的实现
@implementation BLPerson
+ (void)load {
NSLog(@"BLPerson load");
}
+ (void)initialize {
NSLog(@"BLPerson initialize");
}
.........
// 分类的实现
#import "BLPerson+Test.h"
@implementation BLPerson (Test)
+ (void)load {
NSLog(@"BLPerson+Test load");
}
+ (void)initialize {
NSLog(@"BLPerson+Test initialize");
}
........
然后我们启动程序,这里我们先后两次启动,第一次启动的时候,我们不调用BLPerson类,代码如下:
#import <Foundation/Foundation.h>
//#import "BLPerson.h"
//#import "BLPerson+Test.h"
int main(int argc, const char * argv[]) {
// BLPerson *person = [[BLPerson alloc] init];
return 0;
}
这时候控制台打印出来的结果是:
![](https://img.haomeiwen.com/i10378163/1021ea095d040eac.png)
第二次启动,我们调用调用BLPerson类,代码如下:
#import <Foundation/Foundation.h>
#import "BLPerson.h"
#import "BLPerson+Test.h"
int main(int argc, const char * argv[]) {
BLPerson *person = [[BLPerson alloc] init];
return 0;
}
这时候控制台打印出来的结果是:
![](https://img.haomeiwen.com/i10378163/b8f9b1c2f8afa8af.png)
我们可以看到,不管代码中有没有调用BLPerson类,load方法都会调用,而且它不是通过消息发送的方式来调用的,是通过方法的地址直接调用。所以第三个问题的答案就是:
Category中又load方法,在程序启动时,系统就会通过runtime加载load方法。load方法也可以继承
而initialize这个方法,在程序调用BLPerson类时,会先去调用test分类中的initialize方法。其实大家有兴趣可以试试,如果多次调用BLPerson这个类的时候,initialize方法的调用情况,其实这个方法只会每个类对应的只会调用一次,而且是在第一次调用这个类的时候才会触发。所以第四个问题的答案是:
1、load是程序一运行就根据函数地址直接调用,initialize是类第一次接收到消息的时候调用。2、先调用本类的load再调用分类,而且不同的分类,先编译的先调用;分类中的initialize会被优先调用,后编译的分类先调用,这个是按照消息发送机制实现(上述中,笔者就说了分类的方法会在运行时,合并到本类方法列表中,而且放在本类方法列表之前)
这里有个很特别的情况,大家有兴趣可以去试一试,如果一个子类BLStudent继承于BLPerson,他们都没有分类,这时候BLPerson中实现initialize方法,而BLStudent不实现,这个时候看看控制台打印情况,你会有不一样的发现。
代码链接:https://github.com/IBIgLiang/iOS-CategoryStudy