iOS底层原理探索-分类Category的本质

前言

首先,这里有几个与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;
}
posted @ 2022-01-05 14:09  任淏  阅读(155)  评论(0编辑  收藏  举报