1、什么是 Category(分类)?
分类是 Objective-C 2.0 添加的语言特性,主要作用是为已经存在的类添加方法。
虽然继承也能为已有类增加新的方法,但有时继承关系会增加复杂度,在运行时,也无法与父类的原始方法进行区分。通常
Category(分类)
有以下几种使用场景:
- 把类的不同实现方法分开到不同的文件里。
- 声明私有方法。
- 模拟多继承。
- 将
framework
私有方法公开化。2、Category(分类)和Extension(扩展)的区别是什么?
Extension
是在编译阶段与该类同时编译的,它的数据就已经包含在类信息中;
Category
是在运行时,才会将数据合并到类信息中。
Extension
可以直接添加属性;
但是Category
不可以,只能通过runtime
添加。
Extension
中声明的方法只能在该类的@implementation
中实现,所以无法对系统的类(例如NSString
类)使用Extension
;
而Category
可以为系统的类添加方法。3、为什么 Category(分类)不能像 Extension(扩展)一样添加成员变量?
因为Extension
是在编译阶段与该类同时编译的,所以作为类的一部分,可以在编译阶段为类添加成员变量;
而Category
是在运行时期间决定的,成员变量的内存布局已经在编译阶段确定好了(也就是说objc_class
结构体大小是固定的),如果在运行时阶段添加成员变量的话,就会破坏原有类的内存布局,所以Category
无法直接添加成员变量,只能通过runtime
添加setter/getter
达到添加属性的效果(属性和成员变量不一样)。
另外,分类的methodList
是一个二维数组,虽没办法扩展methodLists
指向的内存区域,但可改变methodList
指向的内存区域的值(存储的是指针),所以可以动态添加方法。
从上面我们知道,扩展和分类是有区别的,那么现在就从编译和运行两个阶段了解一下分类。
1. 编译时期
我们把文件编译成c++
源码看一下:
将
.m
文件由OC
转C++
源码方法如下:
打开终端,执行cd 文件所在目录
命令,
然后执行clang -rewrite-objc xxx.m
,
之后xxx.m
所在目录下就会生成一个xxx.cpp
文件,这就是相关的C++
源码。
编译分类文件,会创建一个_Category_t
结构体(如果一个类有多个分类,则 Category
数组中对应多个 Category
),分类的本质就是结构体,存储着分类的实例方法、类方法、属性、协议:
:
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;//属性列表
};
//Person 类的 Category 结构体赋值
static struct _category_t _OBJC_$_CATEGORY_Person_$_Eat __attribute__ ((used, section ("__DATA,__objc_const"))) =
{
"Person", // 类名
0, // &OBJC_CLASS_$_Person, // cls
(const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Eat, // 实例方法列表
0, // 类方法列表
0, // 协议列表
0, // 属性列表
};
运行时,再把各个分类的结构体添加合并到原本类中。所有类的分类都会放到__objc_catlist
列表中:
static struct _category_t *L_OBJC_LABEL_CATEGORY_$ [1] __attribute__((used, section ("__DATA, __objc_catlist,regular,no_dead_strip")))= {
&_OBJC_$_CATEGORY_Person_$_Eat,
};
2. 运行时期
Category 是如何被加载的?
dyld
是苹果的动态加载器,用来加载image
(注意这里image
不是指图片,而是Mach-O
格式的二进制可执行文件)。- 当程序启动时,系统内核首先会加载
dyld
, 然后dyld
会将我们APP
所依赖的各种库加载到内存空间中,其中就包括libobjc
库(OC
和runtime
), 这些工作,是在APP
的main
函数执行前完成的。- 接着通过
Runtime
加载某个类的所有Category
数据。先把原类数据(方法、属性、协议)的数组指针后移(后移位置取决于分类大小),再把各个分类的数据(方法、属性、协议)合并放到前面,所以当对象调用分类和类中都有的同名方法时,先调用分类的方法(后编译先调用)。
dyld加载的流程大致是这样:
- 配置环境变量;
- 加载共享缓存;
- 初始化主 APP;
- 插入动态缓存库;
- 链接主程序;
- 链接插入的动态库;
- 初始化主程序:OC, C++ 全局变量初始化;
- 返回主程序入口函数。
下一篇:Category(分类)加载流程
下下篇:Category(分类)关联属性
网友评论