OC基础-Category分类

240 阅读6分钟
本文已参与新人创作礼活动,一起开启掘金创作之路。

一.Category底层结构

image.png

1.通过Runtime加载某个类的所有Category数据

2.把所有Category的方法、属性、协议数据,合并到一个大数组中

后面参与编译的Category数据,会在数组的前面

3.将合并后的分类数据(方法、属性、协议),插入到类原来数据的前面

【对象方法列表合并到类方法列表中 类方法列表合并到元类方法列表】

屏幕快照 2019-09-11 下午6.46.47.png

面试时实现原理详细说说: Category是被添加到class_rw_t 的对应结构里, 实际上是 Category_t 的结构体,在运行时,新添加的方法,都被以倒序插入到原有方法列表的最前面,所以不同的 Category,添加了同一个方法,执行的实际上是最后一个。

这个方法列表就是methods,一个二维数组。

Category 在刚刚编译完的时候,和原来的类是分开的,只有在程序运行起来后,通过 Runtime ,Category 和原来的类才会合并到一起。

如何合并:

mememove,memcpy:这俩方法是位移、复制,简单理解就是原有的方法移动到最后,根根新开辟的控件,

把前面的位置留给分类,然后分类中的方法,按照倒序依次插入,可以得出的结论就就是,越晚参与编译的分类,里面的方法才是生效的那个。

什么时候合并:

  1. 程序启动后,通过编译之后,Runtime 会进行初始化,调用 _objc_init。
  2. 然后会 map_images。
  3. 接下来调用 map_images_nolock。
  4. 再然后就是 read_images,这个方法会读取所有的类的相关信息。
  5. 最后是调用 reMethodizeClass:,这个方法是重新方法化的意思。
  6. 在 reMethodizeClass: 方法内部会调用 attachCategories: ,这个方法会传入 Class 和 Category ,会将方法列表,协议列表等与原有的类合并。最后加入到 class_rw_t 结构体

Category和Extension的区别是什么?

 Extension在编译的时候,它的数据就已经包含在类信息中

 Category是在运行时,才会将数据合并到类信息中 

二、你用分类做了哪些事?

  • 声明私有方法 - 把分类的头文件放到对应数组类的.m中,就满足了私有方法的一个声明和使用,又对外不暴露
  • 分解体积庞大的类文件
  • 把Framework的私有方法公开

三、分类中都可以添加哪些内容?

  1. 实例方法
  2. 类方法
  3. 协议
  4. 实例属性(实例属性只声明了对应的get和set方法,但没有在分类中添加上相应的实例变量)
  5. 想要添加实例变量,可以通过关联对象来为分类添加实例变量

四.分类添加成员变量

1.默认情况下,因为分类底层结构的限制,不能添加成员变量到分类中。但可以通过关联对象来间接实现
2.关联对象提供了以下API

(1)添加关联对象

void objc_setAssociatedObject(id object, const void * key,id value, objc_AssociationPolicy policy)

(2)获得关联对象

id objc_getAssociatedObject(id object, const void * key)

(3)移除所有的关联对象

void objc_removeAssociatedObjects(id object)

2.key的常见用法

  1. static void *MyKey = &MyKey;

objc_setAssociatedObject(obj, MyKey, value, OBJC_ASSOCIATION_RETAIN_NONATOMIC)

objc_getAssociatedObject(obj, MyKey)

  1. static char MyKey;

objc_setAssociatedObject(obj, &MyKey, value, OBJC_ASSOCIATION_RETAIN_NONATOMIC)

objc_getAssociatedObject(obj, &MyKey)

  1. 使用属性名作为key

objc_setAssociatedObject(obj, @"property", value, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

objc_getAssociatedObject(obj, @"property");

  1. 使用get方法的@selecor作为key

objc_setAssociatedObject(obj, @selector(getter), value, OBJC_ASSOCIATION_RETAIN_NONATOMIC)

objc_getAssociatedObject(obj, @selector(getter))

截屏2022-05-24 下午10.09.21.png 屏幕快照 2019-09-11 下午7.01.11.png

3.关联对象的原理
  • 关联对象是由系统提供的,由AssociationsManager管理,这个类有一个成员变量叫做AssociationsHashMap,我们创建的每一对象的关联对象,都存储在AssociationsHashMap这个容器中。(AssociationsHashMap:通过Hash来实现的一个Map或者说是字典)
  • 所有对象的关联内容都在同一个全局容器中。(为不同的类添加的关联对象的值,都放在了一个全局容器当中)
实现关联对象技术的核心对象有
  • AssociationsManager

  • AssociationsHashMap

  • ObjectAssociationMap

  • ObjcAssociation

屏幕快照 2019-09-11 下午7.02.17.png

屏幕快照 2019-09-11 下午7.02.59.png

五.+load方法

1.+load方法会在runtime加载类、分类时调用

2.每个类、分类的+load,在程序运行过程中只调用一次

3.调用顺序

(1)先调用类的+load

按照编译先后顺序调用(先编译,先调用)

调用子类的+load之前会先调用父类的+load

(2)再调用分类的+load

按照编译先后顺序调用(先编译,先调用)

综合上述机制,完整的调用顺序为:

  1. 程序启动,类被加载到内存。

  2. 父类的load方法被调用。

  3. 子类的load方法被调用。

  4. 分类的方法被添加到类的方法列表中(按编译顺序)。

  5. 分类的load方法被调用(按编译顺序)。

  6. 后续运行时:当调用类的方法时,优先从方法列表的头部查找(分类方法可能覆盖原始类方法)。

多个分类的+load顺序

  • 规则:分类的+load按编译顺序调用,与文件名或代码位置无关。
  • 编译顺序调整:在 Xcode 的Build Phases → Compile Sources中调整文件顺序,后添加的文件优先编译(列表底部的文件先编译)。

六.+initialize方法

1.+initialize方法会在类第一次接收到消息时调用

2.调用顺序

先调用父类的+initialize,再调用子类的+initialize

(先初始化父类,再初始化子类,每个类只会初始化1次)

3.+initialize和+load的很大区别是,+initialize是通过objc_msgSend进行调用的,所以有以下特点

(1)如果子类没有实现+initialize,会调用父类的+initialize(所以父类的+initialize可能会被调用多次)

(2)如果分类实现了+i起tialize,就覆盖类本身的+initialize调用

load、initialize方法的区别什么?

1.调用方式

1> load是根据函数地址直接调用

2> initialize是通过objc_msgSend调用

2.调用时刻

1> load是runtime加载类、分类的时候调用(只会调用1次)

2> initialize是类第一次接收到消息的时候调用,每一个类只会initialize一次(父类的initialize方法可能会被调用多次)

  1. load、initialize的调用顺序?

1.load

1> 先调用类的load

a) 先编译的类,优先调用load

b) 调用子类的load之前,会先调用父类的load

2> 再调用分类的load

a) 先编译的分类,优先调用load

2.initialize

1> 先初始化父类

2> 再初始化子类(可能最终调用的是父类的initialize方法)