MST

星途 面试题库

面试题:在多类别(Category)情况下,如何确保方法调用的优先级和正确性,尤其是涉及到方法冲突时?

假设在一个项目中,某个类`MyClass`有多个类别,如`MyClass+Category1`、`MyClass+Category2`等,且这些类别中可能存在同名方法。请阐述Objective-C运行时系统如何确定方法的调用顺序,在实际开发中,怎样设计和管理这些类别,以避免因方法冲突导致的不可预测行为?并举例说明如何利用runtime相关知识来解决可能出现的问题。
13.5万 热度难度
编程语言Objective-C

知识考点

AI 面试

面试题答案

一键面试

1. Objective - C运行时系统确定方法调用顺序的方式

在Objective - C运行时,当向一个对象发送消息时,运行时系统会按照以下顺序查找方法实现:

  1. 类的方法列表:首先在接收者对象所属类的方法列表中查找方法。如果找到了对应的方法,就直接调用该方法的实现。
  2. 父类的方法列表:如果在当前类的方法列表中没有找到,运行时会沿着继承链向上,在父类的方法列表中查找,直到找到方法或到达继承链的顶端(NSObject类)。
  3. 动态方法解析:如果在类和父类的方法列表中都没有找到方法,运行时会进入动态方法解析阶段。它会调用+ (BOOL)resolveInstanceMethod:(SEL)sel(实例方法)或+ (BOOL)resolveClassMethod:(SEL)sel(类方法)方法,允许开发者在运行时动态添加方法实现。如果动态添加了方法,运行时会重新开始查找过程。
  4. 备用接收者:如果动态方法解析没有找到方法,运行时会调用- (id)forwardingTargetForSelector:(SEL)aSelector方法,允许对象返回一个备用的对象来处理这个消息。如果返回了非nil的对象,运行时会向这个备用对象发送消息。
  5. 完整的消息转发:如果forwardingTargetForSelector:返回nil,运行时会进入完整的消息转发阶段。它会创建一个NSInvocation对象,包含方法调用的所有信息,然后调用- (void)forwardInvocation:(NSInvocation *)anInvocation方法。开发者可以在这个方法中手动处理消息转发,例如将消息转发给其他对象。如果forwardInvocation:没有处理消息,运行时会调用- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector方法获取方法签名,如果获取到有效的签名,会再次调用forwardInvocation:,否则会抛出unrecognized selector sent to instance异常。

当存在类别(Category)时,类别中的方法会在运行时被添加到类的方法列表头部。这意味着如果类本身和类别中有同名方法,类别中的方法会优先被找到和调用。如果多个类别中有同名方法,最后参与编译的类别中的方法会优先被调用,因为它在方法列表的更靠前位置。

2. 实际开发中设计和管理类别的方法,避免方法冲突

命名规范

  • 使用唯一前缀:为类别中的方法命名添加唯一的前缀,以降低与其他类别或类本身方法冲突的可能性。例如,如果类别是用于处理网络相关功能,可以使用如net_作为前缀,方法名可以是- (void)net_fetchDataWithCompletion:(void(^)(id data, NSError *error))completion
  • 明确功能描述:方法名要清晰地描述其功能,这样即使不同类别有相似功能,也能通过方法名区分。例如,一个用于加载图片的类别,方法名可以是- (void)loadImageFromURL:(NSURL *)url completion:(void(^)(UIImage *image, NSError *error))completion

组织类别

  • 按功能分组:将相关功能的类别放在一起,便于管理和维护。例如,将所有与用户界面相关的类别放在一个文件夹,与数据处理相关的类别放在另一个文件夹。
  • 避免过度使用类别:不要为一个类创建过多的类别,尽量将功能相似的方法放在一个类别中。如果类别过多,会增加方法冲突的风险,也不利于代码的可读性和维护性。

编译顺序控制

虽然不推荐依赖编译顺序来避免方法冲突,但在某些情况下,可以通过调整类别文件的编译顺序来确保特定的方法优先被调用。不过这种方法不够可靠,因为编译顺序可能会因为项目结构的变化而改变。

3. 利用runtime相关知识解决可能出现的问题示例

假设MyClass有两个类别MyClass+Category1MyClass+Category2,它们都定义了一个名为commonMethod的方法。我们可以利用runtime的方法交换技术来解决方法冲突问题,同时保留两个方法的功能。

#import <objc/runtime.h>

@implementation MyClass (Category1)
- (void)commonMethod {
    NSLog(@"Category1's commonMethod");
}
@end

@implementation MyClass (Category2)
- (void)commonMethod {
    NSLog(@"Category2's commonMethod");
}
@end

@implementation MyClass
+ (void)load {
    Method originalMethod1 = class_getInstanceMethod(self, @selector(commonMethod));
    Method swappedMethod1 = class_getInstanceMethod(self, @selector(category1_commonMethod));
    method_exchangeImplementations(originalMethod1, swappedMethod1);
    
    Method originalMethod2 = class_getInstanceMethod(self, @selector(commonMethod));
    Method swappedMethod2 = class_getInstanceMethod(self, @selector(category2_commonMethod));
    method_exchangeImplementations(originalMethod2, swappedMethod2);
}

- (void)category1_commonMethod {
    // 先调用Category2的方法
    [self category2_commonMethod];
    NSLog(@"After calling Category2's method, execute Category1's commonMethod");
}

- (void)category2_commonMethod {
    NSLog(@"Execute Category2's commonMethod");
}
@end

在上述代码中,通过+load方法在类加载时进行方法交换。这样,当调用commonMethod时,实际上会先调用category2_commonMethod,然后再执行category1_commonMethod中的其他逻辑,从而同时保留了两个类别中commonMethod的功能,避免了简单的方法覆盖导致的功能丢失。