Category分类原理
分类的结构
每个分类文件编译后产生的结构体 _category_t
都是独立的,以下为分类的结构体
1 | struct _category_t |
instance_methods | class_methods | protocols | properties |
---|---|---|---|
实例方法 | 类方法 | 遵守的协议 | 属性列表 |
分类的合并
分类中的实例方法和类方法会通过运行时机制 Runtime 分别合并到类对象和元类对象中
打开 obje-os.mm 源码进行探究
找寻的顺序依次为 _objc_init()
-> map_images()
-> map_images_nolock()
-> _read_images()
-> remethodizeClass()
-> attachCategories()
1 | static void |
- old_method 为原类方法所占用的内存,new_malloc 为新申请的内存区域
- 然后 old_method 复制到所申请新的内存区域的最末端
- 最后添加分类的方法 new_method ,从整个方法的内存从首端开始覆盖
- 编译顺序越靠后的分类方法的优先级越高
所以在调用实例对象的方法时,会优先调用分类中的实例对象方法,具体的分类顺序需要在 XCode 中设置分类文件的编译顺序,在 Project-BuildPhases-Compile Sources 中进行设置
分类中的load方法和initialize方法
load方法
load()
方法会在runtime加载 类,分类 时调用,无论这些 类 或 分类 是否包含于 main() 函数的文件中或包含于其他的类文件中,它们各自的load()
方法都会在内存中被执行,且 load()
方法不会通过消息的发送机制进行调用。
load方法的调用顺序
分类中的load方法
和分类中的实例方法 相反 ,load()
方法的内存位置并不会被后编译的 分类 中的 load()
所覆盖,只与 分类 文件的编译顺序有关,编译越靠前的分类越优先调用。
继承中的load方法
先查找到其源码,在 objc-runtime-new.mm
中的prepare_load_methods()
方法中调用了schedule_class_load()
方法。
1 | static void schedule_class_load(Class cls) |
其中有一个递归的调用,传入的参数为cls->superclass
,且注释说明其用意是确保父类最优先调用。
initialize方法
仅在类第一次接收消息的时候调用 initialize()
方法,如果分类中实现了 initialize()
,会直接覆盖本身的initialize()
方法。
打开源码,在 objc-runtime-new.mm
文件中
1 | if (initialize && !cls->isInitialized()) { |
其中对类是否初始化进行了判断,如果没有初始化则进入 _class_initialize()
方法。
于是找到方法所在的文件 objc-initialize.mm
1 | void _class_initialize(Class cls) |
if (supercls && !supercls->isInitialized())
会对传入的父类先进行 initialize 的判断,然后再对子类进行 initialize。
所以如果是继承关系的类,在子类第一次接收消息的时候会先调用父类的 initialize()
方法,然后再调用子类的 initialize()
方法。
总结
- 调用方式
load() 根据函数地址直接调用
initialize() 是通过 objc_msgSend() 调用
调用时刻
load() 是运行时在加载类,分类的时候调用(只会调用1次)
initialize() 是类第一次接收到消息的时候调用,每一个类只会使用 initialize() 初始化一次,但父类的 initialize() 可能会被调用多次
调用顺序
load
- 先调用类的 load()
- 先编译的类,优先调用 load()
- 调用子类的 load() 之前,会先调用父类的 load()
- 再调用分类的 load()
先编译的分类,优先调用 load()
initialize
- 先初始化父类
- 再初始化子类(可能最终调用的是父类的initialize方法)