注册

iOS Cateogry的深入理解&&load方法调用&&分类重写方法的调用顺序(一)

首先先看几个面试问题

  • Cateogry里面有load方法么? load方法什么时候调用?load方法有继承么?

1. 新建一个项目,并添加TCPerson类,并给TCPerson添加两个分类

a4cb59b29a429e45023364dde5bae296.png

2.新建一个TCStudent类继承自TCPerson,并且给TCStudent也添加两个分类

fa9649732abf2fe321e6a2bb7b343826.png

Cateogry里面有load方法么?

  • 答:分类里面肯定有load

#import "TCPerson.h"

@implementation TCPerson
+ (void)load{

}
@end
#import "TCPerson+TCtest1.h"

@implementation TCPerson (TCtest1)
+ (void)load{

}
@end
#import "TCPerson+TCTest2.h"

@implementation TCPerson (TCTest2)
+ (void)load{

}
@end

load方法什么时候调用?

load方法在runtime加载类和分类的时候调用load

#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {
@autoreleasepool {

}
return 0;
}

@implementation TCPerson
+ (void)load{
NSLog(@"TCPerson +load");
}
@end


@implementation TCPerson (TCtest1)
+ (void)load{
NSLog(@"TCPerson (TCtest1) +load");
}
@end
@implementation TCPerson (TCTest2)
+ (void)load{
NSLog(@"TCPerson (TCtest2) +load");
}
@end
可以看到我们在main里面不导入任何的头文件,也不引用任何的类,直接运行,控制台输出

从输出结果我们可以看出,三个load方法都被调用

问题:分类重写方法,真的是覆盖原有类的方法么?如果不是,到底分类的方法是怎么调用的?

  • 首先我们在TCPerson申明一个方法+ (void)test并且在它的两个分类都重写+ (void)test


#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface TCPerson : NSObject
+ (void)test;
@end

NS_ASSUME_NONNULL_END

#import "TCPerson.h"

@implementation TCPerson
+ (void)load{
NSLog(@"TCPerson +load");
}
+ (void)test{
NSLog(@"TCPerson +test");
}
@end
分类重写test
#import "TCPerson+TCtest1.h"

@implementation TCPerson (TCtest1)
+ (void)load{
NSLog(@"TCPerson (TCtest1) +load");
}
+ (void)test{
NSLog(@"TCPerson (TCtest1) +test1");
}
@end
#import "TCPerson+TCTest2.h"

@implementation TCPerson (TCTest2)
+ (void)load{
NSLog(@"TCPerson (TCtest2) +load");
}
+ (void)test{
NSLog(@"TCPerson (TCtest2) +test2");
}
@end

在main里面我们调用test

#import <Foundation/Foundation.h>
#import "TCPerson.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
[TCPerson test];
}
return 0;
}

从输出结果中我们可以看到,只有分类2中的test被调用,为什么只调用分类2中的test了?


8471e1d6af0faf1df1690ae04cbcef00.png


因为编译顺序是分类2在后,1在前,这个时候我们改变编译顺序(拖动文件就行了)


e18505bb7f6efde9eddae6e43019c926.png

细心的老铁会看到,为什么load方法一直都在调用,这是为什么了?它和test方法到底有什么不同了?真的是我们理解中的load不覆盖,test覆盖了,所以才出现这种情况么?

我们打印TCPerson的类方法

void printMethodNamesOfClass(Class cls)
{
unsigned int count;
// 获得方法数组
Method *methodList = class_copyMethodList(cls, &count);

// 存储方法名
NSMutableString *methodNames = [NSMutableString string];

// 遍历所有的方法
for (int i = 0; i < count; i++) {
// 获得方法
Method method = methodList[I];
// 获得方法名
NSString *methodName = NSStringFromSelector(method_getName(method));
// 拼接方法名
[methodNames appendString:methodName];
[methodNames appendString:@", "];
}

// 释放
free(methodList);

// 打印方法名
NSLog(@"%@ %@", cls, methodNames);
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
[TCPerson test];
printMethodNamesOfClass(object_getClass([TCPerson class]));
}
return 0;
}


可以看到,TCPerson的所有类方法名,并不是覆盖,三个load,三个test,方法都在

load源码分析:查看objc底层源码我们可以看到:

void call_load_methods(void)
{
static bool loading = NO;
bool more_categories;

loadMethodLock.assertLocked();

// Re-entrant calls do nothing; the outermost call will finish the job.
if (loading) return;
loading = YES;

void *pool = objc_autoreleasePoolPush();

do {
// 1. Repeatedly call class +loads until there aren't any more
while (loadable_classes_used > 0) {
call_class_loads();
}

// 2. Call category +loads ONCE
more_categories = call_category_loads();

// 3. Run more +loads if there are classes OR more untried categories
} while (loadable_classes_used > 0 || more_categories);

objc_autoreleasePoolPop(pool);

loading = NO;
}
load方法它是先调用 while (loadable_classes_used > 0) {call_class_loads(); }类的load,再调用more_categories = call_category_loads()分类的load,和编译顺序无关,都会调用
我们查看call_class_loads()方法

static void call_class_loads(void)
{
int I;

// Detach current loadable list.
struct loadable_class *classes = loadable_classes;
int used = loadable_classes_used;
loadable_classes = nil;
loadable_classes_allocated = 0;
loadable_classes_used = 0;

// Call all +loads for the detached list.
for (i = 0; i < used; i++) {
Class cls = classes[i].cls;
load_method_t load_method = (load_method_t)classes[i].method;
if (!cls) continue;

if (PrintLoading) {
_objc_inform("LOAD: +[%s load]\n", cls->nameForLogging());
}
(*load_method)(cls, SEL_load);
}

// Destroy the detached list.
if (classes) free(classes);
}
其通过的是load_method_t函数指针直接调用
函数指针直接调用
typedef void(*load_method_t)(id, SEL);

其分类load方法调用也是一样

static bool call_category_loads(void)
{
int i, shift;
bool new_categories_added = NO;

// Detach current loadable list.
struct loadable_category *cats = loadable_categories;
int used = loadable_categories_used;
int allocated = loadable_categories_allocated;
loadable_categories = nil;
loadable_categories_allocated = 0;
loadable_categories_used = 0;

// Call all +loads for the detached list.
for (i = 0; i < used; i++) {
Category cat = cats[i].cat;
load_method_t load_method = (load_method_t)cats[i].method;
Class cls;
if (!cat) continue;

cls = _category_getClass(cat);
if (cls && cls->isLoadable()) {
if (PrintLoading) {
_objc_inform("LOAD: +[%s(%s) load]\n",
cls->nameForLogging(),
_category_getName(cat));
}
(*load_method)(cls, SEL_load);
cats[i].cat = nil;
}
}

为什么test不一样了

因为test是因为消息机制调用的,objc_msgSend([TCPerson class], @selector(test));消息机制就牵扯到了isa方法查找,test在元类方法里面顺序查找的

load只在加载类的时候调用一次,且先调用类的load,再调用分类的

load的继承关系调用
首先我们先看TCStudent
#import "TCStudent.h"

@implementation TCStudent

@end

不写load方法调用

TCStudent写上load


从中可以看出子类不写load的方法,调用父类的load,当子类调用load时,先调用父类的load,再调用子类的load,父类子类load取决于你写load方法没有,如果都写了,先调用父类的,再调用子类的

总结:先调用类的load,如果有子类,则先看子类是否写了load,如果写了,则先调用父类的load,再调用子类的load,当类子类调用完了,再是分类,分类的load取决于编译顺序,先编译,则先调用,test的方法调用走的是消息发送机制,其底层原理和load方法有着本质的区别,消息发送主要取决于isa的方法查找顺序



作者:枫紫
链接:https://www.jianshu.com/p/f66921e24ffe









0 个评论

要回复文章请先登录注册