前言
首先,这里有几个与Category相关的面试题,大家可以看一下
1、Category如何使用?
2、Category的原理是什么?
3、Category与类扩展的区别?
4、Category中load方法是什么时候调用的?load方法能被继承吗?
5、load和initialize的区别是什么?他们在category中的调用顺序是怎样的?出现继承的时候他们之间的调用过程是什么?
6、Category是否可以添加成员变量?如果可以,如何添加?
这几个面试题你能答出几个呢?如果有不会的地方,那咱们一起来学习下吧
Category
Category分类的作用:在不改变原有的类的前提下,可以为类单独添加一些方法、协议、属性。
首先,我们创建一个类YZPerson,其里面有一个对象方法-(void)run;
然后分别新建两个分类:YZPerson+Eat、YZPerson+Drink。里面分别有四个方法:
- (void)eat1
{
NSLog(@"YZPerson+Eat-eat1");
}
- (void)eat2
{
NSLog(@"YZPerson+Eat-eat2");
}
+ (void)eat3
{
NSLog(@"YZPerson+Eat-eat3");
}
+ (void)eat4
{
NSLog(@"YZPerson+Eat-eat4");
}
使用xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc YZPerson+Eat.m
命令行指令,可以将YZPerson+Eat.m转化为C语言源码YZPerson+Eat.cpp
编译后的分类文件,全部转化为_category_t类型的结构体。
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; //属性列表
};
查找源码,可以看到其赋值方法
其中,第3和第4的赋值是如下两个图
从源码可以看出,分类在经历过编译后,将分类里面的内容:对象方法、类方法、协议、属性都转化为类型为_category_t的结构体变量。
对分类的源码分析:
找到remethodizeClass(cls)核心方法的实现:
static void
attachCategories(Class cls, category_list *cats, bool flush_caches)
{
if (!cats) return;
if (PrintReplacedMethods) printReplacements(cls, cats);
bool isMeta = cls->isMetaClass();
// fixme rearrange to remove these intermediate allocations
method_list_t **mlists = (method_list_t **)
malloc(cats->count * sizeof(*mlists));
property_list_t **proplists = (property_list_t **)
malloc(cats->count * sizeof(*proplists));
protocol_list_t **protolists = (protocol_list_t **)
malloc(cats->count * sizeof(*protolists));
// Count backwards through cats to get newest categories first
int mcount = 0;
int propcount = 0;
int protocount = 0;
int i = cats->count;
bool fromBundle = NO;
while (i--) { //取出某个分类,i--,先取的最后编译的那一个
auto& entry = cats->list[i];
//对方法列表的操作
method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
if (mlist) {
mlists[mcount++] = mlist;//mcount++,对第一个取出的进行操作
fromBundle |= entry.hi->isBundle();
}
//对属性列表的操作
property_list_t *proplist =
entry.cat->propertiesForMeta(isMeta, entry.hi);
if (proplist) {
proplists[propcount++] = proplist;
}
//对协议列表的操作
protocol_list_t *protolist = entry.cat->protocols;
if (protolist) {
protolists[protocount++] = protolist;
}
}
auto rw = cls->data();
prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
//将所有分类的对象(类)方法列表附加到原来类的对象(类)方法列表里面
rw->methods.attachLists(mlists, mcount);//mcount个数
free(mlists);
if (flush_caches && mcount > 0) flushCaches(cls);
rw->properties.attachLists(proplists, propcount);
free(proplists);
rw->protocols.attachLists(protolists, protocount);
free(protolists);
}
其中,attachLists方法的实现:
void attachLists(List* const * addedLists, uint32_t addedCount) {
if (addedCount == 0) return;
if (hasArray()) {
// many lists -> many lists
uint32_t oldCount = array()->count;
uint32_t newCount = oldCount + addedCount;
setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));
array()->count = newCount;
//array()->lists + addedCount = array()->lists
memmove(array()->lists + addedCount, array()->lists,
oldCount * sizeof(array()->lists[0]));
//addedLists分类数据
//addedLists覆盖array()->lists数据
memcpy(array()->lists, addedLists,
addedCount * sizeof(array()->lists[0]));
}
else if (!list && addedCount == 1) {
// 0 lists -> 1 list
list = addedLists[0];
}
else {
// 1 list -> many lists
List* oldList = list;
uint32_t oldCount = oldList ? 1 : 0;
uint32_t newCount = oldCount + addedCount;
setArray((array_t *)malloc(array_t::byteSize(newCount)));
array()->count = newCount;
if (oldList) array()->lists[addedCount] = oldList;
memcpy(array()->lists, addedLists,
addedCount * sizeof(array()->lists[0]));
}
}
通过查阅以上源码,可以得到:
- 在运行时,通过runtime机制,将多个分类里面的【方法列表(包括:对象方法列表和类方法列表)、协议列表和属性列表】分别集合成数组,然后将新的数组添加到【原来类对象里面的方法列表、元类里面的类方法列表、类对象里面的协议列表、属性列表】的最前面,也就是将分类里面的内容动态的添加到了类对象和元对象里面。
- 同时,由于是添加在最前面,所以当分类、原类、父类里面都有同一个方法时(例如:-(void)run;方法),优先执行分类里面的方法,如果没有再执行原类里面的方法,如果再没有才会去父类里面找该方法。 需要注意的是,是优先调用,并没有覆盖原类中的方法。
- 有多个分类同时有某一个方法的时候,由于遍历是i- -,然后做的mcount++操作,因此,最后编译的分类文件,第一个被查找。
问:什么时候决定分类文件是最后被编译的呢?
在下面的文件,最后一个被编译。
总结:Category的加载过程
- 通过Runtime加载某个类的所有Category数据
- 把所有Category的方法、属性、协议数据合并到一个大数组中(最后面参与编译的Category数据会在数组前面)
- 将合并后的分类数据(包括方法、属性、协议),插入到原来数据的前面
以上就是Category被加载的过程,也是Categorey的原理。
分原子父
分类在前,原类在后(分类添加到原类的前面)
原类在前,父类在后(消息发送机制)
+(void)load;方法
下面介绍一下有关load相关的知识
- 在程序启动的时候会加载所有的类和分类,并调用所有类和分类的+load方法。也就是不管程序在运行过程中是否调用过该类,在程序初始化的时候,都会调用+load方法且只会调用一次。
- 先加载父类,再加载子类
- 先加载原始类,再加载分类
初始化load调用顺序:父子原分
有一点需要说明的是,+(void)load;方法跟分类中自定义方法不一样。因为,如果是自定义方法,原类跟分类方法一样的话,只会调用分类的方法。而+(void)load;方法会把所有的原来、分类里面的+(void)load;都会调用一遍。同样是原类和分类里面一样的方法,为什么会出现不一样的结果呢?
我们继续查看源码
自定义方法的调用[YZPerson test];
是消息传递机制,因此会通过isa指针在元类中查找类方法,如果有分类+test方法,则优先调用分类的+test方法。
而+load方法,是根据直接在内存中找到+load的内存地址,通过load_method方法调用的。
先调用原类的load方法,再调用分类的load方法;
先调用父类的load方法,再调用子类的load方法;
没有继承关系的多个原类,按编译顺序调用(先编译,先调用);
多个分类只按编译顺序调用(先编译,先调用);
+initialize方法
下面介绍一下有关initialize相关的知识
+initialize方法会在类第一次接收到消息时调用。
- 在第一次使用某个类时(比如创建对象等),就会调用一次+initialize方法
- 一个类只会调用一次+initialize方法
- 调用顺序:先调用父类的,再调用子类的
初始化initialize调用顺序:父子分原
我们查看相关源码:
通过源码分析,不难看出上面的知识点。
由于是基于isa指针机制,+initialize方法有以下特点:
- 如果分类实现了+initialize,就调用分类的+initialize,不会再调用类本身的+initialize调用(网上有说是覆盖原类中的+initialize方法,其实并不是真正的覆盖,而是没有调用原类中的+initialize方法)
父类
@implementation YZPerson
+ (void)initialize
{
NSLog(@"YZPerson-initialize");
}
@end
父类的分类
@implementation YZPerson (Eat)
+ (void)initialize
{
NSLog(@"YZPerson(Eat)-initialize");
}
@end
子类(原类)
@implementation YZStudent
//+(void)load
//{
// NSLog(@"YZStudent-load");
//}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
[YZStudent alloc];
}
return 0;
}
神奇的一幕出现了:
2020-02-26 17:02:11.224559+0800 Category[75206:2732274] YZPerson(Eat)-initialize
2020-02-26 17:02:11.224800+0800 Category[75206:2732274] YZPerson(Eat)-initialize
不是说好的initialize只调用一次吗?怎么调用了两次?为什么呢?
首先,打印出来的是分类,这个没有问题,因为分类方法在父类的方法前面,优先显示分类的。
[YZStudent alloc];会先去找父类的,父类YZPerson并没有实现initialize方法,因此,第一次打印是父类的initialize;
父类调用完毕后,并没有结束,而是去调用其本身的initialize方法,其本身没有initialize方法,由于继承关系,就去父类里面找initialize,最后调父类的initialize。
伪代码:
if (原类没有初始化)
{
if (父类没有初始化)
{
objc_msgSend([YZPerson alloc], @selector(initialize));
}
objc_msgSend([YZStudent alloc], @selector(initialize));
}
因此,会出现调用两次。其实每个类的初始化还是只有一次。第一次是父类Person的初始化,第二次是子类Student的初始化,由于子类没有+initialize,所以调用父类的+initialize方法,也就是:如果子类没有实现+initialize,会调用父类的+initialize(所以父类的+initialize可能被调用多次)
问:分类可以添加属性吗?
分类只能添加方法,不能直接添加属性,但可以间接添加属性。
直接在分类里面写成员变量,会报错❌
分类里面,可以写属性,但是不可以直接写成员变量
因为,category里面没有ivarList相关的东西去存储成员变量
也正是这个原因,不能添加成员变量
在普通类中,@property (assign, nonatomic) int age;
会做三件事:
- 生成age的成员变量
- 生成age的get、set方法的声明
- 生成age的get、set方法的实现
而在分类中,@property (assign, nonatomic) int weight;
可以写,但是它的作用只有一个:
生成weight的get、set方法的声明
如何实现为分类间接添加属性呢?
方法一:创建全局变量
static int height_;
-(void)setHeight:(int)height
{
height_ = height;
}
- (int)height
{
return height_;
}
使用全局变量,保存数据
可以实现赋值、取值
但问题是,多个对象创建,使用的都是同一份height,也就是对象1的height=10,对象2的height=12,那么,对象1的height也变成12了。因此,不行
方法二:字典
使用字典,将self(地址)作为key,属性值作为value
可以解决上面说的多个对象同时修改属性的问题(因为self不一样)
但是,也有缺点,一个是内存释放问题,一个是线程安全问题
当然,你也可以加一些判断条件,做好线程安全的问题,比较麻烦
方法三:关联对象
我们可以通过runtime中的关联对象的方法(objc_setAssociatedObject)
实现分类中属性的get、set方法的实现,具体实现如下:
必看:
问:原类中有一个属性age,子类也有一个属性age,会有什么问题吗?
会有一个警告⚠️
警告产生的原因是:
在写属性值的时候,使用@property,起到三个作用:
生成成员变量_age
声明get\set方法
实现get\set方法
在之前的OC中,写一个@property,还需要对应写一个@synthesize
@synthesize age = _age;
其作用是,将你写的age属性,和_age成员变量联系起来(@synthesize 合成访问器方法)
现在,Xcode不需要写@synthesize age = _age;
,在写@property的时候,会自动给加上
问题在于,由于父类、子类有同名属性,造成子类里面的属性,@synthesize age = _age;
用的是父类的_age
一般不会出问题,但是,在特殊情况下会出问题,比如:
父类是readonly
@property (nonatomic, assign, readonly) int age;
子类是readwrite
@property (nonatomic, assign, readwrite) int age;
子类赋值:
student.age = 10;
会造成崩溃
Terminating app due to uncaught exception 'NSInvalidArgumentException', reason:
'-[YZStudent setAge:]: unrecognized selector sent to instance 0x600002a38150'
原因是,子类调用age的时候,会调用到父类里面的值,而父类是readonly的
面试题解答:
调用顺序
categrory:分原子父
load:父子原分
initialize:父子分原
categrory方法,完全遵守消息发送机制,因此是分子父
load和initialize方法,都是代码中明确写到的:递归调用父类,因此是 父子
load方法代码中明确写的,先调用原类再调用分类,因此是 原分
initialize方法中,没有明确写原类、分类的调用关系,因此,遵循消息发送机制,因此是分原
1、Category如何使用
分类可以在不修改原来类模型的基础上拓充方法;
2、Category的原理是什么?
在编译的时候,转化为category_t类型的结构体类型。
在运行时将所有Category的方法、属性、协议数据合并到一个大数组中(最后面参与编译的Category数据会在数组前面),将合并后的分类数据(包括方法、属性、协议),插入到原来数据的前面;
3、Category与类扩展的区别?
分类可以在不修改原来类模型的基础上拓充方法
• 分类只能扩充方法、不能扩充成员变量;
• 继承可以扩充方法和成员变量,继承会产生新的类;
• 分类是有名称的,类扩展没有名称;
• 分类只能扩充方法、不能扩充成员变量;类扩展可以扩充方法和成员变量;
• 类扩展一般就写在.m文件中,用来扩充私有的方法和成员变量(属性);
• 分类是在运行时将数据合并在类信息中,类扩展是编译的时候它的数据就已经包含在类信息中;
4、Category中load方法是什么时候调用的?load方法能被继承吗?
在程序启动的时候会加载所有的类和分类,并调用所有类和分类的+load方法。也就是不管程序在运行过程中是否调用过该类,在程序初始化的时候,都会调用+load方法且只会调用一次。
@implementation YZPerson
+(void)load
{
NSLog(@"YZPerson-load");
}
@end
@implementation YZStudent
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSLog(@"----");
[YZStudent load];
NSLog(@"----");
}
return 0;
}
结果:
2020-02-26 15:23:20.747580+0800 Category[74061:2678272] YZPerson-load
2020-02-26 15:23:20.747839+0800 Category[74061:2678272] ----
2020-02-26 15:23:20.747862+0800 Category[74061:2678272] YZPerson-load
2020-02-26 15:23:20.747871+0800 Category[74061:2678272] ----
load方法可以被继承
但,[YZStudent load];这种调用方法相当于消息发送机制,走的是isa指针那一套,并不是原有系统调用load方法。
5、load和initialize的区别是什么?他们在category中的调用顺序是怎样的?出现继承的时候他们之间的调用过程是什么?
1.调用方式的不同:
load是通过找到函数地址直接调用的;
initialize是通过消息机制objc_msgSend调用的;
2.调用时刻的不同
load是程序运行的时候,通过runtime加载类、分类的时候调用(只会调用一次)
initialize是类第一次使用的时候调用的;(如果子类没有+initialize方法,父类可能会被调用多次)
load在分类中,按编译顺序调用
initialize在分类中,按编译顺序调用
load在继承中调用是按isa指针调用
initialize在继承中调用是按isa指针调用
6、Category是否可以添加成员变量?如果可以,如何添加?
分类不可以直接添加属性,可以间接通过runtime中的关联方式进行添加属性。
扩展知识点:
更多学习
iOS分类(category),类扩展(extension)—史上最全攻略
iOS底层原理总结 - Category的本质