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

Objective-C方法替换(Method Swizzling)核心语法实现

2022-09-285.0k 阅读

一、Method Swizzling 基础概念

在 Objective-C 编程中,Method Swizzling 是一种强大的技术,它允许我们在运行时动态地改变一个类的方法实现。简单来说,就是可以将类中某个方法的实现替换成另一个方法的实现。这一特性在很多场景下都非常有用,比如在不改变原有代码结构的情况下,为系统类添加额外的功能,或者对现有方法进行调试、统计等。

Objective-C 是一门动态语言,其方法调用在运行时才会确定具体要执行的代码。这得益于 Objective-C 的消息机制。当向一个对象发送消息时,运行时系统会在该对象所属类的方法列表中查找对应的方法实现。Method Swizzling 正是利用了这一动态特性,在运行时修改类的方法列表,从而改变方法的实际执行逻辑。

二、相关底层结构剖析

(一)Method 结构体

在 Objective-C 的底层实现中,Method 是一个结构体,定义在 objc/runtime.h 头文件中。它包含了方法的名称、方法的类型编码以及方法的实现等重要信息。其定义如下:

typedef struct method_t *Method;

struct method_t {
    SEL name;
    const char *types;
    IMP imp;
    struct SortBySELAddress :
        public std::binary_function<const method_t&,
                                    const method_t&, bool>
    {
        bool operator() (const method_t& lhs,
                         const method_t& rhs)
        { return lhs.name < rhs.name; }
    };
};
  • SEL(Selector):方法选择器,本质是一个 const char * 类型的字符串,用于唯一标识一个方法。不同类中相同名字的方法会有相同的 SEL
  • types:方法的类型编码,它描述了方法的参数类型和返回值类型。通过这些编码,运行时系统可以在调用方法时正确地处理参数和返回值。
  • IMP(Implementation):方法的具体实现,是一个指向函数的指针,这个函数的结构与方法的参数和返回值相匹配。

(二)类的方法列表

每个类都维护着一个方法列表,用于存储该类所拥有的方法。在 objc_class 结构体(同样定义在 objc/runtime.h 中)中,有一个字段 methodLists 与方法列表相关。虽然实际结构较为复杂,但简单理解就是类通过这个结构来管理自己的方法。当我们向一个对象发送消息时,运行时系统会从这个方法列表中查找对应的 Method 结构体,进而找到方法的实现 IMP 来执行。

三、Method Swizzling 的核心实现步骤

  1. 获取类的实例方法:使用 class_getInstanceMethod 函数可以获取类的实例方法。该函数接受两个参数,第一个是类对象,第二个是方法选择器 SEL。例如,要获取 UIViewController 类的 viewDidLoad 方法,可以这样写:
Method originalMethod = class_getInstanceMethod([UIViewController class], @selector(viewDidLoad));
  1. 获取替换方法:同样使用 class_getInstanceMethod 来获取我们想要用于替换的方法。假设我们自定义了一个 swizzled_viewDidLoad 方法,获取它的方式如下:
Method swizzledMethod = class_getInstanceMethod([UIViewController class], @selector(swizzled_viewDidLoad));
  1. 方法交换:使用 method_exchangeImplementations 函数来交换两个方法的实现。该函数接受两个 Method 结构体作为参数,将它们的 IMP 进行交换。代码如下:
method_exchangeImplementations(originalMethod, swizzledMethod);

完成上述步骤后,当调用 UIViewControllerviewDidLoad 方法时,实际执行的就是 swizzled_viewDidLoad 方法的代码,而调用 swizzled_viewDidLoad 方法时,执行的则是原来 viewDidLoad 方法的代码。

四、代码示例详解

(一)简单的方法替换示例

假设我们有一个自定义的 Person 类,包含一个 sayHello 方法,我们想要将其替换为一个新的实现。

  1. 首先定义 Person 类及其方法:
#import <Foundation/Foundation.h>

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

@implementation Person
- (void)sayHello {
    NSLog(@"Hello, I'm a person.");
}
@end
  1. 然后进行方法替换:
#import <objc/runtime.h>
#import "Person.h"

@interface Person (Swizzling)
- (void)swizzled_sayHello;
@end

@implementation Person (Swizzling)
- (void)swizzled_sayHello {
    // 可以在这里添加额外的逻辑
    NSLog(@"Before swizzled sayHello.");
    // 调用原方法,由于方法实现已经交换,这里实际调用的是原 sayHello 方法
    [self swizzled_sayHello];
    NSLog(@"After swizzled sayHello.");
}
@end

static void load() {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [Person class];
        
        Method originalMethod = class_getInstanceMethod(class, @selector(sayHello));
        Method swizzledMethod = class_getInstanceMethod(class, @selector(swizzled_sayHello));
        
        BOOL didAddMethod =
        class_addMethod(class,
                        @selector(sayHello),
                        method_getImplementation(swizzledMethod),
                        method_getTypeEncoding(swizzledMethod));
        
        if (didAddMethod) {
            class_replaceMethod(class,
                                @selector(swizzled_sayHello),
                                method_getImplementation(originalMethod),
                                method_getTypeEncoding(originalMethod));
        } else {
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
    });
}

在上述代码中,我们定义了一个 Person 类的分类 Person (Swizzling),并在其中定义了 swizzled_sayHello 方法。在 load 方法中(load 方法会在类加载到内存时自动调用一次),我们使用 dispatch_once 确保方法替换只执行一次。

这里有一个需要注意的点,就是在进行方法替换时,先尝试使用 class_addMethod 添加方法。如果添加成功,说明类中原本没有 sayHello 方法(这种情况很少见,但为了代码的完整性需要处理),此时需要使用 class_replaceMethod 来替换 swizzled_sayHello 方法的实现为原 sayHello 方法的实现。如果 class_addMethod 添加失败,说明类中已经有 sayHello 方法,直接使用 method_exchangeImplementations 交换两个方法的实现。

  1. 最后在 main 函数中测试:
#import <Foundation/Foundation.h>
#import "Person.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *person = [[Person alloc] init];
        [person sayHello];
    }
    return 0;
}

运行上述代码,会输出:

Before swizzled sayHello.
Hello, I'm a person.
After swizzled sayHello.

可以看到,sayHello 方法的实现已经被成功替换,并且在新的实现中,我们成功添加了额外的逻辑。

(二)对系统类的方法替换

UIViewControllerviewDidLoad 方法为例,假设我们想要在所有 UIViewController 子类的 viewDidLoad 方法中添加一些统计代码,统计每个视图控制器加载的次数。

  1. 首先创建一个分类:
#import <UIKit/UIKit.h>

@interface UIViewController (Swizzling)
@end

@implementation UIViewController (Swizzling)
+ (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 {
    static NSMutableDictionary<NSString *, NSNumber *> *loadCountDict;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        loadCountDict = [NSMutableDictionary dictionary];
    });
    
    NSString *className = NSStringFromClass([self class]);
    NSNumber *count = loadCountDict[className];
    if (!count) {
        count = @(1);
    } else {
        NSInteger newCount = count.integerValue + 1;
        count = @(newCount);
    }
    loadCountDict[className] = count;
    
    NSLog(@"View controller %@ has been loaded %ld times.", className, (long)count.integerValue);
    
    // 调用原方法
    [self swizzled_viewDidLoad];
}
@end

在这个分类的 load 方法中,我们同样进行了方法替换的操作。在 swizzled_viewDidLoad 方法中,我们实现了统计视图控制器加载次数的逻辑,并在最后调用原 viewDidLoad 方法,保证原有功能不受影响。

  1. 当在项目中使用 UIViewController 及其子类时,每次调用 viewDidLoad 方法,都会输出相应的加载次数统计信息。例如,假设有一个 MyViewController 继承自 UIViewController
#import "MyViewController.h"

@implementation MyViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    // 这里是 MyViewController 自己的 viewDidLoad 逻辑
}
@end

MyViewController 的实例加载时,会输出类似如下的日志:

View controller MyViewController has been loaded 1 times.

再次加载时,会输出:

View controller MyViewController has been loaded 2 times.

通过这种方式,我们在不改变原有视图控制器代码的基础上,成功为所有 UIViewController 子类添加了统计功能。

五、Method Swizzling 的注意事项

  1. 线程安全:由于 Method Swizzling 是在运行时改变类的方法列表,而这个操作不是线程安全的。因此,在进行方法替换时,一定要使用 dispatch_once 等机制确保替换操作只在一个线程中执行一次,避免在多线程环境下出现竞争条件导致程序崩溃或出现不可预期的行为。
  2. 避免递归调用:在替换后的方法实现中,一定要注意避免递归调用。如前面的示例中,在 swizzled_sayHello 方法中调用原 sayHello 方法时,由于方法实现已经交换,所以要调用 [self swizzled_sayHello] 来执行原方法,而不是直接调用 [self sayHello],否则会导致无限递归,最终使程序崩溃。
  3. 对继承体系的影响:Method Swizzling 是针对类进行的操作,会影响该类及其所有子类。在对系统类进行方法替换时,要谨慎考虑对整个继承体系的影响,避免破坏系统的原有行为或导致其他未预期的问题。
  4. 与其他框架的兼容性:如果项目中使用了其他框架,Method Swizzling 可能会与这些框架产生兼容性问题。例如,某些框架可能也对相同的类进行了方法替换,此时可能需要协调两者的操作,以确保程序的正确性。

六、Method Swizzling 的应用场景

  1. 日志记录与调试:可以在不修改原有业务代码的情况下,为方法添加日志记录功能,方便调试和追踪方法的调用流程。例如,为 NSURLSession 的相关方法添加日志,记录每次网络请求的参数和返回结果。
  2. 性能统计:统计方法的执行时间,找出性能瓶颈。比如统计 UITableViewcellForRowAtIndexPath: 方法的执行时间,优化列表的渲染性能。
  3. 功能增强:为系统类或第三方库中的类添加额外的功能。如为 UIImageView 添加加载图片时的占位图显示、加载失败提示等功能。
  4. AOP(面向切面编程):实现类似于 AOP 的功能,将一些通用的功能(如权限验证、数据统计等)从业务逻辑中分离出来,通过 Method Swizzling 统一添加到相关方法中,提高代码的可维护性和复用性。

七、深入理解 Method Swizzling 的原理

Method Swizzling 之所以能够实现,核心在于 Objective-C 的动态特性和运行时机制。当我们调用一个对象的方法时,运行时系统会按照以下步骤进行处理:

  1. 发送消息:代码中向对象发送消息,例如 [object someMethod]。这里的 someMethod 实际上是一个 SEL
  2. 查找方法:运行时系统首先在对象所属类的缓存中查找是否有对应的 Method。如果缓存中没有找到,则在类的方法列表中查找。如果在本类中没有找到,会继续在父类的方法列表中查找,直到找到或者到达继承体系的顶端(NSObject 类)。
  3. 执行方法:找到对应的 Method 后,获取其 IMP,即方法的实际实现函数指针,然后调用该函数执行方法的代码。

Method Swizzling 正是在运行时修改了类的方法列表中的 Method 结构体的 IMP 指针。通过交换两个 MethodIMP,使得原本调用一个方法时,实际执行的是另一个方法的代码。

八、不同场景下的 Method Swizzling 优化

  1. 针对大量类的方法替换:如果需要对大量类进行方法替换,每次都使用 class_getInstanceMethodmethod_exchangeImplementations 可能会带来性能开销。可以考虑批量处理,例如将需要替换的类和方法信息存储在一个数组或字典中,然后一次性进行替换操作。同时,可以对 class_getInstanceMethod 的结果进行缓存,避免重复获取。
  2. 与 KVO(Key - Value Observing)结合:在某些情况下,Method Swizzling 可以与 KVO 结合使用。比如在监听某个对象的属性变化时,通过 Method Swizzling 可以在属性变化的相关方法中添加额外的逻辑,而不仅仅依赖于 KVO 的回调。这样可以实现更复杂的业务逻辑,同时利用两者的优势。
  3. 内存管理优化:在进行方法替换时,要注意内存管理。特别是在替换方法中涉及到对象的创建和释放时,要遵循正确的内存管理规则。如果使用 ARC(自动引用计数),确保对象的生命周期得到正确管理;如果使用 MRC(手动引用计数),要正确调用 retainreleaseautorelease 等方法。

九、Method Swizzling 在不同版本系统中的兼容性

随着 iOS 系统的不断更新,Objective-C 的运行时机制可能会有一些细微的变化。虽然 Method Swizzling 的基本原理保持不变,但在不同版本系统中可能需要注意一些兼容性问题。

  1. 新特性与 API 变化:新系统版本可能引入新的类或方法,在进行方法替换时要确保不会与这些新特性冲突。例如,在 iOS 13 中,UIViewController 新增了一些与暗黑模式相关的方法,如果对 UIViewController 进行方法替换,要注意避免影响这些新方法的正常使用。
  2. 运行时行为调整:苹果可能会对运行时的一些行为进行微调。虽然 Method Swizzling 的核心函数(如 class_getInstanceMethodmethod_exchangeImplementations 等)依然可用,但在某些边缘情况下,其行为可能与之前版本略有不同。在开发针对不同系统版本的应用时,要进行充分的测试,确保 Method Swizzling 在各个版本上都能正常工作。

十、Method Swizzling 与其他动态特性的对比

  1. 与 Category(分类)对比:Category 主要用于为现有类添加新的方法,但不能添加实例变量。它在编译时就确定了要添加的方法,并且不能覆盖原有类中同名方法(除非在运行时使用 Method Swizzling 等手段)。而 Method Swizzling 可以在运行时改变现有方法的实现,更加灵活。例如,Category 适合为系统类添加一些通用的扩展功能,而 Method Swizzling 更适合在不改变原有代码结构的前提下对现有方法进行修改。
  2. 与 Protocol(协议)对比:Protocol 定义了一组方法的声明,但不提供方法的实现,主要用于实现多继承的效果。类通过遵守协议来表明自己具备某些行为。与 Method Swizzling 不同,Protocol 侧重于定义行为规范,而 Method Swizzling 侧重于改变现有方法的实际执行逻辑。例如,一个类遵守 NSCopying 协议来实现复制功能,而 Method Swizzling 可以在运行时改变类中某个方法的实现,使其在复制时执行额外的逻辑。

通过深入理解 Method Swizzling 的核心语法实现、注意事项、应用场景以及与其他动态特性的对比,开发者可以更加灵活和安全地在 Objective-C 项目中使用这一强大的技术,为项目带来更多的可能性和优化空间。无论是在功能增强、调试还是性能优化方面,Method Swizzling 都有着独特的价值,是 Objective-C 开发者必备的技能之一。在实际应用中,要根据项目的具体需求和特点,合理使用 Method Swizzling,确保代码的健壮性和可维护性。