ios面试题面试题

iOS Cateogry的深入理解&&load方法调用&&分类重

2020-11-23  本文已影响0人  枫紫_6174

首先先看几个面试问题

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

类结构

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

image.png

Cateogry里面有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方法什么时候调用?

#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方法都被调用

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

#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了?


编译顺序

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


改变后的顺序
其输出结果为:
image.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在元类方法里面顺序查找的(关于isa,可以查看我的实例对象,类对象,元类对象的关联---isa/superclass指针(2))里面有详细的关于test的方法调用原理

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

load的继承关系调用
首先我们先看TCStudent

#import "TCStudent.h"

@implementation TCStudent

@end

不写load方法调用


TCStudent不写load

TCStudent写上load


TCStudent写上load

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

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

上一篇下一篇

猜你喜欢

热点阅读