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

Objective-C方法调配(Method Swizzling)实战技巧

2021-06-302.9k 阅读

一、Objective-C 方法调配基础概念

1.1 什么是方法调配

在 Objective-C 中,方法调配(Method Swizzling)是一种强大的技术,它允许我们在运行时动态地改变方法的实现。简单来说,我们可以将两个方法的实现进行交换,这样当调用原本的方法时,实际上执行的是被交换的方法的代码。

Objective-C 是一门动态语言,它的方法调用过程与静态语言有很大不同。在编译时,Objective-C 编译器只是记录下方法的调用,而实际的方法查找和执行是在运行时进行的。这就为方法调配提供了可能性。

1.2 方法调配的底层原理

Objective-C 的方法调用基于消息机制。当向一个对象发送消息时,运行时系统会在对象的类的方法列表中查找对应的方法实现。每个类都有一个 isa 指针,指向它的元类(meta - class),元类存储着类方法的列表。而实例对象的方法列表则存储在类中。

方法调配能够实现,关键在于运行时系统提供的一些函数,比如 class_getInstanceMethodmethod_exchangeImplementations 等。class_getInstanceMethod 用于获取类的实例方法,method_exchangeImplementations 则用于交换两个方法的实现。

具体过程如下:首先通过 class_getInstanceMethod 获取需要交换的两个方法的 Method 结构体。Method 结构体中包含了方法的名称、实现等信息。然后调用 method_exchangeImplementations 函数,将这两个 Method 结构体中的实现指针进行交换,从而达到方法调配的目的。

二、方法调配的常见应用场景

2.1 日志记录

在开发过程中,我们经常需要记录方法的调用信息,比如方法名、参数以及返回值等。通过方法调配,我们可以在不修改原有方法代码的基础上,为所有需要记录日志的方法添加日志记录功能。

假设我们有一个 ViewController 类,其中有一个 viewDidLoad 方法,我们希望在每次调用 viewDidLoad 方法时记录日志。

#import <objc/runtime.h>

@implementation ViewController

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];
        
        SEL originalSelector = @selector(viewDidLoad);
        SEL swizzledSelector = @selector(swizzled_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)swizzled_viewDidLoad {
    NSLog(@"开始调用 viewDidLoad 方法");
    [self swizzled_viewDidLoad];
    NSLog(@"结束调用 viewDidLoad 方法");
}

@end

在上述代码中,我们在 load 方法中使用了 dispatch_once 来确保方法调配只执行一次。通过 class_getInstanceMethod 获取 viewDidLoad 方法和我们自定义的 swizzled_viewDidLoad 方法。然后使用 class_addMethod 尝试向类中添加 viewDidLoad 方法,如果添加成功,则使用 class_replaceMethod 替换 swizzled_viewDidLoad 方法的实现;如果添加失败,说明 viewDidLoad 方法已经存在,直接使用 method_exchangeImplementations 交换两个方法的实现。

2.2 性能监测

方法调配还可以用于性能监测。我们可以在方法调用前后记录时间,从而计算方法的执行时间。

以一个网络请求方法为例,假设我们有一个 NetworkManager 类,其中有一个 sendRequest 方法。

#import <objc/runtime.h>

@implementation NetworkManager

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];
        
        SEL originalSelector = @selector(sendRequest);
        SEL swizzledSelector = @selector(swizzled_sendRequest);
        
        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)swizzled_sendRequest {
    NSDate *startDate = [NSDate date];
    [self swizzled_sendRequest];
    NSDate *endDate = [NSDate date];
    NSTimeInterval duration = [endDate timeIntervalSinceDate:startDate];
    NSLog(@"sendRequest 方法执行时间: %f 秒", duration);
}

@end

通过这种方式,我们可以轻松地监测 sendRequest 方法的执行时间,并且不需要在原方法中添加额外的代码,对原有代码的侵入性极小。

2.3 功能扩展

在一些情况下,我们可能需要为系统类或第三方库中的类添加一些额外的功能。例如,我们想为 UIImageView 类添加一个加载网络图片的功能。

#import <objc/runtime.h>
#import <UIKit/UIKit.h>

@interface UIImageView (NetworkImage)

- (void)loadImageFromURL:(NSURL *)url;

@end

@implementation UIImageView (NetworkImage)

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];
        
        SEL originalSelector = @selector(setImage:);
        SEL swizzledSelector = @selector(swizzled_setImage:);
        
        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)swizzled_setImage:(UIImage *)image {
    // 这里可以添加自定义逻辑,比如加载网络图片后再设置
    [self swizzled_setImage:image];
}

- (void)loadImageFromURL:(NSURL *)url {
    // 实际的网络图片加载逻辑
    NSData *imageData = [NSData dataWithContentsOfURL:url];
    UIImage *image = [UIImage imageWithData:imageData];
    [self setImage:image];
}

@end

通过这种方法调配,我们在不改变 UIImageView 原有 setImage: 方法逻辑的基础上,为其扩展了加载网络图片的功能。

三、方法调配的注意事项

3.1 线程安全

由于方法调配会修改类的方法列表,而在多线程环境下,可能会出现多个线程同时进行方法调配的情况,这可能导致数据竞争和未定义行为。因此,在进行方法调配时,一定要确保线程安全。

通常使用 dispatch_once 来保证方法调配代码只执行一次,这在前面的示例中已经有所体现。dispatch_once 会使用 GCD(Grand Central Dispatch)的机制来确保代码块在整个应用程序生命周期内只执行一次,并且是线程安全的。

3.2 避免递归调用

在进行方法调配时,很容易出现递归调用的问题。例如,在交换后的方法中又直接或间接地调用了原方法,这会导致无限循环,最终使应用程序崩溃。

比如,在前面日志记录的例子中,如果在 swizzled_viewDidLoad 方法中不小心写成了 [self viewDidLoad],就会导致递归调用。因为 viewDidLoad 方法已经被交换,再次调用 viewDidLoad 实际上还是会调用 swizzled_viewDidLoad,从而形成无限循环。

为了避免这种情况,一定要确保在交换后的方法中调用的是交换后的方法名,而不是原方法名。

3.3 影响继承体系

方法调配是在类的层面进行的,这意味着它会影响到该类及其所有子类。如果不小心使用方法调配,可能会对子类的行为产生意想不到的影响。

例如,假设我们在父类中进行了方法调配,子类可能会继承这种调配后的行为。如果子类有自己特殊的需求,与父类调配后的行为不兼容,就会出现问题。因此,在进行方法调配时,要充分考虑类的继承体系,尽量确保不会对继承链上的其他类造成负面影响。

3.4 与运行时版本的兼容性

Objective-C 的运行时系统在不同的操作系统版本中可能会有一些细微的变化。虽然方法调配的基本原理不变,但某些运行时函数的行为或参数可能会有所不同。

在使用方法调配时,要注意检查目标操作系统版本,确保代码在不同版本上都能正常工作。可以通过检查系统版本宏来进行条件编译,例如:

#if defined(__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0
// 针对 iOS 10 及以上版本的特殊处理
#elif defined(__IPHONE_9_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_9_0
// 针对 iOS 9 的特殊处理
#else
// 其他版本的通用处理
#endif

四、复杂场景下的方法调配实战

4.1 多级方法调配

在一些复杂的项目中,可能需要对多个方法进行调配,甚至是对已经调配过的方法再次进行调配。

假设我们有一个 Person 类,其中有 eat 方法和 drink 方法。我们先对 eat 方法进行调配,添加日志记录功能,然后再对调配后的 eat 方法进行二次调配,添加性能监测功能。

#import <objc/runtime.h>

@interface Person : NSObject

- (void)eat;
- (void)drink;

@end

@implementation Person

- (void)eat {
    NSLog(@"正在吃东西");
}

- (void)drink {
    NSLog(@"正在喝东西");
}

@end

@interface Person (Logging)

@end

@implementation Person (Logging)

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];
        
        SEL originalSelector = @selector(eat);
        SEL swizzledSelector = @selector(logged_eat);
        
        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)logged_eat {
    NSLog(@"开始调用 eat 方法");
    [self logged_eat];
    NSLog(@"结束调用 eat 方法");
}

@end

@interface Person (PerformanceMonitoring)

@end

@implementation Person (PerformanceMonitoring)

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];
        
        SEL originalSelector = @selector(logged_eat);
        SEL swizzledSelector = @selector(monitored_logged_eat);
        
        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)monitored_logged_eat {
    NSDate *startDate = [NSDate date];
    [self monitored_logged_eat];
    NSDate *endDate = [NSDate date];
    NSTimeInterval duration = [endDate timeIntervalSinceDate:startDate];
    NSLog(@"eat 方法执行时间: %f 秒", duration);
}

@end

在上述代码中,我们先在 Person (Logging) 分类中对 eat 方法进行调配,添加日志记录功能。然后在 Person (PerformanceMonitoring) 分类中对已经调配过的 logged_eat 方法再次进行调配,添加性能监测功能。

4.2 与其他运行时特性结合使用

方法调配可以与 Objective-C 的其他运行时特性,如关联对象(Associated Objects)结合使用,实现更复杂的功能。

假设我们有一个 Button 类,我们想为按钮添加一个点击次数统计功能,并且在每次点击时记录一些额外的信息。

#import <objc/runtime.h>
#import <UIKit/UIKit.h>

@interface UIButton (ClickCount)

@property (nonatomic, assign) NSInteger clickCount;

@end

@implementation UIButton (ClickCount)

static char kClickCountKey;

- (NSInteger)clickCount {
    return [objc_getAssociatedObject(self, &kClickCountKey) integerValue];
}

- (void)setClickCount:(NSInteger)clickCount {
    objc_setAssociatedObject(self, &kClickCountKey, @(clickCount), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];
        
        SEL originalSelector = @selector(sendAction:to:forEvent:);
        SEL swizzledSelector = @selector(swizzled_sendAction:to:forEvent:);
        
        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)swizzled_sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event {
    self.clickCount++;
    NSLog(@"按钮点击次数: %ld", (long)self.clickCount);
    // 这里可以记录更多额外信息,比如点击时间等
    NSDate *clickDate = [NSDate date];
    NSLog(@"点击时间: %@", clickDate);
    [self swizzled_sendAction:action to:target forEvent:event];
}

@end

在上述代码中,我们通过关联对象为 UIButton 添加了一个 clickCount 属性来统计点击次数。同时,通过方法调配,在按钮点击方法 sendAction:to:forEvent: 中实现了点击次数的统计和额外信息的记录。

4.3 处理方法重载和多态

在 Objective-C 中,虽然没有像 C++ 那样严格的方法重载概念,但可以通过不同参数类型来实现类似的效果。在进行方法调配时,需要注意处理这种情况。

假设我们有一个 MathCalculator 类,其中有两个 add 方法,一个接受两个整数参数,另一个接受两个浮点数参数。

#import <objc/runtime.h>

@interface MathCalculator : NSObject

- (int)add:(int)a b:(int)b;
- (float)add:(float)a b:(float)b;

@end

@implementation MathCalculator

- (int)add:(int)a b:(int)b {
    return a + b;
}

- (float)add:(float)a b:(float)b {
    return a + b;
}

@end

@interface MathCalculator (Logging)

@end

@implementation MathCalculator (Logging)

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];
        
        // 处理整数参数的 add 方法
        SEL originalSelector1 = @selector(add:b:);
        SEL swizzledSelector1 = @selector(logged_add_int:b:);
        
        Method originalMethod1 = class_getInstanceMethod(class, originalSelector1);
        Method swizzledMethod1 = class_getInstanceMethod(class, swizzledSelector1);
        
        BOOL didAddMethod1 =
        class_addMethod(class,
                        originalSelector1,
                        method_getImplementation(swizzledMethod1),
                        method_getTypeEncoding(swizzledMethod1));
        
        if (didAddMethod1) {
            class_replaceMethod(class,
                                swizzledSelector1,
                                method_getImplementation(originalMethod1),
                                method_getTypeEncoding(originalMethod1));
        } else {
            method_exchangeImplementations(originalMethod1, swizzledMethod1);
        }
        
        // 处理浮点数参数的 add 方法
        SEL originalSelector2 = @selector(add:b:);
        SEL swizzledSelector2 = @selector(logged_add_float:b:);
        
        Method originalMethod2 = class_getInstanceMethod(class, originalSelector2);
        Method swizzledMethod2 = class_getInstanceMethod(class, swizzledSelector2);
        
        BOOL didAddMethod2 =
        class_addMethod(class,
                        originalSelector2,
                        method_getImplementation(swizzledMethod2),
                        method_getTypeEncoding(swizzledMethod2));
        
        if (didAddMethod2) {
            class_replaceMethod(class,
                                swizzledSelector2,
                                method_getImplementation(originalMethod2),
                                method_getTypeEncoding(originalMethod2));
        } else {
            method_exchangeImplementations(originalMethod2, swizzledMethod2);
        }
    });
}

- (int)logged_add_int:(int)a b:(int)b {
    NSLog(@"调用整数加法方法,参数 a: %d, b: %d", a, b);
    int result = [self logged_add_int:a b:b];
    NSLog(@"整数加法结果: %d", result);
    return result;
}

- (float)logged_add_float:(float)a b:(float)b {
    NSLog(@"调用浮点数加法方法,参数 a: %f, b: %f", a, b);
    float result = [self logged_add_float:a b:b];
    NSLog(@"浮点数加法结果: %f", result);
    return result;
}

@end

在上述代码中,我们通过不同的 SEL 和不同的交换方法名来分别处理两个 add 方法的调配,从而实现对不同参数类型方法的日志记录功能。同时,在处理多态时,由于方法调配是基于类的,子类继承父类的调配行为时,会按照父类调配后的逻辑执行,但子类也可以重写方法并再次进行调配,以满足自身的需求。

通过以上对方法调配在复杂场景下的实战分析,我们可以看到方法调配在 Objective - C 开发中具有非常强大的能力,能够解决很多实际开发中的问题,但同时也需要我们谨慎使用,充分考虑各种可能出现的情况,以确保代码的稳定性和可靠性。