一、Category 本质
我们知道,当调用一个对象的方法时,通过对象的 isa 指针找到类对象,然后在类对象的方法列表中查找方法,如果没有找到,就通过类对象的 superclass 指针找到父类对象,接着去寻找该方法。
分类中的对象方法依然是存储在类对象中的方法列表中,同对象方法在同一个地方,那么调用步骤也同调用对象方法一样。如果是类方法的话,也同样是存储在元类对象中。
1.Category 的底层结构
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);
};
该结构体中包含:
- 对象方法
- 类方法
- 协议
- 属性
但不包含成员变量,因此分类中不能添加成员变量,分类中添加的属性并不会帮助我们自动生成成员变量以及 get 和 set 方法,需要我们自己去实现。
_method_list_t类型的结构体
属性列表结构体
runtime初始化函数源码如下
接着我们来到 &map_images读取模块(images这里代表模块),来到map_images_nolock函数中找到_read_images函数,在_read_images函数中我们找到分类相关代码
分类的实现原理是将category中的方法,属性,协议数据放在category_t结构体中,然后将结构体内的方法列表拷贝到类对象的方法列表中。
3.load 和 initialize
load
介绍
+load方法会在runtime加载类、分类时调用。每个类、分类的+load,在程序运行过程中只调用一次。
调用顺序:
- 先调用类的+load
- 按照编译先后顺序调用(先编译,先调用)
- 调用子类的+load之前会先调用父类的+load
- 再调用分类的+load
- 按照编译先后顺序调用(先编译,先调用)
+load方法是根据方法地址直接调用,并不是经过objc_msgSend函数调用
源码
load方法会在程序启动就会调用,当装载类信息的时候就会调用。 调用顺序看一下源代码。
通过源码我们发现是优先调用类的load方法,之后调用分类的load方法。
我们通过代码验证一下:
我们添加Student继承Presen类,并添加Student+Test分类,分别重写只+load方法,其他什么都不做通过打印发现
确实是优先调用类的load方法之后调用分类的load方法,不过调用类的load方法之前会保证其父类已经调用过load方法。
load 方法调用源码如下:
直接拿到load方法的内存地址直接调用方法,而不是通过消息发送机制调用。
我们可以看到分类中也是通过直接拿到load方法的地址进行调用。
initialize
介绍
+initialize方法会在类第一次接收到消息时调用
调用顺序
- 先调用父类的+initialize,再调用子类的+initialize(递归调用)
- (先初始化父类,再初始化子类,每个类只会初始化1次)
- 分类实现+initialize,会覆盖本身的+initialize方法(因为消息发送机制)
- 子类不实现+initialize,由于消息转发机制,会调用到父类的+initialize方法,多个不实现+initialize方法的子类进行初始化,就会多次调用父类的+initialize,但这不代表父类进行了多次初始化,父类的初始化只会进行一次,但+initialize方法可能会调用多次。
源码
我们为Preson、Student 、Student+Test 添加initialize方法。
我们知道当类第一次接收到消息时,就会调用initialize,相当于第一次使用类的时候就会调用initialize方法。调用子类的initialize之前,会先保证调用父类的initialize方法。如果之前已经调用过initialize,就不会再调用initialize方法了。当分类重写initialize方法时会覆盖原来类的initialize方法。首先我们来看一下initialize的源码。
保证 initialize 只调用一次的源码:
通过锁来保证并发安全,且通过 cls->isInitialized() 保证只执行一次
在 _class_initialize 方法内部通过递归的方式,如果父类没有初始化,那么先调用父类的初始化方法,再调用子类的初始化方法
最终通过 objc_megSend() 调用 initialized() 方法,initialize是通过消息发送机制调用的,消息发送机制通过isa指针找到对应的方法与实现,消息发送机制也使得如果分类也实现了 initialize 方法,那么会覆盖类原来的 initialized 方法
注意点:由于 initialized 是通过 objc_megSend 调用的,遵循消息转发机制,所以如果多个子类均没有实现 initialized 方法,而父类实现了 initialized 方法,那么子类进行初始化时,没有 initialized 则会将 initialized 转发到父类上,调用父类的 initialized 方法,多个子类的初始化会导致父类调用多次 initialized 方法。所以要注意 initialized 不一定只会调用一次
load initialize 异同
1.调用方式
- 1> load是根据函数地址直接调用
- 2> initialize是通过objc_msgSend调用
2.调用时刻
- 1> load是runtime加载类、分类的时候调用(只会调用1次)
- 2> initialize是类第一次接收到消息的时候调用,每一个类只会initialize一次(父类的initialize方法可能会被调用多次)
3.调用顺序
load
- 1> 先调用类的load
- a) 先编译的类,优先调用load
- b) 调用子类的load之前,会先调用父类的load
- 2> 再调用分类的load
- a) 先编译的分类,优先调用load
initialize
- 1> 先初始化父类
- 2> 再初始化子类(可能最终调用的是父类的initialize方法)
二、面试题
1.Category的实现原理,以及Category为什么只能加方法不能加属性。
答:分类的实现原理是将category中的方法,属性,协议数据放在category_t结构体中,然后将结构体内的方法列表拷贝到类对象的方法列表中。
Category可以添加属性,但是并不会自动生成成员变量及set/get方法。因为category_t结构体中并不存在成员变量。通过之前对对象的分析我们知道成员变量是存放在实例对象中的,并且编译的那一刻就已经决定好了。而分类是在运行时才去加载的。那么我们就无法再程序运行时将分类的成员变量中添加到实例对象的结构体中。因此分类中不可以添加成员变量。但是可以间接实现Category有成员变量的效果(关联对象)
2.Category中有load方法吗?load方法是什么时候调用的?load 方法能继承吗?
答:Category中有load方法,load方法在程序启动装载类信息的时候就会调用。load方法可以继承。调用子类的load方法之前,会先调用父类的load方法,但是一般情况下不会主动去调用load方法,都是让系统自动调用
3.load、initialize的区别,以及它们在category重写的时候的调用的次序。
答:区别在于调用方式和调用时刻
调用方式:load是根据函数地址直接调用,initialize是通过objc_msgSend调用
调用时刻:load是runtime加载类、分类的时候调用(只会调用1次),initialize是类第一次接收到消息的时候调用,每一个类只会initialize一次(父类的initialize方法可能会被调用多次)
调用顺序:先调用类的load方法,先编译那个类,就先调用load。在调用load之前会先调用父类的load方法。分类中load方法不会覆盖本类的load方法,先编译的分类优先调用load方法。initialize先初始化父类,之后再初始化子类。如果子类没有实现+initialize,会调用父类的+initialize(所以父类的+initialize可能会被调用多次),如果分类实现了+initialize,就覆盖类本身的+initialize调用。
4.Category和Class Extension的区别是什么?
答:Class Extension在编译的时候,它的数据就已经包含在类信息中
Category是在运行时,才会将数据合并到类信息中