个人面试整理

2 阅读25分钟

1. 元类 为什么要设计metaclass

参考[为什么要设计metaclass - 掘金 (juejin.cn)](https://juejin.cn/post/6844904074630922254)

OC面向对象能力的部分师承于Smalltalk,通过类的划分和消息传递两个亮点解释了为什么要有metaclass。 类对象的isa指向元类对象,元类对象存储着类方法列表。

单一职责

建立稳定、灵活、健壮的设计 单一职责原则(Single Responsibility Principle); 开闭原则(Open Closed Principle); 里氏替换原则(Liskov Substitution Principle); 迪米特法则(Law of Demeter),又叫“最少知道法则”; 接口隔离原则(Interface Segregation Principle); 依赖倒置原则(Dependence Inversion Principle)

实例对象存储属性值的事,类对象存储实例方法列表,元类对象存储类方法列表。 如果没有元类的设计,在类中管理实例方法和类方法,两者存在两个不同的数组中。这样的话在消息接收流程中需要标注上传入的cls到底是实例对象还是类对象。 这也就意味着在查找方法的缓存时同样也需要判断cls到底是个什么类,需要给传入的方法带上是类方法还是实例方法的标识,但是如果有了元类的存在来存储类方法就可以忽略了对对象类型的判断和方法类型的判断可以大大的提升消息发送的效率,并且在不同种类的方法走的都是同一套流程,在之后的维护上也大大节约了成本。 由此可以看出 元类的设计简化了实例方法和类方法的调用流程,大大提升了消息发送的效率。

扩展性
  • 进入_objc_msgSend后首先判断消息的接收者
  • 根据消息接收者的isa指针找到metaclass(因为类方法存在元类中。如果调用的是实例方法,isa指针指向的是类对象。)
  • 进入CacheLookup流程,这一步会去寻找方法缓存,如果缓存命中则直接调用方法的实现,如果缓存不存在则进入objc_msgSend_uncached流程
  • 进入objc_msgSend_uncached流程 会调用 __class_lookupMethodAndLoadCache3 方法
    1. 首先会再一次的从类中寻找需要调用方法的缓存,如果能命中缓存直接返回该方法的实现,如果不能命中则继续往下走
    2. 从类的方法列表中寻找该方法,如果能从列表中找到方法则对方法进行缓存并返回该方法的实现,如果找不到该方法则继续往下走。
    3. 从父类的缓存寻找该方法,如果父类缓存能命中则将方法缓存至当前调用方法的类中(注意这里不是存进父类),如果缓存未命中则遍历父类的方法列表,之后操作如同第2步,未能命中则继续走第3步直到寻找到基类。
    4. 如果到基类依然没有找到该方法则触发动态方法解析流程。
    5. 动态方法解析流程找不到就触发消息转发流程

2. class_copyIvarList & class_copyPropertyList区别

class_copyIvarList

能够获取.h和.m中的所有属性以及大括号中声明的变量,获取的属性名称有下划线(大括号中的除外)

class_copyPropertyList

只能获取由property声明的属性,包括.m中的,获取的属性名称不带下划线。

3. class_rw_t 和 class_ro_t 的区别

每个类都对应有一个class_ro_t结构体和一个class_rw_t结构体,他们都存放着当前类的属性、实例变量、方法、协议等等。在编译期间,class_ro_t结构体就已经确定,在运行 runtime 的 realizeClass时,会生成class_rw_t结构体,该结构体包含了class_ro_t,并且更新data部分,换成class_rw_t结构体的地址。 之前 image.png 之后 image.png

class_ro_t

存放的是编译期间就确定的

class_rw_t

在runtime时才确定,class_rw_t是class_ro_t的超集,当然实际访问类的方法、属性等也都是访问的class_rw_t中的内容

4. category如何被加载的,两个category的load方法的加载顺序,两个category的同名方法的加载顺序

参考 1. Category 相关的方法加载顺序 - 掘金 (juejin.cn) 2. Category的加载机制 - 简书 (jianshu.com) 3. 多个category实现同一个方法调用的顺序_ios 多个category方法调用顺序-CSDN博客

Category

Category 在经历过编译后里面的内容:对象方法、类方法、协议、属性都转化为类型为 category_t 的结构体变量。

category的加载是在运行时 runtime  发生的,加载过程是,把category的实例方法、属性、协议添加到类对象上,把类方法、属性、协议添加到  metaclass 上。

category的load方法执行顺序是根据类的编译顺序决定的,即:[xcode]中的  Build Phases中的Compile Sources 中的文件从上到下的顺序加载的。

category并不会替换掉同名的方法的,也就是说如果 category 和原来类都有 methodA,那么 category 附加完成之后,类的方法列表里会有两个 methodA,并且category添加的methodA会排在原有类的methodA的前面,因此如果存在category的同名方法,那么在调用的时候,则会先找到最后一个编译的 category 里的对应方法。

原因是根据runtime的消息传递机制中的核心函数void objc_msgSend(id self,SEL cmd,...)来发送消息,先从当前类中查找调用的方法,若没有找到则继续从其父类中一层层往上找,那么对于category重写同一个方法,则在消息传递的过程中,会最先找到category中的方法并执行该方法。对于多个分类调用同一个方法,Xcode在运行时是根据Build Phases中的Compile Sources里面的从上至下顺序编译的,编译时通过压栈的方式将多个分类压栈,根据后进先出的原则,后编译的会被先调用,(插入顶部添加,即[methodLists insertObject:category_method atIndex:0];

所以objc_msgSend遍历方法列表查找SEL 对应的IMP时,会先找到分类重写的那个,调用执行)当objc_msgSend找到方法并调用之后,就不再继续传递消息,造成了看似像是覆盖的效果。

Category 方法可能会覆盖于同一个类class 的其它 category 中的方法。但也可能被覆盖,因为无法百分百确保他们的加载优先顺序,出现这种情况通常会在编译时出错。如果在一个开发的SDK中使用了类别, 就最好保证类别名不同于使用者的类别名以及类别方法也不同于使用者的类别方法名, 通常通过加前缀来做到。

  • 在不修改原有类的基础上给原有类添加方法,因为 category 的结构体指针中没有属性列表,只有方法列表。所以原则来说只能给 category 添加方法,不能添加属性,如果需要给 category 添加类似属性功能,可以通过关联对象实现
  • Category 中的方法优先于原有类同名的方法,即会优先调用 category 中的方法,忽略原有类的方法。即 category 与原有类同名方法调用的优先级为: category > 本类 > 父类。开发中尽量不要覆盖本类的方法,如果覆盖会导致本类方法失效

loadinitialize

相同点
  1. 两个函数都是系统自动调用,因此无需手动调用(如果手动调用则与普通函数调用类似);
  2. 两个函数都会隐士调用各自父类对应的 + (void)load 或 + (void)initialize 方法,即子类调用方法之前,会优先调用其父类对应的方法;
  3. 两个函数内部都使用了锁,因此两个函数都是线程安全的
不同点
  1. 调用时机不同:+ (void)load 在 main 函数之前执行,即 objc_init Runtime初始化时调用,且只会调用一次。 + (void)initialize 在类的方法首次被调用时执行,每个类只会调用一次,但父类可能会调用多次;
  2. 调用方式不同:+ (void)load 是根据函数地址直接调用,+ (void)initialize 是通过消息发送机制即 objc_msgSend(id self, SEL _cmd, ...) 调用;
  3. 子类父类调用关系不同:
    • 如果子类没有实现 + (void)load,则不会调用其父类的 + (void)load 方法。
    • 如果子类没有实现 + (void)initialize,则会调用其父类的方法,因此父类的 + (void)initialize 可能会调用多次;
  4. category 对调用的影响不同:
    • 如果 category 中实现了 + (void)load,则会优先调用原类的的 + (void)load,再调用 category 的,即优先级为:父类 > 原类 > category

image.png

load 调用机制

  1. 没有继承关系的不同类中的 + (void)load 的调用顺序跟 Compile Sources 顺序有关,即在前面的优先编译的类或者 category 先调用( 备注:  所有类的 + (void)load 优先级大于 category 的优先级);
  2. 同一个类的 category 的 + (void)load 的调用顺序跟 Compile Sources 顺序有关,即在前面的优先编译的 category 会先调用;
  3. 同一镜像中主工程的 + (void)load 方法优先调用,然后再调用静态库的 + (void)load 方法。有多个静态库时,静态库之间的执行顺序与编译顺序有关,即它们在 Link Binary With Libraries 中的顺序;
  4. 不同镜像中,动态库的 + (void)load 方法优先调用,然后再调用主工程的 + (void)load,多个动态库的 + (void)load 方法的调用顺序跟编译顺序有关,即它们在 Link Binary With Libraries 中的顺序;

initialize 调用机制

  1. 如果 category 中实现了 + (void)initialize,则原类的 + (void)initialize 将不会再调用
  2. 多个 category 中同时实现了 + (void)initialize 方法时,Compile Sources中顺序最下面的一个,即最后一个被编译 Category 的 + (void)initialize 会执行

category的同名方法的加载顺序

多次调整分类的编译顺序,发现仅会调用分类最后编译的。并不是后面创建的就一定被调用,得看创建之后其在buildPhases->Compile Sources里面的位置。也就是普通方法的优先级: 分类> 父类, 优先级高(分类)的同名方法覆盖优先级低的,分类覆盖其他类。

5. category & extension区别,能给NSObject添加Extension吗,结果如何

  • Category的方法中,不可以调用super方法;
  • Category原则上只能增加方法,Extension不仅可以增加方法,还可以增加实例变量(或者属性),只是该实例变量默认是@private类型的声明在.m 文件内 用范围只能在自身类,而不是子类或其他地方)
  • Extension是在编译阶段添加到类中,而Categor是在运行时添加到类中的,导致Extension中声明的方法没被实现,编译器会报警,但是 Category 的方法没被实现编译器是不会有任何警告的
  • Extension不能像 Category 那样拥有独立的实现部分,他所声明的方法只能在其对应的类中实现,类扩展所声明的方法必须依托对应类的实现部分来实现。
  • Extension定义在 .m 文件中的类扩展方法为私有的,定义在 .h 文件(头文件)中的类扩展方法为公有的。类扩展是在 .m 文件中声明 私有方法 的非常好的方式。
//.h文件
@interface myClass : NSObject
@property (readOnly)NSString *name; // 该.h文件对外公开
@end

   //.m文件
@interface myClass ()
@property(readWrite)NSString *name;//该.m文件对外是不公开的.当然这里也可以放在专门的一个.h文件中,但同样不把这个文件进行公开
@end
@implement myClass()
@synthesize name;
//...
@end
  • 当需要声明一个属性,它对外是只读的,但是在内部是可以修改的,这时可以通过Extension来实现;
  • Extension一般用来隐藏类的私有消息(例如封装SDK对外部暴露可调用的方法和属性但是隐藏实现),你必须有一个类的源码才能添加一个类的Extension,所以对于系统一些类,如NSString,就无法添加类扩展,如果只是简单的创建Extension文件是能够创建成功的,但是如果你在文件中添加属性或者方法,在程序中一旦使用了该属性或者方法程序就会崩溃,会报找不到相对应的方法错误信息。(可以给nsobject创建扩展,但是无法添加任何属性方法)

6. 消息转发机制,消息转发机制和其他语言的消息机制优劣对比

消息转发机制是相对于消息传递机制来实现的

可以用来 防止特定崩溃,实现多继承

对于C语言,函数的调用在编译的时候会决定调用哪个函数。编译完成之后直接顺序执行,无任何二义性。OC的函数调用称为消息发送。属于动态调用过程。在编译的时候并不能决定真正调用哪个函数(也就是说,在编译阶段,OC可以调用任何函数,即使这个函数并未实现,只要申明过就不会报错。而C语言在编译阶段就会报错)。只有在真正运行的时候才会根据函数的名称找 到对应的函数来调用。

消息转发机制的流程

当向someObject发送某消息,但runtime在当前类和父类中都找不到对应方法的实现时,runtime并不会立即报错使程序崩溃,而是依次执行下列步骤 image.png

1. 在当前类和父类中寻找对应方法的实现
如果是实例方法调用,则会首先循环该类的实例方法列表,没找到就找父类,一直到根类NSObject;如果是类方法调用,则首先循环该类的元类的方法列表,没找到就继续找父元类,最后直到根元类。如果是上述过程能找到对应的方法则流程结束,如果是没有找到对应的方法,则进入右边的流程。
2. 动态方法解析:向当前类发送 resolveInstanceMethod: 信号,检查是否动态向该类添加了方法。
在该阶段可以使用 `resolveInstanceMethod` 处理实例对象方法调用报错、`resolveClassMethod` 处理类方法调用报错

例子:

调用方法:

Simple * s = [[Simple alloc]init];
[s performSelector:@selector(instanceMethodTest)withObject:@"instanceMethodTest"];
[s performSelector:@selector(classMethodTest) withObject:@"classMethodTest"]

补救方法:

// 实例方法
+(BOOL)resolveInstanceMethod:(SEL)sel {
    if ([NSStringFromSelector(sel) isEqualToString:@"instanceMethodTest:"]) {
        Method method = class_getInstanceMethod([self class],@selector(addDynamicInstanceMethod:));
        IMP methodIMP = method_getImplementation(method);
        const char * types = method_getTypeEncoding(method);
        class_addMethod([self class], sel, methodIMP, types);
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}
// 类方法
+(BOOL)resolveClassMethod:(SEL)sel {
    if ([NSStringFromSelector(sel) isEqualToString:@"classMethodTest:"]) {
        // 类方法都是存在元类中,所以添加方法需要往元类上添加
        Class metaClass = object_getClass([self class]);
        Method method = class_getClassMethod([self class], @selector(addDynamicClassMethod:));
        IMP methodIMP = method_getImplementation(method);
        const char * types = method_getTypeEncoding(method);
        class_addMethod(metaClass, sel, methodIMP, types);
        return YES;
    }
    return [super resolveClassMethod:sel];
}

-(void)addDynamicInstanceMethod:(NSString *)value {
    NSLog(@"addDynamicInstanceMethod value = %@",value);
}

+(void)addDynamicClassMethod:(NSString *)value {
    NSLog(@"addDynamicClassMethod value = %@",value);
}

resolve函数中,如果返回No,那么就会进入快速转发过程;如果是返回yes,那么编译器则会重发一次刚才的消息,然后相当于重新调用了performSelector。因为我们已经通过resolve函数动态添加上了实例方法和类方法,此时重发消息就能正常响应了。

3. 快速消息转发:检查该类是否实现了 forwardingTargetForSelector: 方法,若实现了则调用这个方法。若该方法返回值对象非nil或非self,则向该返回对象重新发送消息。(返回可以执行方法的类或者实例对象)

此时我们继续调用下面两个方法,和之前一样,依旧会报unrecognized selector sent to instance错误,因为这两个方法在resolve阶段并没有能处理,接下来就会进入快速消息转发流程

新建一个类来声明需要调用的方法

#import "SubSimple.h"
@implementation SubSimple
-(void)instanceMethodTestFastForwarding:(NSString *)strValue {
    NSLog(@"instanceMethodTestFastForwarding value=%@",strValue);
}
+(void)classMethodTestFastForwarding:(NSString *)strValue {
    NSLog(@"classMethodTestFastForwarding value=%@",strValue);
}
@end

-(id)forwardingTargetForSelector:(SEL)aSelector+(id)forwardingTargetForSelector:(SEL)aSelector 中进行快速转发 实例方法 和 类方法

// 实例对象
-(id)forwardingTargetForSelector:(SEL)aSelector {
    if ([NSStringFromSelector(aSelector) isEqualToString:@"instanceMethodTestFastForwarding:"]) {
        SubSimple * sub = [[SubSimple alloc]init];
        if ([sub respondsToSelector:aSelector]) {
            return  sub;
        }
    }
    return [super forwardingTargetForSelector:aSelector];
}
// 类对象
+(id)forwardingTargetForSelector:(SEL)aSelector {
    if ([NSStringFromSelector(aSelector) isEqualToString:@"classMethodTestFastForwarding:"]) {
        if ([SubSimple respondsToSelector:aSelector]) {
            return  [SubSimple class];
        }
    }
    return [super forwardingTargetForSelector:aSelector];
}

forwardingTargetForSelector这个函数中主要是需要返回一个可以相应该方法的对象,可以是类对象或者实例对象,只要能响应该方法即可。如果返回self或者nil那么就会进入标准消息转发,也就是完整消息转发阶段

5. 标准消息转发:runtime发送methodSignatureForSelector消息获取Selector对应的方法签名。返回值非空则通过forwardInvocation:转发消息,返回值为空则向当前对象发送doesNotRecognizeSelector:消息,程序崩溃退出。

接下来我们继续调用下面函数,和之前一样,依旧会报unrecognized selector sent to instance错误,因为这两个方法在fast forwarding阶段并没有能处理,接下来就会进入完整的消息转发

[fromView performSelector:@selector(instanceMethodTestNormalForwarding:) withObject:@"instanceMethodTestNormalForwarding"];
[FromView performSelector:@selector(classMethodTestNormalForwarding:) withObject:@"classMethodTestNormalForwarding"];

新建一个类来声明需要调用的方法

#import "Sub2Simple.h"
@implementation Sub2Simple
-(void)instanceMethodTestNormalForwarding:(NSString *)strValue {
    NSLog(@"instanceMethodTestNormalForwarding value=%@",strValue);
}

+(void)classMethodTestNormalForwarding:(NSString *)strValue {
    NSLog(@"classMethodTestNormalForwarding value=%@",strValue);
}
@end

forwardInvocationmethodSignatureForSelector 中进行标准转发 实例方法 和 类方法

// 实例方法
-(void)forwardInvocation:(NSInvocation *)anInvocation {
    SEL selector = anInvocation.selector;
    BOOL found = FALSE;
    if ([NSStringFromSelector(selector) isEqualToString:@"instanceMethodTestNormalForwarding:"]) {
        SubSimple * sub = [[SubSimple alloc]init];
        Sub2Simple * sub2 = [[Sub2Simple alloc]init];
        if ([sub respondsToSelector:selector]) {
            [anInvocation invokeWithTarget:subFromView];
            found = YES;
        }
        if ([sub2 respondsToSelector:selector]){
            [anInvocation invokeWithTarget:fromNSObject];
            found = YES;
        }
        // optional
        if (!found) {
            [self doesNotRecognizeSelector:selector];
        }
    }
}
-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    NSMethodSignature * sign = [super methodSignatureForSelector:aSelector];
    if (!sign) {
        sign = [NSMethodSignature signatureWithObjCTypes:"v@:@"];
    }
    return sign;
}
// 类方法
+(void)forwardInvocation:(NSInvocation *)anInvocation {
    SEL selector = anInvocation.selector;
    BOOL found = FALSE;
    if ([NSStringFromSelector(selector) isEqualToString:@"classMethodTestNormalForwarding:"]) {
        if ([SubSimple respondsToSelector:selector]) {
            [anInvocation invokeWithTarget:[SubSimple class]];
            found = YES;
        }
        if ([Sub2Simple respondsToSelector:selector]){
            [anInvocation invokeWithTarget:[Sub2Simple class]];
            found = YES;
        }
        // optional
        if (!found) {
            [self doesNotRecognizeSelector:selector];
        }
    }
}
+(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    NSMethodSignature * sign = [super methodSignatureForSelector:aSelector];
    if (!sign) {
        sign = [NSMethodSignature signatureWithObjCTypes:"v@:@"];
    }
    return sign;
}

标准需要同时实现methodSignatureForSelectorforwardInvocation两个函数,相当于是重新给该消息进行签名methodSignatureForSelector,然后调用forwardInvocation转发。

标准转发和快速转发的区别:
  • 快速转发转发的对象最多只能有一个,标准转发却可以同时转发多个对象。

  • 快速转发只需要实现forwardingTargetForSelector这个方法,但是标准转发必须同时实现methodSignatureForSelector和forwardInvocation方法。

  • 快速转发必须指定转发对象或者进入标准转发,但是标准转发作为最终步骤,可以不指定转发对象,也可以看心情要不要调用doesNotRecognizeSelector来控制抛异常。标准转发实际上可以用作避免闪退,比如发现没有可转发的对象时,此时友好的弹一个错误提示,而不是直接就闪退,这样能极大程度的优化用户体验。

7. 在方法调用的时候,方法查询-> 动态解析-> 消息转发 之前做了什么

会转成objc_msgSend(id , SEL, ...)方法进行调用,这个方法第一个参数是一个消息接收者对象,息接收者的isa指针找到metaclassclass(因为类方法存在元类中。如果调用的是实例方法,isa指针指向的是类对象。),runtime通过这个isa指针找到这个对象的类/元类,从类/元类对象中的cache中查找是否存在SEL对应的IMP CacheLookup ,若不存在,则会在 method_list中查找,如果还是没找到,则会到supper_class中查找,仍然没找到的话,就会调用_objc_msgForward(id, SEL, ...)进行消息转发

  • 进入_objc_msgSend后首先判断消息的接收者
  • 根据消息接收者的isa指针找到metaclass(因为类方法存在元类中。如果调用的是实例方法,isa指针指向的是类对象。)
  • 进入CacheLookup流程,这一步会去寻找方法缓存,如果缓存命中则直接调用方法的实现,如果缓存不存在则进入objc_msgSend_uncached流程
  • 进入objc_msgSend_uncached流程 会调用 __class_lookupMethodAndLoadCache3 方法
    1. 首先会再一次的从类中寻找需要调用方法的缓存,如果能命中缓存直接返回该方法的实现,如果不能命中则继续往下走
    2. 从类的方法列表中寻找该方法,如果能从列表中找到方法则对方法进行缓存并返回该方法的实现,如果找不到该方法则继续往下走。
    3. 从父类的缓存寻找该方法,如果父类缓存能命中则将方法缓存至当前调用方法的类中(注意这里不是存进父类),如果缓存未命中则遍历父类的方法列表,之后操作如同第2步,未能命中则继续走第3步直到寻找到基类。
    4. 如果到基类依然没有找到该方法则触发动态方法解析流程。
    5. 动态方法解析流程找不到就触发消息转发流程

8. IMPSELMethod的区别和使用场景

一个类(Class)持有一个分发表,在运行期分发消息,表中的每一个实体代表一个方法(Method),它的名字叫做选择子(SEL),对应着一种方法实现(IMP)。
一个类(Class)持有一系列的方法(Method),在load类时,runtime会将所有方法的选择器(SEL)hash后映射到一个集合(NSSet)中(NSSet里的元素不能重复)。
当需要发消息时,会根据选择器(SEL)去查找方法;找到之后,用Method结构体里的函数指针(IMP)去调用方法。这样在运行时查找selecter的速度就会非常快。

具体的分析如下

SEL:

typedef struct objc_selector *SEL,代表方法的名称,类成员方法的指针,方法编号。仅以名字来识别,SEL代表方法在 Runtime 期间的标识符。为 SEL 类型,虽然 SEL 是 objc_selector 结构体指针,但实际上它只是一个 C 字符串。在类加载的时候,编译器会生成与方法相对应的选择子,并注册到 Objective-C 的 Runtime 运行系统。不论两个类是否存在依存关系,只要他们拥有相同的方法名,那么他们的SEL都是相同的。因此类方法定义时,尽量不要用相同的名字,就算是变量类型不同也不行。否则会引起重复,

// 获得SEL的三种方式 
SEL selA = @selector(setTitle:); 
SEL selB = sel_registerName("setTitle:"); 
SEL selC = NSSelectorFromString(@"setTitle:");
// sel转字符串
NSLog(@"%s", sel_getName(selB));
IMP:

typedef id (*IMP)(id, SEL, ...),代表函数指针,即函数执行的入口。该函数使用标准的 C 调用。第一个参数指向 self(它代表当前类实例的地址,如果是类则指向的是它的元类),作为消息的接受者;第二个参数代表方法的选择子;... 代表可选参数,前面的 id 代表返回值。

// 返回方法的具体实现
IMP class_getMethodImplementation(Class cls, SEL name);
IMP class_getMethodImplementation_stret(Class cls, SEL name);
// 类实例是否响应指定的selector
BOOL class_respondsToSelector(Class cls, SEL sel);

可以直接绕过Runtime的消息传递机制,直接执行IMP指向的函数了。省去了一些类的查找,直接向对象发送消息,效率会高一些。

// 根据代码块获取IMP, 其实就是代码块与IMP关联
IMP imp_implementationWithBlock(id block) 
// 根据Method获取IMP
IMP method_getImplementation(Method m) 
// 根据SEL获取IMP
[[objc Class] instanceMethodForSelector:SEL] 

当我们获取一个方法的IMP后,可以直接调用IMP

IMP imp = method_getImplementation(Method m);
// result保存方法的返回值,id表示调用这个方法的对象,SEL是Method的选择器,argument是方法的参数。
id result = imp(id, SEL, argument);
Method:

typedef struct objc_method *Method,Method 对开发者来说是一种不透明的类型,被隐藏在我们平时书写的类或对象的方法背后。它是一个 objc_method 结构体指针,我们可以看到该结构体中包含一个SEL和IMP,实际上相当于在SEL和IMP之间作了一个映射。有了SEL,我们便可以找到对应的IMP,从而调用方法的实现代码。 objc_method 的定义为

/// Method
typedef struct objc_method *Method;
struct objc_method {
    SEL method_name; 
    char *method_types;
    IMP method_imp;
 };
  • 方法名 method_name 类型为 SEL,前面提到过相同名字的方法即使在不同类中定义,它们的方法选择器也相同。
  • 方法类型 method_types 是个 char 指针,存储着方法的参数类型和返回值类型,即是 Type Encoding 编码。
  • method_imp 指向方法的实现,本质上是一个函数的指针,就是前面讲到的 Implementation。
方法操作主要有以下函数:
BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types); // 添加方法
Method class_getInstanceMethod(Class cls, SEL name); // 获取实例方法
Method class_getClassMethod(Class cls, SEL name); // 获取类方法
Method *class_copyMethodList(Class cls, unsigned int *outCount); // 获取所有方法的数组
// 替代方法的实现
IMP class_replaceMethod(Class cls, SEL name, IMP imp, const char *types); 
// 交换两个方法的实现
method_exchangeImplementations(Method m1, Method m2)

通过上面三个可以直接调用方法 跳过消息的转发流程 稍微提升效率

@implementation testManager
-(void)justTest:(NSString*)message{
    NSLog(@"testManager instance method %@",message);
}
+(void)justTest:(NSString*)message{
    NSLog(@"testManager class method %@",message);
}
@end
+(void)test{
    Class test_cls    = NSClassFromString(@"testManager");
    id test_ins = [[test_cls alloc] init];
    SEL sel = sel_registerName("justTest:");
    Method methodA = class_getInstanceMethod(test_cls, sel);
    Method methodB = class_getClassMethod(test_cls,sel);
    IMP impA = method_getImplementation(methodA);
    IMP impB = method_getImplementation(methodB);
    id resultA = impA(test_ins, sel, @"testA");
    id resultB = impB(test_cls, sel, @"testB");

}

输出:

testManager instance method testA
testManager class method testB

9. loadinitialize方法的区别什么?在继承关系中他们有什么区别

相同点
  1. 两个函数都是系统自动调用,因此无需手动调用(如果手动调用则与普通函数调用类似);
  2. 两个函数都会隐士调用各自父类对应的 + (void)load 或 + (void)initialize 方法,即子类调用方法之前,会优先调用其父类对应的方法;
  3. 两个函数内部都使用了锁,因此两个函数都是线程安全的
不同点
  1. 调用时机不同:+ (void)load 在 main 函数之前执行,即 objc_init Runtime初始化时调用,且只会调用一次。 + (void)initialize 在类的方法首次被调用时执行,每个类只会调用一次,但父类可能会调用多次;
  2. 调用方式不同:+ (void)load 是根据函数地址直接调用,+ (void)initialize 是通过消息发送机制即 objc_msgSend(id self, SEL _cmd, ...) 调用;
  3. 子类父类调用关系不同:
    • 如果子类没有实现 + (void)load,则不会调用其父类的 + (void)load 方法。
    • 如果子类没有实现 + (void)initialize,则会调用其父类的方法,因此父类的 + (void)initialize 可能会调用多次;
  4. category 对调用的影响不同:
    • 如果 category 中实现了 + (void)load,则会优先调用原类的的 + (void)load,再调用 category 的,即优先级为:父类 > 原类 > category
    • 如果 category 中实现了 + (void)initialize,则原类的 + (void)initialize 将不会再调用

load 调用机制

  1. 没有继承关系的不同类中的 + (void)load 的调用顺序跟 Compile Sources 顺序有关,即在前面的优先编译的类或者 category 先调用( 备注:  所有类的 + (void)load 优先级大于 category 的优先级);
  2. 同一个类的 category 的 + (void)load 的调用顺序跟 Compile Sources 顺序有关,即在前面的优先编译的 category 会先调用;
  3. 同一镜像中主工程的 + (void)load 方法优先调用,然后再调用静态库的 + (void)load 方法。有多个静态库时,静态库之间的执行顺序与编译顺序有关,即它们在 Link Binary With Libraries 中的顺序;
  4. 不同镜像中,动态库的 + (void)load 方法优先调用,然后再调用主工程的 + (void)load,多个动态库的 + (void)load 方法的调用顺序跟编译顺序有关,即它们在 Link Binary With Libraries 中的顺序;

initialize 调用机制

  1. 如果 category 中实现了 + (void)initialize,则原类的 + (void)initialize 将不会再调用
  2. 多个 category 中同时实现了 + (void)initialize 方法时,Compile Sources中顺序最下面的一个,即最后一个被编译 Category 的 + (void)initialize 会执行

10. 消息转发机制的劣势

消息转发机制是一种用于在分布式系统中传递消息的方法,它具有以下优点和劣势:

优点:

  1. 可靠性:通过设置重试机制,可以保证消息不会丢失。
  2. 可扩展性:当系统的节点数量增加时,可以很方便地扩展消息转发机制。
  3. 灵活性:可以根据实际情况自由选择转发策略,并且可以随时调整。

劣势:

  1. 性能问题:由于消息转发需要经过多次传递,这可能导致性能问题。
  2. 复杂度问题:实现消息转发机制需要考虑很多因素,使得代码实现变得复杂。

总的来说,消息转发机制是一种非常有用的技术,但是也存在一些问题和限制。

参考文章:

2020 阿里、字节iOS面试题之Runtime相关问题1(附答案) - 简书 (jianshu.com)

iOS_selector、SEL、IMP、Method都是什么,以及之间的关系_ios sel-CSDN博客

消息转发机制的优劣-掘金 (juejin.cn)

iOS 调用IMP/objc_msgSend详细说明 - 简书 (jianshu.com)