Category面试题(类和类别中普通方法,+load方法 ,+
前言
可怜身上衣正单,心忧炭贱愿天寒
----有感于各种强制解除劳动关系却仍有人为血汗工厂打工
要了解这些方法的调用顺序,首先要了解category的底层结构和加载处理过程。
category的底层结构和加载处理过程
下图是category的底层结构 category的底层结构加载处理过程:
通过Runtime加载某个类的所有Category数据
把所有Category的方法、属性、协议数据,合并到一个大数组中
后面参与编译的Category数据,会在数组的前面
将合并后的分类数据(方法、属性、协议),插入到类原来数据的前面
然后category的普通方法是走的msg_Send方法,即通过isa指针和superclass指针查找方法列表,找到就直接执行,不再继续找。
普通方法的调用顺序
我们先说结论:当父类和子类和子类的类别同时实现某个方法test,当调用test方法时,他会走msg_send()消息转发机制,只调用最先找到的那个方法,查找顺序为分类->类->父类,如果两个分类实现同一个方法,那么先执行后编译的那个,也就是编译文件列表里下面那个;下面我们证明一下。
我们首先创建一个RMAnimal 类,在里边声明一个+(void)test方法,然后创建RMPerson类继承自RMAnimal,再创建RMPerson+weight,RMPerson+Height分类,同时实现+(void)test
@interface RMAnimal : NSObject
+ (void)test;
@end
@implementation RMAnimal
+ (void)test {
NSLog(@"test ============RMAnimal");
}
@end
@implementation RMPerson
+ (void)test {
NSLog(@"test ============RMPerson");
}
@end
@implementation RMPerson (weight)
+ (void)test {
NSLog(@"test ============RMPerson (weight)");
}
@end
@implementation RMPerson (Height)
+ (void)test {
NSLog(@"test ============RMPerson (Height)");
}
@end
执行代码 [RMPerson test];
[RMPerson test];
最终打印结果是
test ============RMPerson (weight)
此时的编译顺序如下:
类别的编译顺序
可以看到两个类别的编译顺序为RMPerson+Height,RMPerson+weight,只是执行了RMPerson+weight的test方法,这说明类别里的方法后编译的先被找到。那么为什么呢,因为在底层category最后将类别的方法,属性和协议与原来的类结合到一起的时候(执行attachCategories方法如下),取数组元素是执行一个while循环 倒序取的,所以在后边的方法,会被放到前边。
attachCategories(Class cls, category_list *cats, bool flush_caches)
{
if (!cats) return;
if (PrintReplacedMethods) printReplacements(cls, cats);
bool isMeta = cls->isMetaClass();
// fixme rearrange to remove these intermediate allocations
method_list_t **mlists = (method_list_t **)
malloc(cats->count * sizeof(*mlists));
property_list_t **proplists = (property_list_t **)
malloc(cats->count * sizeof(*proplists));
protocol_list_t **protolists = (protocol_list_t **)
malloc(cats->count * sizeof(*protolists));
// Count backwards through cats to get newest categories first
int mcount = 0;
int propcount = 0;
int protocount = 0;
int i = cats->count;
bool fromBundle = NO;
while (i--) {
auto& entry = cats->list[i];
method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
if (mlist) {
mlists[mcount++] = mlist;
fromBundle |= entry.hi->isBundle();
}
property_list_t *proplist =
entry.cat->propertiesForMeta(isMeta, entry.hi);
if (proplist) {
proplists[propcount++] = proplist;
}
protocol_list_t *protolist = entry.cat->protocols;
if (protolist) {
protolists[protocount++] = protolist;
}
}
auto rw = cls->data();
prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
rw->methods.attachLists(mlists, mcount);
free(mlists);
if (flush_caches && mcount > 0) flushCaches(cls);
rw->properties.attachLists(proplists, propcount);
free(proplists);
rw->protocols.attachLists(protolists, protocount);
free(protolists);
}
+ (void)load方法的执行顺序
1、如果所有的父类和子类以及父类的类别和子类类别都实现了load方法,所有的load方法都会被执行,且执行顺序为,父类->子类->所有的类别,当子类里有平级关系(比如class Man 和class Woman均继承自于Person),则按照Compile Sources 出现的顺序执行(上边的先执行),所有的类别有不分父类类别和子类类别,均按照Compile Sources 出现的顺序执行。
2、当子类未实现 load 方法时,在加载该子类时,不会去调用其父类 load 方法。
参考链接:iOS load方法调用机制解析
+ (void)initialize方法的执行顺序
1、initialize 方法是在这个类第一次被使用到时才调用,具体为第一次调用该类的相关方法;
2、父类的 initialize 先执行;
3、如果子类没有实现 initialize,则会调用父类的initialize;
4、如果子类实现了 initialize,那么就直接执行子类的 initialize
5、理论上只会调用一次,但是因为采用了 objc_msgSend 来调用,所以如果子类没有实现 initialize,那么就会多次调用父类的 initialize,可以通过添加 if (self == [ClassName self]) 来进行判断;
6、不像 load 方法,会区分类和分类保存在两个数组中分别执行, 分类的 initialize 方法会覆盖原来类的 initialize,且遵循分类的编译顺序原则,最靠后的分类最终替换掉之前的 initialize 方法;