Category
-
Category
的本质:就是_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; //类属性列表结构体
method_list_t *methodsForMeta(bool isMeta) {
if (isMeta) return classMethods;
else return instanceMethods;
}
property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
protocol_list_t *protocolsForMeta(bool isMeta) {
if (isMeta) return nullptr;
else return protocols;
}
};
特别注意:_category_t 结构体 中不包含 _ivar_list_t(经Clang编译证实,类的申明中是有『const struct _ivar_list_t *ivars;』) 类型,也就是不包含『成员变量结构体』。这就是为什么类别不能添加成员变量的根本原因。
-
加载时机:是在运行时阶段动态(
dyld 的动态链接器
)加载的。-
dyld 的动态链接器
:用来加载所有的库和可执行文件。 - 1、通过Runtime加载某个类的所有Category数据
- 2、把所有Category的方法、属性、协议数据,合并到一个大数组中后面参与编译的Category数据,会在数组的前面
- 3、通过
memmove
把原有类的移到最后,然后通过memcpy
将合并后的分类数据(方法、属性、协议)放到初始位置。-->故类别的优先级高于原有类的方法属性协议等。
-
-
添加属性:
Category
中虽然可以添加属性,但是不会生成对应的成员变量,也不能生成getter
、setter
方法。// 1. 通过 key : value 的形式给对象 object 设置关联属性 void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy); // 2. 通过 key 获取关联的属性 object id objc_getAssociatedObject(id object, const void *key); // 3. 移除对象所关联的属性 void objc_removeAssociatedObjects(id object);
+ load
分析
- 源码分析:
//循环获取类
static void schedule_class_load(Class cls)
{
if (!cls) return;
ASSERT(cls->isRealized()); // _read_images should realize
if (cls->data()->flags & RW_LOADED) return;
// Ensure superclass-first ordering
schedule_class_load(cls->superclass); //重点:递归调用此类的superclass,在递归回溯阶段,会将最顶层的superclass添加到数组的最前面,依次往下,直到自身-此类
add_class_to_loadable_list(cls);
cls->setInfo(RW_LOADED);
}
void call_load_methods(void) { //代码片段
//循环遍历类及其分类
do {
// 1. Repeatedly call class +loads until there aren't any more
while (loadable_classes_used > 0) { //先循环遍历完所有类及其父类
call_class_loads(); //执行后会直接把 loadable_classes_used = 0;
}
// 2. Call category +loads ONCE
more_categories = call_category_loads(); //在遍历所有分类
// 3. Run more +loads if there are classes OR more untried categories
} while (loadable_classes_used > 0 || more_categories);
}
//故能得出以下的结论
- 整体结论:
- 本类的
+ load
调用顺序先于分类的+ load
。 -
+ load
方法除非主动调用,否则只会调用一次。 - 调用时机:
+load
方法在runtime
加载类、分类的时候调用。 - 如果子类没有实现
+ load
,则不会调用其父类的。
- 本类的
- 先调用类的
+ load
- 按照编译先后顺序调用(先编译,先调用)
- 调用子类的
+ load
之前会先调用父类的+ load
- 再调用分类的
+ load
- 调用完主类,再调用分类,按照编译顺序,依次调用;
- 注意:子类和父类的分类
+ load
调用顺序是按编译顺序决定的,所有使用时注意可能是:父类 -> 子类 -> 父类类别 -> 子类类别
,也可能是父类 -> 子类 -> 子类类别 -> 父类类别
。
+ initialize
分析
- 源码分析:
//初始化部分源码
void initializeNonMetaClass(Class cls)
{
ASSERT(!cls->isMetaClass());
Class supercls;
bool reallyInitialize = NO;
// Make sure super is done initializing BEFORE beginning to initialize cls.
// See note about deadlock above.
supercls = cls->superclass; //获取类的superclass
if (supercls && !supercls->isInitialized()) { //如果存在superclass 且没有初始化,则递归调用-初始化函数,回溯阶段就会先调用父类的initialize,在调用子类的initialize
initializeNonMetaClass(supercls); //递归方法本身
}
//调用
callInitialize(cls);
}
//调用initialize的方法实现(objc_msgSend发送消息)
void callInitialize(Class cls)
{
((void(*)(Class, SEL))objc_msgSend)(cls, @selector(initialize));
asm("");
}
- 整体结论:
-
+ initialize
方法会在类第一次接受到消息时调用; - 调用顺序:
- 先调用父类的
+ initialize
,在调用子类的+ initialize
; - 如果已经初始化则不会再调用,每个类只会初始化一次。
- 先调用父类的
- 调用次数:
- 如果子类没有实现
+ initialize
,会调用父类的+ initialize
(所以父类的+ initialize
可能会被调用多次)
- 如果子类没有实现
-
属性绑定/关联对象
- 运行时为一个已存在的类绑定成员变量,(使外部使用达到本身属性的效果)
- 关联对象不是存在被关联的对象本身内存中;而是存储在全局的统一的一个
AssociationsManager
中(详情见下图)-
AssociationsManager
:全局管理维护关联属性 AssociationsHashMap
AssociationsMap
ObjectAssociation
-
- 设置关联对象为nil,就相当于是移除关联对象
涉及方法
//注意以下三个方法的的key必须保持一致。
//赋值方法(类似于setter)
objc_setAssociatedObject(<#id _Nonnull object#>, <#const void * _Nonnull key#>, <#id _Nullable value#>, <#objc_AssociationPolicy policy#>);
//获值方法(类似于getter)
objc_getAssociatedObject(<#id _Nonnull object#>, <#const void * _Nonnull key#>)
//移除方法(一般由系统自行调用,不会主动调用)
objc_removeAssociatedObjects(<#id _Nonnull object#>)
-
objc_setAssociatedObject
的参数policy
说明:objc_AssociationPolicy
对应的修饰符 OBJC_ASSOCIATION_ASSIGN
assign OBJC_ASSOCIATION_RETAIN_NONATOMIC
nonatomic&strong OBJC_ASSOCIATION_COPY_NONATOMIC
nonatomic© OBJC_ASSOCIATION_RETAIN
atomic&retain OBJC_ASSOCIATION_COPY
atomic©
使用方式
-
用
@select(getter)
(最推荐)//例如:@selector(isOpenBlank), 而getter方法中,可用 _cmd 代替(因为实际会把隐藏把此参数传进来) objc_setAssociatedObject(self, @selector(isOpenBlank), @(isOpenBlank), OBJC_ASSOCIATION_ASSIGN); objc_getAssociatedObject(self, _cmd);
-
静态key--占用字节较少 (推荐)
static const char isOpenBlank_key_sm; objc_setAssociatedObject(self, &isOpenBlank_key_sm, @(isOpenBlank), OBJC_ASSOCIATION_ASSIGN); objc_getAssociatedObject(self, &isOpenBlank_key_sm);
-
静态key-存自身地址(不推荐)
//使用较麻烦, static const void *kname = &kname; objc_setAssociatedObject(self, &kname, @(isOpenBlank), OBJC_ASSOCIATION_ASSIGN); objc_getAssociatedObject(self, &kname);
-
直接字面量(不太推荐)
//改的时候不太方便,需要改两处,当然可以用宏定义(稍显麻烦且没必要) objc_setAssociatedObject(self, @"name_k", @(isOpenBlank), OBJC_ASSOCIATION_ASSIGN); objc_getAssociatedObject(self, @"name_k");
加static
:表示,作用于仅限于当前的文件,既extern const void * kname;
无法进行访问。
面试题
1、 Category和类扩展的区别
- Category扩展的(属性、方法、协议)等是在运行时动态的插入到对应类中
类扩展在编译的时候,他的数据就已经包含在类信息中。- Category无法扩展成员变量,类扩展可以。
- Category能实现方法,但类扩展只能申明。
2、 + load
的子类、父类及其分类为什么都能在编译时调用?
根据底层源码(如下的源码)分析,
+ load
方法不是通过消息机制调用的,而是通过函数指针找到其内存中的IMP来直接调用。
struct loadable_class { //结构体:Load方法特用
Class cls; // may be nil
IMP method;
};
Class cls = classes[i].cls;
load_method_t load_method = (load_method_t)classes[i].method; //获取方法实现
if (!cls) continue; //如果为空则结束本次循环
if (PrintLoading) {
_objc_inform("LOAD: +[%s load]\n", cls->nameForLogging());
}
(*load_method)(cls, @selector(load)); //通过函数指针直接调用
3、 + load
和 + initialize
的区别:
- 调用时机:
+ load
在运行时加载类和分类时调用,+ initialize
在类第一次发送消息是调用- 调用方式:
+ load
直接通过函数指针调用其实现,+ initialize
则遵守消息机制objc_msgSend
进行调用- 类别中实现:
+ load
的子类、父类及其分类都会调用,而+ initialize
则会覆盖本类的实现。- 调用顺序:
+ load
先类(父类->子类)再分类,+ initialize
也是先父类再子类,但如果分类中实现则会调用分类的。
1.调用方式:
1> load是根据函数地址直接调用
2> initialize是通过objc_msgSend调用
2.调用时刻:
1> load是runtime加载类、分类的时候调用(只会调用1次)
2> initialize是类第一次接收到消息的时候调用,每一个类只会initialize一次(父类的initialize方法可能会被调用多次)
3.load、initialize的调用顺序:
1.load
1> 先调用类的load
a) 先编译的类,优先调用load
b) 调用子类的load之前,会先调用父类的load
2> 再调用分类的load
a) 先编译的分类,优先调用load
2.initialize
1> 先初始化父类
2> 再初始化子类(可能最终调用的是父类的initialize方法)