Objective-C之Category的底层实现原理

本文深入探讨Objective-C Category的使用场景和实现原理。Category用于拆分类的实现,便于管理和维护。文章分析了Category的底层结构,指出Category方法在运行时合并到类对象,且编译顺序影响方法调用顺序。同时,文章解答了关于Category方法覆盖、调用顺序等常见问题。
摘要由CSDN通过智能技术生成

Objective-C的+load方法调用原理分析
Objective-C的+initialize方法调用原理分析

Category的使用场景

我个人粗浅理解,就是将一个类的实现,拆解成小的模块,便于管理和维护。因为实际项目中,有些类的功能可能会非常复杂,导致一个类的代码过多,这对后期修改和维护是比较不利的,所以category方便了程序员,可以根据功能,业务等形式的划分,将类的一大堆方法分组放置以及调用。
#####有趣的思考
先来看一个最简单的category结构,一下代码定义了一个CLPerson类 和它的一个category CLPerson+Test

// ******************** CLPerson
#import <Foundation/Foundation.h>
@interface CLPerson : NSObject
-(void)run;
@end

#import "CLPerson.h"  
@implementation CLPerson
-(void)run
{
   
    NSLog(@"CLPerson Run");
}
@end

// ******************** CLPerson+Test
#import "CLPerson.h"
@interface CLPerson (Test)
-(void)test;
@end

#import "CLPerson+Test.h"
@implementation CLPerson (Test)
-(void)test{
   
    NSLog(@"Test");
}
@end

// ******************** CLPerson+Eat
#import "CLPerson.h"
@interface CLPerson (Eat)
-(void)eat;
@end

#import "CLPerson+Eat.h"
@implementation CLPerson (Eat)
-(void)eat{
   
    NSLog(@"Eat");
}
@end

请问❓❓❓:以下的两个方法调用,底层到底发生了什么,它们本质是否相同?

CLPerson *person = [[CLPerson alloc]init];
[person run]; //类的实例方法调用
[person test];//分类的实例方法调用
[person eat];//分类的实例方法调用

我们都知道,[实例对象 方法]这种写法,经过底层转换之后,实际上就是,objc_msgSend(类对象, @selector(实例方法)),也就我们oc的一个基本概念,消息发送机制。因此,我们可以推定,***[person run]这句代码,在消息发送机制下,首先会根据 personisa指针找到CLPerson的类对象,然后在类对象的方法列表(method_list_t * methods)里面找到该方法的实现,然后进行调用。***
接下来,你肯定会想

  • 那么[person test][person eat]呢?它的消息是发送给谁呢?
  • 是发送给person的类对象吗?
  • 还是说,对于CLPerson+Test.hCLPerson+Eat.h来说,也有其独立对应的分类对象呢?
    带着这些思考和问题,我们接下来一步一步地进行拆解。


Category的实现原理

底层结构——所有一切始于编译

要想知道原理,不要猜,也不要轻易相信别人说的东西,自己验证一下才是最靠谱的。在命令行下,进入CLPerson+Test.m文件所在路径执行以下命令–>

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

得到编译后的c++文件CLPerson+Test.cpp,将其拖入xcode项目中进行查看,但是不要加入编译列表,否则程序跑不起来。直接查看文件底部,就可以找到category相关的底层信息,请看下图剖析

上图比较粗糙,请谅解,但比文字描述来的更加直观,上面基本上分析清楚了在编译结束之后,category是以何种形式存在的,现在用文字来总结一下:
category经过编译过程之后,系统为其定义了如下的一个结构体

//注意,编译后的cpp文件一般比较长,会有好几万行,
//一般我们关注类结构相关的信息,都在最后,
//所以可以直接把文件拖到底,便可以找到这些信息
struct _category_t {
   
	const char  *name; //用来存放类名
	struct _class_t *cls;
	const struct _method_list_t *instance_methods;//用来存放category里面的实例方法列表
	const struct _method_list_t *class_methods;//用来存放category里面的类方法列表
	const struct _protocol_list_t *protocols;//用来存放category里面的协议列表
	const struct _prop_list_t *properties;//用来存放category里面的属性列表
}; 

这个struct _category_t结构体,就是在程序在编译之后,被用来存放category的相关信息(instance methods, class methodsprotocolproperty)的。

反过来描述,编译的时候,系统会给每一个category生成一个对应的结构体变量,而且他们都是struct _category_t类型的,然后把category里面的信息存到这个变量里面。

在我的示例里面,这个变量的名称叫_OBJC_$_CATEGORY_CLPerson_$_Test,这个名字很清晰的表明,它存储的是Objective-c下的CLPerson类的Test分类的信息。

struct _category_t中定义了六个成员变量,除去其中的第二个,我个人还没搞明白有什么用,其他的五个作用则非常清晰了

  • const char *name;
    上图中的a部分,其值表示category所对应的类的名字。
  • const struct _method_list_t *instance_methods;
    上图中的b部分,其值就是实例方法列表,可以看到里面正好放了我们定义的实例方法 -test
  • const struct _method_list_t *class_methods;
    上图中的c部分,其值就是类方法列表,可以看到里面放了我们定义的类方法 -classTest
  • const struct _protocol_list_t *protocols;
    上图中的d部分,其值就是协议列表,可以看到里面存放了 NSCoping协议
  • const struct _prop_list_t *properties;
    上图中的e部分,其值就是属性,可以看到里面有我们定义的age属性
源码分析

上面的篇章,我们通过查看编译后的cpp文件,了解了category在编译阶段完成后的存在形式,以CLPerson+Test为例,它所对应的struct _category_t变量中,第一个成员变量name的值为"CLPerson"(CLPerson+Eat对应的name也是"CLPerson",可以自行验证),而且根据我在对象的本质(上)——OC对象的底层实现中所讨论所得出的结果可以知道,一个OC类XXX在底层都存在一个对应的C++结构体实现struct XXX_IMPL,但我们在CLPerson+Test.cpp文件中,并没有发现 struct CLPerson+Test_IMPL/struct CLPerson+Eat_IMPL,因此,我猜想CLPersoncategory中的信息,应该还是存储在CLPerson所对应的class对象和meta-class对象中,category自己并没有独立的class对象和meta-class对象。CLPerson旗下的所有category里面的信息,应该是在某个阶段被合并到了类的CLPersonclass对象和meta-class对象中。从编译的结果看,我们并没有发现有合并的操作,仅仅是给每个category生成了对应的struct _category_t类型的变量,存放其信息。所以我合理怀疑,合并操作应该是发生在Runtime阶段。

为了证明以上猜想,我们还是要挖掘Runtime的源码。我们先去苹果官网下载一份objc4的最新源码。然后我们直接寻找objc-os.mm文件,这个文件可以看作是Runtime进行初始化的地方。然后找到_objc_init()方法,这个方法是Runtime被加载后执行的第一个方法,可以理解成Runtime的入口方法。

/***********************************************************************
* _objc_init
* Bootstrap initialization. Registers our image notifier w
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值