MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Objective-C运行时中的Selector与IMP解析及其在性能优化中的应用

2021-09-184.0k 阅读

1. 理解 Objective-C 运行时

Objective-C 是一门动态语言,其运行时系统是核心所在。运行时系统在程序运行时动态地处理消息发送、动态方法解析、消息转发等机制。这与静态语言在编译时就确定函数调用的方式截然不同。

在运行时,对象接收到消息后,运行时系统会在该对象的类的方法列表中查找对应的实现。这个过程涉及到两个关键概念:Selector 和 IMP。

2. Selector 解析

2.1 Selector 的定义与本质

Selector 本质上是一个指向方法的唯一标识的指针。它在运行时用于标识一个方法的名称。在 Objective-C 中,当你向一个对象发送消息时,比如 [obj someMethod],编译器会将 someMethod 转换为一个 Selector。

Selector 是通过 @selector() 表达式或者 NSSelectorFromString() 函数来创建的。例如:

SEL selector1 = @selector(viewDidLoad);
SEL selector2 = NSSelectorFromString(@"viewDidLoad");

这里 selector1selector2 指向的是同一个 Selector,它们都标识了 viewDidLoad 这个方法。

Selector 的唯一性保证了在整个程序运行过程中,相同名称的方法对应同一个 Selector。这使得运行时系统能够高效地查找方法的实现。

2.2 Selector 的存储与查找

Selector 存储在全局的哈希表中。当创建一个新的 Selector 时,运行时系统会首先在哈希表中查找是否已经存在相同名称的 Selector。如果存在,则直接返回已有的 Selector;如果不存在,则创建一个新的 Selector 并插入到哈希表中。

这种存储和查找方式使得 Selector 的创建和比较操作都非常高效。由于 Selector 是基于哈希表查找的,所以即使在一个大型项目中,查找一个 Selector 的时间复杂度也近似为 O(1)。

3. IMP 解析

3.1 IMP 的定义与本质

IMP 是指向方法实现的指针。当运行时系统根据 Selector 在类的方法列表中找到对应的方法时,它会获取到该方法的 IMP。IMP 实际上是一个函数指针,指向具体实现该方法的代码块。

例如,假设有一个简单的类 Person

@interface Person : NSObject
- (void)sayHello;
@end

@implementation Person
- (void)sayHello {
    NSLog(@"Hello!");
}
@end

[person sayHello] 这条消息发送时,运行时系统会找到 sayHello 方法对应的 IMP,这个 IMP 指向 -(void)sayHello 方法实现中的 NSLog(@"Hello!") 代码块。

3.2 IMP 与方法列表

每个类都维护着一个方法列表,这个列表中存储着该类及其父类的所有方法的 Selector 和 IMP 的对应关系。当对象接收到消息时,运行时系统首先在该对象所属类的方法列表中查找 Selector 对应的 IMP。如果在本类中没有找到,则会沿着继承链向父类的方法列表中查找,直到找到对应的 IMP 或者到达根类 NSObject

4. Selector 与 IMP 的关系

Selector 和 IMP 是紧密关联的,但又有明确的分工。Selector 主要用于标识方法,在消息发送时作为查找方法实现的索引;而 IMP 则实际指向方法的具体实现代码。

当对象接收到消息时,运行时系统首先根据消息中的方法名生成 Selector,然后在对象所属类的方法列表中查找该 Selector 对应的 IMP。找到 IMP 后,就调用该 IMP 所指向的函数来执行方法的具体逻辑。

例如,对于以下代码:

@interface Dog : NSObject
- (void)bark;
@end

@implementation Dog
- (void)bark {
    NSLog(@"Woof!");
}
@end

Dog *dog = [[Dog alloc] init];
SEL barkSelector = @selector(bark);
IMP barkIMP = [dog methodForSelector:barkSelector];
void (*func)(id, SEL) = (void (*)(id, SEL))barkIMP;
func(dog, barkSelector);

这里首先通过 @selector(bark) 获取到 bark 方法的 Selector,然后通过 [dog methodForSelector:barkSelector] 获取到该 Selector 对应的 IMP。接着将 IMP 转换为函数指针 func,最后通过 func 调用方法,实现了手动模拟消息发送的过程。

5. 在性能优化中的应用

5.1 减少动态方法解析开销

在运行时,当对象接收到一个它无法立即响应的消息时,会触发动态方法解析。这个过程涉及到在类的方法列表中查找、动态添加方法等操作,会带来一定的性能开销。

通过提前缓存 Selector 和 IMP 的对应关系,可以减少动态方法解析的次数。例如,在一个频繁调用某个方法的场景中,可以在类的初始化阶段就获取到该方法的 IMP 并缓存起来。

@interface MyClass : NSObject
- (void)frequentMethod;
@end

@implementation MyClass {
    IMP _frequentMethodIMP;
}

+ (void)initialize {
    if (self == [MyClass class]) {
        SEL selector = @selector(frequentMethod);
        _frequentMethodIMP = [self instanceMethodForSelector:selector];
    }
}

- (void)frequentMethod {
    // 方法实现
}

- (void)callFrequentMethod {
    void (*func)(id, SEL) = (void (*)(id, SEL))_frequentMethodIMP;
    func(self, @selector(frequentMethod));
}
@end

这样在每次调用 callFrequentMethod 时,直接通过缓存的 IMP 调用方法,避免了每次都通过运行时查找 IMP 的开销。

5.2 方法交换与性能监控

通过方法交换(Method Swizzling)技术,可以在运行时替换类的某个方法的实现。这在性能监控、日志记录等方面有广泛应用。

例如,要监控某个类的 viewDidLoad 方法的执行时间,可以进行如下操作:

#import <objc/runtime.h>

@interface UIViewController (PerformanceMonitoring)
@end

@implementation UIViewController (PerformanceMonitoring)
+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];
        
        SEL originalSelector = @selector(viewDidLoad);
        SEL swizzledSelector = @selector(performance_monitoring_viewDidLoad);
        
        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
        
        BOOL didAddMethod =
        class_addMethod(class,
                        originalSelector,
                        method_getImplementation(swizzledMethod),
                        method_getTypeEncoding(swizzledMethod));
        
        if (didAddMethod) {
            class_replaceMethod(class,
                                swizzledSelector,
                                method_getImplementation(originalMethod),
                                method_getTypeEncoding(originalMethod));
        } else {
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
    });
}

- (void)performance_monitoring_viewDidLoad {
    NSDate *startDate = [NSDate date];
    [self performance_monitoring_viewDidLoad];
    NSTimeInterval duration = -[startDate timeIntervalSinceNow];
    NSLog(@"viewDidLoad took %f seconds", duration);
}
@end

通过这种方法交换,在每次 viewDidLoad 方法执行前后记录时间,从而实现对该方法性能的监控。

5.3 避免不必要的消息转发

消息转发是运行时的另一个重要机制,当对象无法响应某个消息时,会进入消息转发流程。消息转发分为动态方法解析、备用接收者和完整的消息转发三个阶段,每个阶段都有一定的性能开销。

为了避免不必要的消息转发,可以在类中实现 respondsToSelector: 方法,提前判断对象是否能够响应某个消息。例如:

@interface MyObject : NSObject
@end

@implementation MyObject
- (BOOL)respondsToSelector:(SEL)aSelector {
    if (aSelector == @selector(someSpecialMethod)) {
        return YES;
    }
    return [super respondsToSelector:aSelector];
}
@end

这样在消息发送前,运行时系统会先调用 respondsToSelector: 方法,如果返回 NO,则直接进入消息转发流程;如果返回 YES,则继续查找 IMP 并执行方法。通过合理实现 respondsToSelector: 方法,可以减少不必要的消息转发,提高性能。

5.4 利用 IMP 进行直接函数调用

由于 IMP 是指向方法实现的函数指针,在某些性能敏感的场景中,可以直接通过 IMP 进行函数调用,而不是通过常规的消息发送机制。

例如,在一个循环中频繁调用某个方法:

@interface Calculator : NSObject
- (int)add:(int)a b:(int)b;
@end

@implementation Calculator
- (int)add:(int)a b:(int)b {
    return a + b;
}
@end

Calculator *calculator = [[Calculator alloc] init];
SEL addSelector = @selector(add:b:);
IMP addIMP = [calculator methodForSelector:addSelector];
int (*addFunc)(id, SEL, int, int) = (int (*)(id, SEL, int, int))addIMP;

for (int i = 0; i < 1000000; i++) {
    int result = addFunc(calculator, addSelector, i, i + 1);
    // 处理结果
}

这种直接通过 IMP 进行函数调用的方式,避免了消息发送过程中的一些额外开销,在性能上比常规的消息发送方式有一定提升。不过,这种方式需要小心使用,因为它绕过了运行时的一些特性,如动态方法解析和消息转发。

6. 实际案例分析

6.1 大型项目中的性能瓶颈优化

假设在一个大型的 iOS 应用中,有一个频繁使用的视图控制器类 MainViewController。该类的 viewWillAppear: 方法包含了大量的初始化和数据加载操作,随着功能的不断增加,这个方法的执行时间越来越长,成为了应用的性能瓶颈。

通过使用方法交换技术,我们可以在 viewWillAppear: 方法前后记录时间,找出具体耗时的操作。

@interface MainViewController (Performance)
@end

@implementation MainViewController (Performance)
+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];
        
        SEL originalSelector = @selector(viewWillAppear:);
        SEL swizzledSelector = @selector(performance_viewWillAppear:);
        
        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
        
        BOOL didAddMethod =
        class_addMethod(class,
                        originalSelector,
                        method_getImplementation(swizzledMethod),
                        method_getTypeEncoding(swizzledMethod));
        
        if (didAddMethod) {
            class_replaceMethod(class,
                                swizzledSelector,
                                method_getImplementation(originalMethod),
                                method_getTypeEncoding(originalMethod));
        } else {
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
    });
}

- (void)performance_viewWillAppear:(BOOL)animated {
    NSDate *startDate = [NSDate date];
    [self performance_viewWillAppear:animated];
    NSTimeInterval duration = -[startDate timeIntervalSinceNow];
    NSLog(@"viewWillAppear took %f seconds", duration);
}
@end

通过分析日志,发现其中一个数据加载操作 loadUserData 方法耗时较长。进一步优化该方法,比如采用异步加载或者缓存数据的方式,从而提高了 viewWillAppear: 方法的性能。

6.2 优化频繁调用的工具类方法

假设有一个工具类 StringUtils,其中有一个方法 stringByTrimmingWhitespace: 用于去除字符串两端的空白字符,这个方法在整个应用中被频繁调用。

为了提高性能,可以在类的初始化阶段缓存该方法的 IMP。

@interface StringUtils : NSObject
+ (NSString *)stringByTrimmingWhitespace:(NSString *)string;
@end

@implementation StringUtils {
    IMP _trimIMP;
}

+ (void)initialize {
    if (self == [StringUtils class]) {
        SEL selector = @selector(stringByTrimmingWhitespace:);
        _trimIMP = [self methodForSelector:selector];
    }
}

+ (NSString *)stringByTrimmingWhitespace:(NSString *)string {
    return [string stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
}

+ (NSString *)cachedTrimString:(NSString *)string {
    NSString *(*func)(id, SEL, NSString *) = (NSString *(*)(id, SEL, NSString *))_trimIMP;
    return func(self, @selector(stringByTrimmingWhitespace:), string);
}
@end

在需要调用去除空白字符的地方,使用 [StringUtils cachedTrimString:] 方法,通过缓存的 IMP 直接调用方法,减少了每次查找 IMP 的开销,提高了性能。

7. 注意事项

7.1 方法交换的线程安全

在进行方法交换时,由于涉及到全局的类结构修改,需要注意线程安全。通常使用 dispatch_once 来确保方法交换只执行一次,并且在多线程环境下不会出现竞争条件。

7.2 IMP 缓存的更新

当类的方法实现发生变化时(例如通过动态方法解析动态添加了方法),缓存的 IMP 可能需要更新。否则,可能会导致调用到旧的方法实现,出现逻辑错误。

7.3 避免过度优化

虽然通过 Selector 和 IMP 进行性能优化可以带来一定的性能提升,但过度优化可能会导致代码可读性和维护性下降。在进行性能优化之前,需要通过性能分析工具(如 Instruments)确定真正的性能瓶颈,有针对性地进行优化。

通过深入理解 Objective-C 运行时中的 Selector 和 IMP,并合理运用它们在性能优化中的特性,可以有效地提升应用的性能,打造更加流畅、高效的用户体验。无论是在小型项目还是大型企业级应用中,这些优化技巧都具有重要的价值。