Category 原理源码分析 load、initialize
本节将解释下一下问题:
1.Category的实现原理?
2.Category跟Extension的区别?
3.Category有load方法么?父类子类Category之间调用顺序是什么?
4.load、initialize方法的区别,调用顺序是什么?以及出现继承他们的调用顺序是什么?
5.Categoryn能添加成员变量么?如果可以,如何添加?
1、Category实现原理
创建一个Category首先可以将OC转换成C语言编译文件查看一下,当前目录下(注意方法要实现)
Category转换成C语言编译xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc NSObject+Test.m -o NSObject+Test.cpp
原理: 从编译文件我们可以看出,Category编译之后底层会转换成一个_category_t的结构体,内部包含着Category的对象方法,类方法,属性,协议信息。在运行时期,会经过runtime合并到类信息中(类方法放置元类对象中)。 PS: 类方法存储方式,可以看我之前的文章Class本质。
下面我们就来搞下:runtime是如何将Category中的信息,合并到对应类信息中呢?
下载runtime的源码:objc-os.mm这个文件是入口文件,内部void_objc_init(void)方法是初始化方法
runtime初始化内部调用map_images函数 (读取镜像/模块),进入函数,在进入map_images_nolock函数实现,再进入_read_images函数,(从函数命也能够看出来 到了读取模块的地方了)
开始读取Category在进入重新布局class方法,内部调用了attachCategories方法,追加类别
开始追加Category 解析Category方法内部实现然后进入attachLists函数调用。
Category追加到当前类2.Category跟Extension的区别?
class extension 是在编译前就储存在类信息中了,而Category是在运行时才会被载入类信息中的
Category是在运行时才会被合并到类信息中去的。
3.Category有load方法么?父类子类Category之间调用顺序是什么?
load方法特点:是在runtime装载类/Category的时候调用,不管你是否用的该类、Category都会调用load方法,runtime是采用的是直接找到对应类的load函数地址调用,而并非是用objc_msgsend()来调用。
调用顺序是先调用类的load方法,再调用Category的load方法,如果类中存在继承关系,先调用super class的load方法在调用子类的load方法。
源码来看:(objc-os.mm-->load_images-->call_load_methods())
load方法加载顺序 真正调用class的load方法从图中可以看出,先进行循环class的load方法执行,在进行Category方法的执行。
那么继承关系又是怎么调用的呢?同样我们也可以通过源码来分析。
源码路径:(objc-os.mm-->load_images-->prepare_load_methods()-->schedule_class_load)
预加载class列表可以看到apple用递归调用,然后每次传入自身父类,生成的数组是@[@(super class),@(son class)]这种类型,而且我们从上上图 我们也可以看出,循环数组是从i=0开始的,也就意味着父类的调用顺序会比子类要早。
总结:load方法的调用顺序是:
1.先加载class中的load方法。
> 存在继承关系的类,会先进行加载父类,在加载子类,比如person.h、 son.h(son继承person)会先加载person的load方法
>不存在继承关系的类,会根据编译顺序来加载load方法。比如dog.h 、cat.h两个类的编译顺序谁在先先调用。(编译顺序工程>Tagets>build phases >complie sources 可以看到编译顺序)
2.再进行加载Category中的load方法
>会根据编译顺序来加载load方法。比如NSObject +dog.h 、NSObject +cat.h两个分类的编译顺序谁在先先调用。(编译顺序工程>Tagets>build phases >complie sources 可以看到编译顺序)
4.load、initialize方法的区别,调用顺序是什么?以及出现继承他们的调用顺序是什么?
initialze 方法是在对象第一次接受消息的时候调用,采用的是objc_msgSend()消息转发机制,
而且每个类只调用一次,父类优先调用,父类存在列表,并且实现的该方法,根据消息转发机制,类别调用顺序优于类对象,所以会先调用父类类别。
比如现在有两个类,person 、student 继承关系,还有两个类别,person+eat 、student+eat、四个文件的都实现了initialize方法,当发生[student alloc]的时候,也就是对象第一次接受纤细的时候,会先从父类中寻找,superclass的方法列表中,分类优于对象,所以执行的是person+eat 中的initialize方法,进而在调用student中的initialize方法,相同道理,会调用student+eat中的initialze方法。
load、initialze区别在于实现方式不同,load是直接找到load函数地址来调用,而initialze是采用消息转发机制来进行调用。因为initialze是通过objc_msgSend进行调用的,所以会有以下特点:如果子类没有实现,会调用父类的initialze,所以父类的initialze可能被调用多次,如果分类实现了initialze,就会覆盖类本身的initialze调用。
总结:load、initialze的区别总结?
1》调用方式:load是根据函数地址直接到调用。initialze是通过objc_msgSend()调用。
2》调用时刻:load是runtime加载类,Category的时候调用,只会调用一次,而initialze是在类第一次接受到消息的时候调用,每个类只会initialze一次,(父类的initialze可能会被多次调用)
3》load调用顺序:
a> load 先调用类的load,先编译的类优先调用load方法,调用子类的load之前,会调用父类的load
b> 再调用分类的load
4》initialze调用顺序:
a> 先初始化父类
b> 再初始化子类,可能最终调用的父类的initialze方法。
5.Categoryn能添加成员变量么?如果可以,如何添加?
不能直接添加成员变量,因为Category中添加属性不会自动生成 _成员变量 、setter、getter方法的实现,只会生成setter、getter方法的声明。但是可以用运行时间接关联对象,完成添加属性。
运用runtime 关联对象的api,可以达到属性关联的目的。这里并不是说类别就可以添加属性了,而是通过runtime达到跟添加属性一样的目的而已。
关联属性objc_setAssociatedObject(<#id _Nonnull object#>, <#const void * _Nonnull key#>, <#id _Nullable value#>, <#objc_AssociationPolicy policy#>)
三个参数,第一个是当前对象,第二个是一个常亮指针。这里面用@selector(age) 返回的是一个唯一的age的函数指针地址,可以作为常亮。
第三个
第三个参数根据你参数定义的关键字选择对应的枚举值即可