Category 是OC最常用的技术,可以较少侵入和不知道原类内部实现的情况下为原类添加方法 ,属性, 协议。正是因为这个原因,往往优于继承被采用。若不深究原理很有可能对原类造成毁灭的影响,尤其是在+load和 + initialize 方法的使用中。所以一直是面试的热点。
另外一个原因Category也是runtime很重要的技术体现:编译期加载Category数据,运行时写入原始类的数据中。
面试
(文末回答,也请评论你遇到的面试问题,共同进步。)
- Category的基本原理?
- Category 可以添加+load方法吗?执行顺序?
- Category 和Extension的区别 ?
- Category 为什么不能添加成员变量?Category 添加属性要注意什么?
原理
每一个分类只是作为原类的扩展,并不会形成一个新的类,不会创建新的类对象。在编译期会把Category中的方法、属性、协议 打包成一个category_t的结构体,在运行时会把每个category_t中的方法、属性、协议添加到原类的class_rw_t中。
如果添加的方法与原类的方法同名,并不会覆盖掉原始类的同名方法。而是把所有Category的方法、属性、协议数据,合并到一个大数组中
,后面参与编译的Category数据,会在数组的前面,然后将合并后的分类数据(方法、属性、协议),插入到类原来数据的前面。最后方法的执行按照OC的消息分发机制执行:
实例对象根据isa指针找到类对象,在类对象的方法缓存列表里根据方法名查找(缓存列表是一个hash表,根据方法名hash算法算出偏移量找到对应的方法实现),如果缓存中没有找到再去方法列表中查找(如果排序按照二分法查找如果没有排序遍历查找),找到之后根据方法名和苹果的hash算法算出偏移量插入到缓存列表里去。如果没有找到根据类对象的superClass指针去父类的类对象缓存列表和方法列表中查找。如果找到了缓存到子类的缓存列表里,如果继续找一直到基类还是找不到就会动态方法解析,若仍不处理然后就到了消息转发,如果没有实现转发就会跑出错误。
查找方法时会遍历如下的二维数组:
[
[method_t, method_t], //Category A
[method_t, method_t], //Category B
[method_t, method_t] //原始类
]
遍历数组的第一个元素Category A添加的方法列表时,若找到就会执行,所以这也是分类的方法先调用的原因。
category的数据结构category_t这个结构体中有实例方法列表,对象方法列表,协议列表,属性列表,唯独没有成员变量列表,这也是为什么在category中添加成员变量的时候会编译都不能通过。
屏幕快照 2021-01-04 下午7.49.00.png比较特殊的+load方法,分类的+load方法同样会像实例方法一样,运行时被插入到metaClass的方法列表里去。但是不同的是他不会被分类覆盖,每个分类和原类的+load方法都会在runtime加载类和分类时调用,而且重点是+load的执行并不是通过message_Send 去执行,而是通过load_method这个结构体里的IMP直接执行。
struct load_method {
Class class;
IMP loadMethod
}
但是如果是通过 [Class load] 手动调用的方式就会按照OC消息分发机制执行了,如果子类没有重写load方法会根据superClass去父类中查找并且先执行分类的+load方法。
用途:
- 为现有的类添加方法、属性、协议, 往往用于系统的控件和类。
- hook 类的某一个方法。
- 按功能或者业务拆分类的实现,比如UIApplication。
扩展:
OC 的方法调用机制
OC 中Class的结构
OC 中Class的内存管理(关联对象如何释放)
面试答案
Category的基本原理?
在编译期会把Category中的方法、属性、协议 打包成一个category_t的结构体,在运行时会把每个category_t中的方法、属性、协议添加到原类的class_rw_t中。
Category 可以添加+load方法吗?执行顺序?
当然可以添加,但是不会覆盖原类的+load方法。
1.先调用类的+load
按照编译先后顺序调用(先编译,先调用)
调用子类的+load之前会先调用父类的+load
2.再调用分类的+load
按照编译先后顺序调用(先编译,先调用)和继承没有关系。
Category 和 Extension的区别?
Category是运行时才把数据添加到类的结构中,而Extension中的方法原类中没有区别,编译器就加载到了类的结构中。
Category 为是否能添加成员变量?Category 添加属性要注意什么?
不可以添加成员变量,因为成员变量存储在class_ro_t这张表里,这张表是只读的不能在运行时修改,而且category_t这个结构体中没有储存成员变量的list。但是可以根据关联对象实现类似的效果。
可以添加属性到类的属性列表中, 但是没有实现set 和 get方法,只是声名。同样可以通过运行时关联对象,动态为类实现set和get方法。
objc_setAssociatedObject(obj, @selector(getter), value, OBJC_ASSOCIATION_RETAIN_NONATOMIC)
objc_getAssociatedObject(obj, @selector(getter))
@selector(getter)方法地址作为key把对应的属性value存储到一个全局的AssociatedManager中。
关联对象的存储流程
如有错误或者新的见解欢迎在评论区约谈...
网友评论