Category的概述以及实现原理
Category和Class Extension 类扩展的区别
Category中load方法是什么时候调用的?
使用关联对象为分类添加成员变量
load 与 initialize 的区别
作用
分类的主要作用就是在不改变原有类的前提下,动态地给这个类添加一些方法。具体在项目中大概有这么几个好处:
1、分类在架构设计上面可以达到解耦的效果。 开发过程中比较繁重啰嗦的业务代码对项目的可读性造成了压力,为追求架构清晰,降低维护成本低,可以通过分类进行梳理。典型的就是将项目中 AppDelegate 拆分。 AppDelegate 作为程序的入口,一般都会实现各种第三方 SDK 的初始化、写各种版本的容错代码、实现通知、支付逻辑等等功能,所以 AppDelegate 这个类很容易臃肿,这个时候可以通过实现 AppDelegate 分类来将不同的业务代码分离。
2、通过实现分类的 load 方法来实现 Method Swizzling
3、通过分类来为已知的类扩展方法和属性,Category 不会为我们的属性添加实例变量和存取方法,我们可以通过关联对象这个技术来实现对象绑定
注意事项:
1:如果category中有和原有类同名的方法,会优先调用分类中的方法,就是说会忽略原有类的方法。
2:如果多个分类中都有和原有类中同名的方法,那么调用该方法的时候执行谁由编译器决定,编译器会执行最后一个参与编译的分类中的方法。
3:过渡使用分类 也会导致APP项目 支离破碎感+性能降低
4:我们在分类中添加了属性之后,系统只是为我们生成了get方法和set方法的声明
,并没有为我们生成成员变量和方法的实现
5: 名字相同的分类会引起编译报错;
Category 的实现原理
我们知道 Objective-C 通过 Runtime 运行时来实现动态语言这个特性,所有的类和对象,在 Runtime 中都是用结构体来表示的,Category 在 Runtime 中是用结构体 category_t 来表示的,下面是结构体 category_t 具体表示:
typedef 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;//添加的所有属性
} category_t;
通过结构体 category_t 可以知道,在 Category 中我们可以增加实例方法、类方法、协议、属性。我们这里简述下 Category 的实现原理:
1、在编译时期,会将分类中实现的方法生成一个结构体 method_list_t 、将声明的属性生成一个结构体 property_list_t ,然后通过这些结构体生成一个结构体 category_t 。
2、然后将结构体 category_t 保存下来
3、在运行时期,Runtime 会拿到编译时期我们保存下来的结构体 category_t。然后将结构体 category_t 中的实例方法列表、协议列表、属性列表添加到主类中。将结构体 category_t 中的类方法列表、协议列表添加到主类的 metaClass 中。
- 在程序运行的时候,通过Runtime加载某个类的所有Category数据,同时将Category中的方法、属性、协议数据合并到一个大数组中,后来参与编译的Category数据,会在数组的前面。
- 将合并后的分类数据包括方法、属性、协议等信息,插入到类原来数据的前面,所以这也就造成了如果分类和类中有相同的方法,调用的时候会优先调用分类的方法,而且如果多个分类中有相同的名称方法则会优先调用最后参与编译的
例如上图,如果这两个分类中有相同名称的方法则最终会调用红框中的
因为源码太多 就不在一一贴图解释了,这里列举了源码的阅读顺序,感兴趣的朋友可以自己尝试着去仔细分析一下
objc-os.mm
_objc_init
map_images
map_images_nolock
objc-runtime-new.mm
_read_images
remethodizeClass
attachCategories
attachLists
realloc、memmove、 memcpy
Category 为什么不能添加实例变量
通过结构体 category_t ,我们就可以知道,在 Category 中我们可以增加实例方法、类方法、协议、属性。这里没有 objc_ivar_list 结构体,代表我们不可以在分类中添加实例变量。
因为在运行期,对象的内存布局已经确定,如果添加实例变量就会破坏类的内部布局,这个就是 Category 中不能添加实例变量的根本原因。
使用关联对象为分类添加成员变量
上面我们说到 在分类中添加了属性之后,系统只是为我们生成了get方法和set方法的声明,并没有为我们生成成员变量和方法的实现,下面我们来验证这一说法
我们为GSPerson
对象添加了一个分类, 并且在分类中设置了一个height
的属性,下面我们来使用一下该分类对象:
#import "GSPerson.h"
@interface GSPerson (Utility)
@property (nonatomic, assign) int height;
@end
我们为height
赋值20 ,运行起来之后竟然奔溃了,并且提示我们
reason: '-[GSPerson setHeight:]: unrecognized selector sent to instance 0x1006259a0'
是不是印证了我们的说法?
那么我们该如何为Category
添加一个成员变量呢?答案是关联对象:
其实主要涉及到关联对象的两个方法:objc_getAssociatedObject
和 objc_setAssociatedObject
/*
参数一:id object : 获取哪个对象里面的关联的属性。
参数二:void * == id key : 什么属性,与objc_setAssociatedObject中的key相对应,即通过key值取出value。
*/
objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key)
/*
参数一:id object : 给哪个对象添加属性,这里要给自己添加属性,用self
参数二:void * == id key : 属性名,根据key获取关联对象的属性的值,在objc_getAssociatedObject中通过此key获得属性的值并返回。
参数三:id value : 关联的值,也就是set方法传入的值给属性去保存。
参数四:objc_AssociationPolicy policy : 策略,属性以什么形式保存。
*/
objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key,
id _Nullable value, objc_AssociationPolicy policy)
其中objc_AssociationPolicy policy
策略是个枚举值
typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
OBJC_ASSOCIATION_ASSIGN = 0, // 指定一个弱引用相关联的对象
OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, // 指定相关对象的强引用,非原子性
OBJC_ASSOCIATION_COPY_NONATOMIC = 3, // 指定相关的对象被复制,非原子性
OBJC_ASSOCIATION_RETAIN = 01401, // 指定相关对象的强引用,原子性
OBJC_ASSOCIATION_COPY = 01403 // 指定相关的对象被复制,原子性
};
具体的对应关系如下所示:
了解了这两个方法后,为关联对象添加属性就非常的简单了,比如为 GSPerson
对象添加height
属性具体如下:
#import "GSPerson+Utility.h"
#import <objc/runtime.h>
@implementation GSPerson (Utility)
-(void)setHeight:(int)height{
//key值只要是一个指针即可,我们可以传入@selector(name)
objc_setAssociatedObject(self, @selector(height), @(height), OBJC_ASSOCIATION_ASSIGN);
//或者
//objc_setAssociatedObject(self, @"height", @(height), OBJC_ASSOCIATION_ASSIGN);
}
-(int)height{
//_cmd == @selector(height)
return [objc_getAssociatedObject(self, _cmd) intValue];
//或者
//return [objc_getAssociatedObject(self, @"height") intValue];
}
@end
那么问题来了,我们用关联对象为Category
添加的成员变量具体是存储在了哪呢?
下面我们通过一幅图来了解下关联对象的本质:
具体到更直观的数据结构上,我们可以看下图
- 关联对象是由
AssociationManager
全局类管理并存储在AssociationHashMap
中。 - 所有对象的关联内容都存在于一个全局容器中,和宿主类是无关的。
Extension 类扩展
一般用类扩展做什么工作?
- 声明私有属性
- 声明私有方法,一般没有多大用,只是为了阅读方便而已
- 声明私有成员变量
类扩展有什么特点?
- 编译时决议
- 只以声明的形式存在,多数情况下存在于宿主类的.m文件中
- 不能为系统类添加扩展
Category和Class Extension 类扩展的区别
有了上面的特点,我们就方便的将分类和扩展放到一起比较了。
- 分类是在运行时才会将数据合并到类信息中。而类扩展在程序编译的时候,它的数据就已经包含在类信息中了
- 分类中既可以有声明也可以有实现,而类扩展只以声明的形式存在,多数情况下存在于宿主类的.m文件中
- 可以为系统类添加分类,但是不能添加扩展
- 在分类中只能添加“方法”,不能增加成员变量。而扩展中既可以添加方法也可以添加成员变量。
Category中load方法是什么时候调用的?
load方法在程序启动加载类信息的时候就会调用。它是根据函数地址直接调用的,而不是通过消息机制的。调用顺序如下:
-
先调用类的+load
- 按照编译先后顺序调用(先编译,先调用)
- 调用子类的+load之前会先调用父类的+load
-
再调用分类的+load
- 按照编译的先后顺序调用(先编译,先调用)
load、initialize的区别
1. 调用顺序
以main为分界,load方法在main函数之前执行,initialize在main函数之后执行
2.相同点和不同点
2.1 相同点
- load和initialize会被自动调用,不能手动调用它们。
- 子类实现了load和initialize的话,会隐式调用父类的load和initialize方法
- load和initialize方法内部使用了锁,因此它们是线程安全的。
2.2 不同点
子类中没有实现load方法的话,不会调用父类的load方法;而子类如果没有实现initialize方法的话,也会自动调用父类的initialize方法。
load方法是在类被装在进来的时候就会调用,initialize在第一次给某个类发送消息时调用(比如实例化一个对象),并且只会调用一次,是懒加载模式,如果这个类一直没有使用,就不回调用到initialize方法。
3. load
在执行load方法之前,会调用load_images方法,用来扫描镜像中的+ load符号,将需要调用 load 方法的类添加到一个列表中loadable_classes,在这个列表中,会先把父类加入到待加载列表,这样保证父类在子类前调用load方法,而分类中的load方法会在类的load的方法后面加入另外一个待加载列表loadable_categories,这样保证了两个规则:
- 父类先于子类调用
- 类先于分类调用
在扫描完load方法加入到待加载方法后,会调用call_load_methods,先从loadable_classes调用类的load方法,call_class_loads;调用完loadable_classes后会调用loadable_categories中分类的load方法,call_category_loads。
调用顺序如下:
- 父类load先于类添加到loadable_classes列表,通过call_class_loads,调用列表中的load方法,这样父类的load先于类的load执行
- 当loadable_classes为空的时候,查看loadable_classes是否为空,如果不为空则调用call_category_loads加载分类中的load方法,这样分类的load在类之后执行
4. initialize
initialize 只会在对应类的方法第一次被调用时,才会调用,initialize 方法是在 alloc 方法之前调用的,alloc 的调用导致了前者的执行。
initialize的调用栈中,直接调用其方法的其实是_class_initialize 这个C语言函数,在这个方法中,主要是向为初始化的类发送+initialize消息,不过会强制父类先发送。
与 load 不同,initialize 方法调用时,所有的类都已经加载到了内存中。
5. 使用场景
5.1 load
load一般是用来交换方法Method Swizzle,由于它是线程安全的,而且一定会调用且只会调用一次,通常在使用UrlRouter的时候注册类的时候也在load方法中注册
5.2 initialize
initialize方法主要用来对一些不方便在编译期初始化的对象进行赋值,或者说对一些静态常量进行初始化操作
总结:
一、调用方式
1、load是根据函数地址直接调用
2、initialize是通过objc_msgSend调用
二、调用时刻
1、load是runtime加载类、分类的时候调用(只会调用一次)
2、initialize是类第一次接收到消息的时候调用, 每一个类只会initialize一次(如果子类没有实现initialize方法, 会调用父类的initialize方法, 所以父类的initialize方法可能会调用多次)
3、load比initialize先调用
三、使用场景
load 和 initialize 方法本质都是做初始化的,只不过级别或者说针对的过程不一样。load 只会调用一次,在 main 方法调用之前做初始化,比如方法交换。initialize 方法针对的是 main 方法之后,而且是懒加载(类第一次接收到消息的时候调用),使用到时才初始化。鉴于 objc_msgSend 的机制,存在多次调用的可能,但是可以使用代码进行判断。
四、load和initialize的调用顺序
load调用顺序
调用顺序,需要同时满足四种规则
先调用类的load, 在调用分类的load
先编译的类, 优先调用load
调用子类的load之前, 会先调用父类的load(父类 -> 子类)
没有父子关系的类及分类之间,按Build Phases ->Complie Source 中的编译顺序
特点:
load在父类,子类,分类之间的调用不存在覆盖,只存在先后执行顺序
initialize调用顺序
initialize调用的优先级为 (父类分类 > 父类) > (子类分类 > 子类)
initialize在父类,子类,父分类,子分类之间的调用存在覆盖【(父类与子类)(父分类与子分类)本类与分类,两两存在覆盖关系,两组相互之间只会调用优先级最高的一个】,有优先级
特点:
优先级:先分类后本类【分类覆盖本类】
顺序是:先父类后子类【存在先后执行,不存在覆盖】
通过消息机制调用, 当子类没有initialize方法时, 会调用(父类分类>父类)的initialize方法, 所以(父类分类>父类)的initialize方法会调用多次