前言
首先,这里有几个与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的结构体变量。
对分类的源码分析:
1.运行时的初始化:
2.调用_dyld_objc_notify_register方法,传入map_images地址(方法地址或者函数地址):
3.调用map_images_nolock方法,在map_images_nolock方法中调用_read_images方法(镜像,加载一些模块):
4.加载分类信息(分类信息是个二维数组):
5.找到remethodizeClass(cls)核心方法的实现(给类对象和原类对象重新组织方法):
static void
// cls 类对象
// cats 分类列表
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被加载的过程,也是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可能被调用多次)
问:分类可以添加属性吗?
我们知道,分类只能添加方法,不能添加属性。这句话其实不严谨,应该说:
分类只能添加方法,不能直接添加属性,可以间接添加属性。
在普通类中,@property (assign, nonatomic) int age;
会做三件事:
生成age的成员变量
生成age的get、set方法的声明
生成age的get、set方法的实现
而在分类中,@property (assign, nonatomic) int weight;可以写,但是它的作用只有一个:
生成weight的get、set方法的声明
如何实现为分类间接添加属性呢?
我们可以通过runtime中的关联对象的方法(objc_setAssociatedObject)实现分类中属性的get、set方法的实现,具体实现如下:
@interface YZPerson : NSObject
@property (assign, nonatomic) int age;
@end
@interface YZPerson (Eat)
@property (copy, nonatomic) NSString *name;
@end
#import <objc/runtime.h>
@implementation YZPerson (Eat)
- (void)setName:(NSString *)name
{
objc_setAssociatedObject(self, @selector(setName:), name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (NSString *)name
{
return objc_getAssociatedObject(self, @selector(setName:));
}
@end
YZPerson *person1 = [[YZPerson alloc] init];
person1.age = 10;
person1.name = @"zhangSan";
YZPerson *person2 = [[YZPerson alloc] init];
person2.age = 20;
person2.name = @"liSi";
NSLog(@"person1.age = %d, person2.age = %d", person1.age, person2.age);
NSLog(@"person1.name = %@, person2.name = %@", person1.name, person2.name);
结果:
2020-02-27 16:26:56.015710+0800 Category[6423:189583] person1.age = 10, person2.age = 20
2020-02-27 16:26:56.015980+0800 Category[6423:189583] person1.name = zhangSan, person2.name = liSi
面试题解答:
调用顺序
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的本质
{
"_track_id" = 3492084489;
"anonymous_id" = "2AADC4B8-CE6C-4BE2-BEBC-4DA23CEC7A74";
"distinct_id" = newId;
event = "$AppPageLeave";
identities = {
"$identity_idfv" = "2AADC4B8-CE6C-4BE2-BEBC-4DA23CEC7A74";
"$identity_login_id" = newId;
};
lib = {
"$app_version" = "1.4.1";
"$lib" = iOS;
"$lib_method" = code;
"$lib_version" = "4.1.3";
};
"login_id" = newId;
properties = {
"$app_id" = "cn.sensorsdata.SensorsData";
"$app_name" = SensorsData;
"$app_version" = "1.4.1";
"$device_id" = "2AADC4B8-CE6C-4BE2-BEBC-4DA23CEC7A74";
"$is_first_day" = 0;
"$lib" = iOS;
"$lib_method" = code;
"$lib_version" = "4.1.3";
"$manufacturer" = Apple;
"$model" = "x86_64";
"$network_type" = WIFI;
"$os" = iOS;
"$os_version" = "15.2";
"$screen_height" = 896;
"$screen_name" = DemoController;
"$screen_width" = 414;
"$timezone_offset" = "-480";
"$title" = "SensorsAnalytics iOS Demo";
"$url" = WoShiYiGeURL;
"$wifi" = 1;
AAA = "2AADC4B8-CE6C-4BE2-BEBC-4DA23CEC7A74";
"__APPState__" = 0;
"event_duration" = "17.352";
};
time = 1640921936297;
type = track;
}