Category的实现原理,Load,Initial调用时机

2024-01-13 16:58

本文主要是介绍Category的实现原理,Load,Initial调用时机,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

在简书上看到这篇文章,读完欲罢不能。分析得实在是透彻,赶紧转一下。添加一些自己体会及思考。

在看这篇文章的同时,可以去下载一下苹果的OC源码,对照分析,更利于体会。而且苹果的源码里面还可以搞清楚好多东西。
苹果的源码中,有很多宝藏,每看一次,都能有所收获。
OC源码下载地址:https://opensource.apple.com/tarballs/objc4/

部分内容原文地址:https://www.jianshu.com/p/fa66c8be42a2

看下面文章前,先思考下面三个问题

  1. Category的实现原理,以及Category为什么只能加方法不能加属性。
  2. Category中有load方法吗?load方法是什么时候调用的?load 方法能继承吗?
  3. load、initialize的区别,以及它们在category重写的时候的调用的次序。

Category的本质
首先我们写一段简单的代码,之后的分析都基于这段代码。
Presen类

// Presen.h
#import <Foundation/Foundation.h>
@interface Preson : NSObject
{
int _age;
}
- (void)run;
@end// Presen.m
#import "Preson.h"
@implementation Preson
- (void)run
{NSLog(@"Person - run");
}
@end

Presen扩展1

// Presen+Test.h
#import "Preson.h"
@interface Preson (Test) <NSCopying>
- (void)test;
+ (void)abc;
@property (assign, nonatomic) int age;
- (void)setAge:(int)age;
- (int)age;
@end// Presen+Test.m
#import "Preson+Test.h"
@implementation Preson (Test)
- (void)test {}
+ (void)abc{ }
- (void)setAge:(int)age {}
- (int)age
{return 10;
}
@end

Presen分类2

// Preson+Test2.h
#import "Preson.h"
@interface Preson (Test2)
@end// Preson+Test2.m
#import "Preson+Test2.h"
@implementation Preson (Test2)
- (void)run
{NSLog(@"Person (Test2) - run");
}
@end

我们知道实例对象的isa指针指向类对象,类对象的isa指针指向元类对象,当p调用run方法时,通过实例对象的isa指针找到类对象,然后在类对象中查找对象方法,如果没有找到,就通过类对象的superclass指针找到父类对象,接着去寻找run方法。

那么当调用分类的方法时,步骤是否和调用对象方法一样呢?
分类中的对象方法依然是存储在类对象中的,同本类对象方法在同一个地方,调用步骤也同调用对象方法一样。如果是类方法的话,也同样是存储在元类对象中。
那么分类方法是如何存储在类对象中的,我们来通过源码看一下分类的底层结构。

分类的底层结构
如何验证上述问题?通过查看分类的源码我们可以找到category_t 结构体。

struct category_t {const char *name;	// 类名称classref_t cls;		// 指向类;struct method_list_t *instanceMethods; // 对象方法struct method_list_t *classMethods; // 类方法struct protocol_list_t *protocols; // 协议struct property_list_t *instanceProperties; // 属性// Fields below this point are not always present on disk.struct property_list_t *_classProperties;   // 不太明白这个classProperties是干嘛的,有空再研究, --heqin
};

从源码基本可以看出我们平时使用categroy的方式,对象方法,类方法,协议,和属性都可以找到对应的存储方式。并且我们发现分类结构体中是不存在成员变量的,因此分类中是不允许添加成员变量的。分类中添加的属性并不会帮助我们自动生成成员变量,只会生成get set方法的声明,需要我们自己去实现。 (如果我们不去实现,则会触发编译警告,编译器会建议我们使用@dynamic关键字。)

通过源码我们发现,分类的方法,协议,属性等好像确实是存放在categroy结构体里面的,那么他又是如何存储在类对象中的呢?
我们来看一下底层的内部方法探寻其中的原理。
首先我们通过命令行将Preson+Test.m文件转化为c++文件,查看其中的编译过程。

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc Preson+Test.m

在分类转化为c++文件中可以看出_category_t结构体中,存放着类名,对象方法列表,类方法列表,协议列表,以及属性列表。
在这里插入图片描述
c++文件中category_t结构体

紧接着,我们可以看到_method_list_t类型的结构体,如下图所示
在这里插入图片描述
对象方法列表结构体

上图中我们发现这个结构体_OBJC_CATEGORY_INSTANCE_METHODS_Preson_Test从名称可以看出是INSTANCE_METHODS对象方法,并且一一对应为上面结构体内赋值。我们可以看到结构体中存储了方法占用的内存,方法数量,以及方法列表。并且从上图中找到分类中我们实现对应的对象方法,test , setAge, age三个方法
接下来我们发现同样的_method_list_t类型的类方法结构体,如下图所示
在这里插入图片描述类对象方法列表

同上面对象方法列表一样,这个我们可以看出是类方法列表结构体 _OBJC_CATEGORY_CLASS_METHODS_Preson_Test,同对象方法结构体相同,同样可以看到我们实现的类方法,abc。
接下来是协议方法列表
在这里插入图片描述协议方法列表

通过上述源码可以看到先将协议方法通过_method_list_t结构体存储,之后通过_protocol_t结构体存储在_OBJC_CATEGORY_PROTOCOLS_Preson_Test中同_protocol_list_t结构体一一对应,分别为protocol_count 协议数量以及存储了协议方法的_protocol_t结构体。
最后我们可以看到属性列表
在这里插入图片描述
属性列表结构体

属性列表结构体_OBJC_PROP_LIST_Preson_Test同_prop_list_t结构体对应,存储属性的占用空间,属性属性数量,以及属性列表,从上图中可以看到我们自己写的age属性。
最后我们可以看到定义了_OBJC_CATEGORY_Preson_Test结构体,并且将我们上面着重分析的结构体一一赋值,我们通过两张图片对照一下。
在这里插入图片描述
_category_t
在这里插入图片描述
_OBJC_CATEGORY_Preson_Test

上下两张图一一对应,并且我们看到定义_class_t类型的OBJC_CLASS_Preson结构体,最后将_OBJC_CATEGORY_Preson_Test的cls指针指向OBJC_CLASS_Preson结构体地址。我们这里可以看出,cls指针指向的应该是分类的主类类对象的地址。
通过以上分析我们发现。分类源码中确实是将我们定义的对象方法,类方法,属性等都存放在catagory_t结构体中。接下来我们在回到runtime源码查看catagory_t存储的方法,属性,协议等是如何存储在类对象中的。
首先来到runtime初始化函数
在这里插入图片描述runtime初始化函数

接着我们来到 &map_images读取模块(images这里代表模块),来到map_images_nolock函数中找到_read_images函数,在_read_images函数中我们找到分类相关代码
在这里插入图片描述
Discover categories代码

从上述代码中我们可以知道这段代码是用来查找有没有分类的。通过_getObjc2CategoryList函数获取到分类列表之后,进行遍历,获取其中的方法,协议,属性等。可以看到最终都调用了remethodizeClass(cls);函数。我们来到remethodizeClass(cls);函数内部查看。
在这里插入图片描述
remethodizeClass函数内部

通过上述代码我们发现attachCategories函数接收了类对象cls和分类数组cats,如我们一开始写的代码所示,一个类可以有多个分类。之前我们说到分类信息存储在category_t结构体中,那么多个分类则保存在category_list中。

我们来到attachCategories函数内部。
在这里插入图片描述
attachCategories函数内部实现

  1. 上述源码中可以看出,首先根据方法列表,属性列表,协议列表,malloc分配内存,根据多少个分类以及每一块方法需要多少内存来分配相应的内存地址。之后从分类数组里面往三个数组里面存放分类数组里面存放的分类方法,属性以及协议放入对应mlist、proplists、protolosts数组中,这三个数组放着所有分类的方法,属性和协议。
  2. 之后通过类对象的data()方法,拿到类对象的class_rw_t结构体rw,在class结构中我们介绍过,class_rw_t中存放着类对象的方法,属性和协议等数据,rw结构体通过类对象的data方法获取,所以rw里面存放这类对象里面的数据。
    之后分别通过rw调用方法列表、属性列表、协议列表的attachList函数,将所有的分类的方法、属性、协议列表数组传进去,我们大致可以猜想到在attachList方法内部将分类和本类相应的对象方法,属性,和协议进行了合并。
  3. 注意上面的while(i–), 这里是一个倒序合并分类。 即最后编译的category的methodList会先放入到mlist数组中。这也是为什么后编译的category会生效的原因。–heqin

我们来看一下attachLists函数内部。
在这里插入图片描述
attachLists函数内部实现

上述源代码中有两个重要的数组
array()->lists: 类对象原来的方法列表,属性列表,协议列表。
addedLists:传入所有分类的方法列表,属性列表,协议列表。
attachLists函数中最重要的两个方法为memmove内存移动和memcpy内存拷贝。我们先来分别看一下这两个函数 (简单说就是函数的拷贝)

// memmove :内存移动。
/*  __dst : 移动内存的目的地
*   __src : 被移动的内存首地址
*   __len : 被移动的内存长度
*   将__src的内存移动__len块内存到__dst中
*/
void    *memmove(void *__dst, const void *__src, size_t __len);// memcpy :内存拷贝。
/*  __dst : 拷贝内存的拷贝目的地
*   __src : 被拷贝的内存首地址
*   __n : 被移动的内存长度
*   将__src的内存移动__n块内存到__dst中
*/
void    *memcpy(void *__dst, const void *__src, size_t __n);

下面我们图示经过memmove和memcpy方法过后的内存变化。
在这里插入图片描述
未经过内存移动和拷贝时

经过memmove方法之后,内存变化为

// array()->lists 原来方法、属性、协议列表数组
// addedCount 分类数组长度
// oldCount * sizeof(array()->lists[0]) 原来数组占据的空间
memmove(array()->lists + addedCount, array()->lists, oldCount * sizeof(array()->lists[0]));

在这里插入图片描述
memmove方法之后内存变化

经过memmove方法之后,我们发现,虽然本类的方法,属性,协议列表会分别后移,但是本类的对应数组的指针依然指向原始位置。
memcpy方法之后,内存变化

// array()->lists 原来方法、属性、协议列表数组
// addedLists 分类方法、属性、协议列表数组
// addedCount * sizeof(array()->lists[0]) 原来数组占据的空间
memcpy(array()->lists, addedLists, addedCount * sizeof(array()->lists[0]));

在这里插入图片描述
memmove方法之后,内存变化

我们发现原来指针并没有改变,至始至终指向开头的位置。并且经过memmove和memcpy方法之后,分类的方法,属性,协议列表被放在了类对象中原本存储的方法,属性,协议列表前面。
那么为什么要将分类方法的列表追加到本来的对象方法前面呢,这样做的目的是为了保证分类方法优先调用,我们知道当分类重写本类的方法时,会覆盖本类的方法。 注:原始方法被往后移动了。
其实经过上面的分析我们知道本质上并不是覆盖,而是优先调用。本类的方法依然在内存中的。我们可以通过打印所有类的所有方法名来查看

- (void)printMethodNamesOfClass:(Class)cls
{unsigned int count;// 获得方法数组Method *methodList = class_copyMethodList(cls, &count);// 存储方法名NSMutableString *methodNames = [NSMutableString string];// 遍历所有的方法for (int i = 0; i < count; i++) {// 获得方法Method method = methodList[i];// 获得方法名NSString *methodName = NSStringFromSelector(method_getName(method));// 拼接方法名[methodNames appendString:methodName];[methodNames appendString:@", "];}// 释放free(methodList);// 打印方法名NSLog(@"%@ - %@", cls, methodNames);
}
- (void)viewDidLoad {[super viewDidLoad];    Preson *p = [[Preson alloc] init];[p run];[self printMethodNamesOfClass:[Preson class]];
}

通过下图中打印内容可以发现,调用的是Test2中的run方法,并且Person类中存储着两个run方法。
在这里插入图片描述
打印所有方法

总结:
问: Category的实现原理,以及Category为什么只能加方法不能加属性?
答:分类的实现原理是将category中的方法,属性,协议数据放在category_t结构体中,然后将结构体内的方法列表拷贝到类对象的方法列表中。
Category可以添加属性,但是并不会自动生成成员变量及set/get方法。因为category_t结构体中并不存在成员变量。通过之前对对象的分析我们知道成员变量是存放在实例对象中的,并且编译的那一刻就已经决定好了。而分类是在运行时才去加载的。那么我们就无法再程序运行时将分类的成员变量中添加到实例对象的结构体中。因此分类中不可以添加成员变量。
load 和 initialize
load方法会在程序启动就会调用,当装载类信息的时候就会调用。
调用顺序看一下源代码。
在这里插入图片描述
load方法调用顺序

通过源码我们发现是优先调用类的load方法,之后调用分类的load方法。

我们通过代码验证一下:
我们添加Student继承Presen类,并添加Student+Test分类,分别重写只+load方法,其他什么都不做通过打印发现
在这里插入图片描述
load方法打印

确实是优先调用类的load方法之后调用分类的load方法,不过调用类的load方法之前会保证其父类已经调用过load方法。

之后我们为Preson、Student 、Student+Test 添加initialize方法。
我们知道当类第一次接收到消息时,就会调用initialize,相当于第一次使用类的时候就会调用initialize方法。调用子类的initialize之前,会先保证调用父类的initialize方法。如果之前已经调用过initialize,就不会再调用initialize方法了。当分类重写initialize方法时会先调用分类的方法。但是load方法并不会被覆盖,首先我们来看一下initialize的源码。
在这里插入图片描述
initialize调用源码

上图中我们发现,initialize是通过消息发送机制调用的,消息发送机制通过isa指针找到对应的方法与实现,因此先找到分类方法中的实现,会优先调用分类方法中的实现。
我们再来看一下load方法的调用源码
在这里插入图片描述
load方法的调用源码

我们看到load方法中直接拿到load方法的内存地址直接调用方法,不在是通过消息发送机制调用。
在这里插入图片描述
分类load方法的调用源码

我们可以看到分类中也是通过直接拿到load方法的地址进行调用。因此正如我们之前试验的一样,分类中重写load方法,并不会优先调用分类的load方法,而不调用本类中的load方法了。

总结
问:Category中有load方法吗?load方法是什么时候调用的?load 方法能继承吗?
答:Category中有load方法,load方法在程序启动装载类信息的时候就会调用。load方法可以继承。调用子类的load方法之前,会先调用父类的load方法
问:load、initialize的区别,以及它们在category重写的时候的调用的次序。
答:区别在于调用方式和调用时刻
调用方式:load是根据函数地址直接调用,initialize是通过objc_msgSend调用
调用时刻:load是runtime加载类、分类的时候调用(只会调用1次),initialize是类第一次接收到消息的时候调用,每一个类只会initialize一次(父类的initialize方法可能会被调用多次)
调用顺序:先调用类的load方法,先编译那个类,就先调用load。在调用load之前会先调用父类的load方法。分类中load方法不会覆盖本类的load方法,先编译的分类优先调用load方法。initialize先初始化父类,之后再初始化子类。如果子类没有实现+initialize,会调用父类的+initialize(所以父类的+initialize可能会被调用多次),如果分类实现了+initialize,就覆盖类本身的+initialize调用。

这篇关于Category的实现原理,Load,Initial调用时机的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/602159

相关文章

使用animation.css库快速实现CSS3旋转动画效果

《使用animation.css库快速实现CSS3旋转动画效果》随着Web技术的不断发展,动画效果已经成为了网页设计中不可或缺的一部分,本文将深入探讨animation.css的工作原理,如何使用以及... 目录1. css3动画技术简介2. animation.css库介绍2.1 animation.cs

Java进行日期解析与格式化的实现代码

《Java进行日期解析与格式化的实现代码》使用Java搭配ApacheCommonsLang3和Natty库,可以实现灵活高效的日期解析与格式化,本文将通过相关示例为大家讲讲具体的实践操作,需要的可以... 目录一、背景二、依赖介绍1. Apache Commons Lang32. Natty三、核心实现代

SpringBoot实现接口数据加解密的三种实战方案

《SpringBoot实现接口数据加解密的三种实战方案》在金融支付、用户隐私信息传输等场景中,接口数据若以明文传输,极易被中间人攻击窃取,SpringBoot提供了多种优雅的加解密实现方案,本文将从原... 目录一、为什么需要接口数据加解密?二、核心加解密算法选择1. 对称加密(AES)2. 非对称加密(R

基于Go语言实现Base62编码的三种方式以及对比分析

《基于Go语言实现Base62编码的三种方式以及对比分析》Base62编码是一种在字符编码中使用62个字符的编码方式,在计算机科学中,,Go语言是一种静态类型、编译型语言,它由Google开发并开源,... 目录一、标准库现状与解决方案1. 标准库对比表2. 解决方案完整实现代码(含边界处理)二、关键实现细

python通过curl实现访问deepseek的API

《python通过curl实现访问deepseek的API》这篇文章主要为大家详细介绍了python如何通过curl实现访问deepseek的API,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编... API申请和充值下面是deepeek的API网站https://platform.deepsee

SpringBoot实现二维码生成的详细步骤与完整代码

《SpringBoot实现二维码生成的详细步骤与完整代码》如今,二维码的应用场景非常广泛,从支付到信息分享,二维码都扮演着重要角色,SpringBoot是一个非常流行的Java基于Spring框架的微... 目录一、环境搭建二、创建 Spring Boot 项目三、引入二维码生成依赖四、编写二维码生成代码五

MyBatisX逆向工程的实现示例

《MyBatisX逆向工程的实现示例》本文主要介绍了MyBatisX逆向工程的实现示例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学... 目录逆向工程准备好数据库、表安装MyBATisX插件项目连接数据库引入依赖pom.XML生成实体类、

C#实现查找并删除PDF中的空白页面

《C#实现查找并删除PDF中的空白页面》PDF文件中的空白页并不少见,因为它们有可能是作者有意留下的,也有可能是在处理文档时不小心添加的,下面我们来看看如何使用Spire.PDFfor.NET通过C#... 目录安装 Spire.PDF for .NETC# 查找并删除 PDF 文档中的空白页C# 添加与删

Java实现MinIO文件上传的加解密操作

《Java实现MinIO文件上传的加解密操作》在云存储场景中,数据安全是核心需求之一,MinIO作为高性能对象存储服务,支持通过客户端加密(CSE)在数据上传前完成加密,下面我们来看看如何通过Java... 目录一、背景与需求二、技术选型与原理1. 加密方案对比2. 核心算法选择三、完整代码实现1. 加密上

Java使用WebView实现桌面程序的技术指南

《Java使用WebView实现桌面程序的技术指南》在现代软件开发中,许多应用需要在桌面程序中嵌入Web页面,例如,你可能需要在Java桌面应用中嵌入一部分Web前端,或者加载一个HTML5界面以增强... 目录1、简述2、WebView 特点3、搭建 WebView 示例3.1 添加 JavaFX 依赖3