Category
- Category的信息需要在程序运行时,通过Runtime,动态加载到对应类中
- 把所有Category的方法、属性、协议数据,合并到一个大数组中,后面参与编译的Category数据,会在数组的前面
- 将合并后的分类数据(方法、属性、协议),插入到类原来数据的前面
- Category的结构
struct category_t {
const char *name;
classref_t cls;
WrappedPtr<method_list_t, PtrauthStrip> instanceMethods;
WrappedPtr<method_list_t, PtrauthStrip> 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;
// ...
};
Category的加载处理过程
- 通过Runtime加载某个类的所有Category数据
- 把所有的Category的方法,属性,协议数据,合并到一个大数组中
- 后面参与编译的Category数据,会在数组的前面
- 将合并后的分类数据(方法,属性,协议),插入到类原来数据的前面
- 源码解读顺序
+load方法
+load
方法会在runtime
加载类,分类时调用- 每个类,分类的
+load
,在程序运行过程中只调用一次 - 调用顺序
- 先调用类的+load
- 按照编译先后顺序调用(先编译,先调用)
- 调用子类的+load之前会先调用父类的+load
- 再调用分类的+load
- 按照编译先后顺序调用(先编译,先调用)
- 先调用类的+load
objc4
源码解读过程
+initialize方法
+initialize
方法会在类第一次接收消息时调用- 调用顺序
- 先调用父类的
+initialize
,再调用子类的+initialize
- (先初始化父类,再初始化子类,每个类只会初始化1次)
- 先调用父类的
+initialize
和+load
的很大区别是,+initialize
是通过objc_msgSend
进行调用的,所以有以下特点- 如果子类没有实现
+initialize
,会调用父类的+initialize
(所以父类的+initialize
可能会被调用多次) - 如果分类实现了
+initialize
,就覆盖类本身的+initialize
调用
- 如果子类没有实现
objc4
源码解读过程
+load vs +initialize 异同点和使用场景
-
区别点 | 区别 | +load | +initialize | |:--:|:--:|:--:| | 调用时机 | 在runtime加载类、分类时调用,在main函数之前调用(只会调用一次) | 在类第一次接收到消息时调用,惰性调用,每个类只会initialize一次(父类的initialize方法可能会被调用多次) | | 调用方式 | 根据方法地址调用 | objc_msgSend | | 调用顺序 |
1.父类 -> 类 -> 子类 -> 分类
2.按照编译先后顺序调用(先编译,先调用) |1.分类 -> 父类 -> 类 -> 子类
2.如果子类没有实现+initialize,会调用父类的+initialize(所以父类的+initialize可能会被调用多次(只初始化化一次)) | | 方法是否会覆盖调用 | 不会 | 会 | | 调用次数 | 一次 | 可能多次(父类实现里被调用多次) | -
相同点
- 1.
load
和initialize
会被自动调用,不能手动调用它们。 - 2.子类实现了
load
和initialize
的话,会隐式调用父类的load
和initialize
方法。 - 3.
load
和initialize
方法内部使用了锁,因此它们是线程安全的。
- 1.
-
使用场景
+load
一般是用来交换方法Method Swizzle
,由于它是线程安全的,而且一定会调用且只会调用一次,通常在使用UrlRouter的时候注册类的时候也在+load
方法中注册。+initialize
方法主要用来对一些不方便在编译期初始化的对象进行赋值,或者说对一些静态常量进行初始化操作。
-
注意点
+initialize
方法会被自动继承,所以,+initialize
的出错率要比+load
更大一些。- 如何避免
- 判断类
+ (void)initialize{ if (self == [MyClass class]) { .... } }
- dispatch_once,一次性代码
+ (void)initialize { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ ... }); }
+load
在main函数之前执行,如果+load
函数里面任务过重,会影响应用的启动速度
Category关联对象
- 默认情况下,因为分类底层结构的限制,不能添加成员变量到分类中。但可以通过关联对象来间接实现
关联对象提供了以下API
- 添加关联对象
void objc_setAssociatedObject(id object, const void * key,
id value,
objc_AssociationPolicy policy
- 获得关联对象
id objc_getAssociatedObject(id object, const void * key)
- 移除所有的关联对象
void objc_removeAssociatedObjects(id object)
- 实际使用
- (void)setName:(NSString *)name {
objc_setAssociatedObject(self, @selector(name), name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (NSString *)name {
// 隐式参数 _cmd == @selector(name)
return objc_getAssociatedObject(self, _cmd);
}
key的常见用法
static void *MyKey = &MyKey;
objc_setAssociatedObject(obj, MyKey, value, OBJC_ASSOCIATION_RETAIN_NONATOMIC)
objc_getAssociatedObject(obj, MyKey)
static char MyKey;
objc_setAssociatedObject(obj, &MyKey, value, OBJC_ASSOCIATION_RETAIN_NONATOMIC)
objc_getAssociatedObject(obj, &MyKey)
使用属性名作为key
objc_setAssociatedObject(obj, @"property", value, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
objc_getAssociatedObject(obj, @"property");
使用get方法的@selecor作为key
objc_setAssociatedObject(obj, @selector(getter), value, OBJC_ASSOCIATION_RETAIN_NONATOMIC)
objc_getAssociatedObject(obj, @selector(getter))
objc_AssociationPolicy
objc_AssociationPolicy | 对应的修饰符 |
---|---|
OBJC_ASSOCIATION_ASSIGN | assign |
OBJC_ASSOCIATION_RETAIN_NONATOMIC | strong, nonatomic |
OBJC_ASSOCIATION_COPY_NONATOMIC | copy, nonatomic |
OBJC_ASSOCIATION_RETAIN | strong, atomic |
OBJC_ASSOCIATION_COPY | copy, atomic |
关联对象的原理
-
实现关联对象技术的核心对象有
- AssociationsManager
- AssociationsHashMap
- ObjectAssociationMap
- ObjcAssociation
-
objc4源码解读:objc-references.mm
-
设置关联的关键函数
- 关联对象并不是存储在被关联对象本身内存中
- 关联对象存储在全局的统一的一个
AssociationsManager
中 - 设置关联对象为nil,就相当于是移除关联对象
void objc_setAssociatedObject(id object, const void * key, id value, objc_AssociationPolicy policy)
面试题
-
- Category的使用场合是什么?
-
- Category的实现原理
- Category编译之后的底层结构是struct category_t,里面存储着分类的对象方法、类方法、属性、协议信息
- 在程序运行的时候,runtime会将Category的数据,合并到类信息中(类对象、元类对象中)
-
- Category和Class Extension的区别是什么?
- Class Extension在编译的时候,它的数据就已经包含在类信息中
- Category是在运行时,才会将数据合并到类信息中
-
- Category中有load方法吗?load方法是什么时候调用的?load 方法能继承吗?
- 有load方法
- load方法在runtime加载类、分类的时候调用
- load方法可以继承,但是一般情况下不会主动去调用load方法,都是让系统自动调用
-
- Category能否添加成员变量?如果可以,如何给Category添加成员变量?
- 不能直接Category添加成员变量
- 可以间接实现,通过Runtime关联对象的方式
-
- 怎么保证在+load方法里交换方法总是在别人交换完之后执行的(交换方法的实现总是以我们交换的为准)