【iOS】源码学习-方法交换

【iOS】源码学习-方法交换

前言

方法交换就是传说中的"iOS黑魔法",其主要作用是在运行时将一个方法的实现替换成另一个方法的实现。

方法交换定义

在OC中就是利用方法交换实现AOP。每个类都维护一个方法列表methodList,方法列表中有不同的方法Method,每个方法中包含了方法的SEL和IMP。方法交换就是将SEL和IMP原本的对应断开,并将新SEL和新IMP生成对应关系。

OOP和AOP都是一种编程的思想:

  • OOP:面向对象编程。编程思想更加倾向于对业务模块的封装,划分出更加清晰的逻辑单元。
  • AOP:面向切面编程。是一种通过分离横切关注点来增强代码模块化的编程范式。它通过动态注入逻辑的方式,将与核心业务无关的通用功能(如日志、事务、权限控制等)从代码中解耦,从而提升可维护性和复用性。

方法交换API

通过SEL获取方法Method:

  • class_getInstanceMethod():获取实例方法
  • class_getClassMethod():获取类方法
  • method_getImplementation:获取一个方法的实现
  • method_setImplementation:设置一个方法的实现
  • method_getTypeEncoding:获取方法实现的编码类型:method_getTypeEncoding
  • class_addMethod:添加方法实现
  • class_replaceMethod:用一个方法的实现替换另一个方法的实现,即aIMP指向bIMP,但是bIMP不一定指向aIMP
  • method_exchangeImplementations:交换两个方法的实现,即aIMP指向bIMP,但是bIMP也指向aIMP

方法交换坑点

使用一次性问题

method-swizzling必须在+load方法中执行(这保证了在任何对象调用该方法之前,交换已完成),而load反复调用多次,这样会导致方法的重复交换,使方法SEL的指向又恢复为原来的IMP。可以通过在dispatch_once中实现单例使方法交换只执行一次。

objc 复制代码
+ (void)load{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [LGRuntimeTool lg_bestMethodSwizzlingWithClass:self oriSEL:@selector(helloword) swizzledSEL:@selector(lg_studentInstanceMethod)];
    });
}

使用dispatch_once是为了确保交换逻辑的线程安全和幂等性(不因重复执行而出问题,即同一个操作无论执行多少次最终效果完全一样),即使在多线程环境下也只执行一次。

子类没有实现,父类实现

在子类分类中交换一个子类未实现、父类实现的实例方法,使用method_exchangeImplementations会导致父类实例调用时崩溃。

父类LYDPerson:

objc 复制代码
@interface LYDPerson : NSObject

- (void)personInstanceMethod;

@end
  
@implementation LYDPerson

- (void)personInstanceMethod {
    NSLog(@"person对象方法:%s", __func__);
}

子类LYDStudent:什么都没有实现

分类LYD:

objc 复制代码
@interface LYDStudent (LYD)

- (void)lyd_studentInstanceMethod;

@end
  
@implementation LYDStudent (LYD)

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [LYDRuntimeTool lyd_methodSwizzlingWithClass:self originSEL:@selector(personInstanceMethod) swizzledSEL:@selector(lyd_studentInstanceMethod)];
    });
}

- (void)lyd_studentInstanceMethod {
    [self lyd_studentInstanceMethod];
    NSLog(@"LYDStudent分类lyd_studentInstanceMethod");
}

@end

方法交换:

objc 复制代码
+ (void)lyd_methodSwizzlingWithClass:(Class)cls originSEL:(SEL)originSEL swizzledSEL:(SEL)swizzledSEL {
    if (!cls) {
        return;
    }
    // cls=LYDStudent originSEL=personInstanceMethod
    // 子类没有实现这个方法,class_getInstanceMethod沿着继承链向上找,最终拿到父类LYDPerson里的这个方法,而不是子类自己的
    // 因此LYDStudent的personInstanceMethod和LYDStudent分类的lyd_studentInstanceMethod方法交换实际上是LYDPerson的personInstanceMethod和LYDStudent分类的lyd_studentInstanceMethod方法交换
    Method originMethod = class_getInstanceMethod(cls, originSEL);
    Method swizzledMethod = class_getInstanceMethod(cls, swizzledSEL);
    method_exchangeImplementations(originMethod, swizzledMethod);
}

运行结果:

从日志我们可以看出崩溃的原因是子类LYDStudent本身没有personInstanceMethod方法,于是会向上查找父类LYDPerson的Method。然后执行method_exchangeImplementations会把LYDPerson里的personInstanceMethod IMP和LYDStudent分类里的lyd_studentInstanceMethod IMP互换。这样父类LYDPerson的personInstanceMethod指向子类分类的实现,而父类本身没有lyd_studentInstanceMethod。因此p调用personInstanceMethod时,p走到子类分类的方法,父类找不到该方法,找不到IMP,消息未识别,直接崩溃。

解决办法:优先使用class_addMethod尝试给当前子类添加原方法

子类没有实现,父类也没有实现

目标方法在整个继承链中完全无实现,class_getInstanceMethod返回nil,method_exchangeImplementations遇到nil参数,直接放弃执行,即没有交换。而lg_studentInstanceMethod依旧指向自己原本的代码,方法实现里又调用自己,进而陷入死循环。

解决办法:增加前置判空

先判断原方法是否为空,为空则手动给原方法补实现,并把交换方法改成空Block,彻底切断自调用。

方法依赖_cmd

_cmd:关键字,类型是SEL,每个OC方法天生自带的隐藏参数。

OC实例方法底层本质是C函数:void 方法底层函数(id self, SEL _cmd, 其他参数...)

  • self:当前调用的对象
  • _cmd:本次调用使用的方法名SEL
objc 复制代码
@implementation LYDPerson

- (void)run {
    SEL currentSel = _cmd;
    NSString *selName = NSStringFromSelector(currentSel);
    if ([selName isEqualToString:@"run"]) {
        NSLog(@"调用run");
    } else {
        NSLog(@"没调用");
    }
}

- (void)lyd_run {
    NSLog(@"原生lyd_run方法");
}

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class cls = [self class];
        Method runMethod = class_getInstanceMethod(cls, @selector(run));
        Method lydRunMethod = class_getInstanceMethod(cls, @selector(lyd_run));
        method_exchangeImplementations(runMethod, lydRunMethod);
    });
}

@end

交换后,调用方法run,run指向了lyd_run的代码,直接打印原生lyd_run方法。调用方法lyd_run,指向了run的代码,进入run函数体,函数内_cmd为当前调用名lyd_run,因此走else分支,输出没调用。

方法逻辑依赖_cmd做判断,一旦方法交换,判断实效,原有业务逻辑被破坏。

类方法和实例方法的交换方法原理类似,唯一的区别是类方法存在元类中。在调用class_addMethod和class_replaceMethod方法添加和替换时,需要传入的类是元类。可以通过object_getClass方法获取类的元类。

方法交换应用

方法交换最常用的应用是防止数组、字典等越界崩溃

在iOS中NSNumber、NSArray、NSDictionary等这些类都是类族,一个NSArray的实现可能由多个类组成。所以如果想对NSArray进行方法交换,必须取到真身进行,直接对NSArray进行操作是无效的。

objc 复制代码
#import "NSArray+SafeCrash.h"
#import <objc/runtime.h>

@implementation NSArray (SafeCrash)

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        NSLog(@"load方法");
        Method firstMethod = class_getInstanceMethod(objc_getClass("NSConstantArray"), @selector(objectAtIndex:));
        Method secondMethod = class_getInstanceMethod(objc_getClass("NSConstantArray"), @selector(new_objectAtIndex:));
            
        method_exchangeImplementations(firstMethod, secondMethod);
    });
}

- (id)new_objectAtIndex:(NSUInteger)index {
    if (index >= self.count) {
        NSLog(@"数组越界:下标%lu,数组总数%lu", (unsigned long)index, (unsigned long)self.count);
        return nil;
    } else {
        // 交换后,调用new_objectAtIndex等价于调用原生objectAtIndex
        return [self new_objectAtIndex:index];
    }
}

@end

这样无论有没有越界访问,都不会报错,正常输出。

总结

方法交换时机由dyld决定,dyld负责把程序和库加载进内存并触发+load,Runtime负责真正交换方法IMP。因此,方法交换必须写在+load里,由dyld触发,保证在第一次使用类之前完成交换,并且使用dispatch_once保证方法交换只执行一次,避免重复交换逻辑错乱。

相关推荐
飞翔中文网1 小时前
Java学习笔记之泛型
java·笔记·学习
li星野1 小时前
RAG优化系列:自适应检索(Adaptive Retrieval)——让系统智能选择是否检索
人工智能·python·学习
喜欢踢足球的老罗10 小时前
从移动开发转型 AI Agent 工程师:我做了一个开源学习系统
人工智能·学习
wuxinyan12311 小时前
工业级大模型学习之路030:Streamlit 企业级智能体前端工作台
前端·学习·streamlit·智能体
长安紫薯12 小时前
学习AI日记
学习
星恒随风12 小时前
C语言数据结构排序算法详解(下):冒泡排序、快速排序、归并排序和计数排序
c语言·数据结构·笔记·学习·排序算法
NagatoYukee12 小时前
Spring Security基础部分学习
java·学习·spring
米小葱13 小时前
【学习笔记】cmake
笔记·学习
hurrycry_小亦13 小时前
苹果WWDC 2026前瞻:Ferret-Pro端侧大模型即将亮相|小亦之闻|AI 编程三日速递!(5月26日~5月28日)
macos·ios·wwdc