【iOS】 方法交换

【iOS】 方法交换 method-swizzling

前言

之前看过有关于消息转发的内容,这里我们可以简单看一下有关于iOS的黑魔法,方法交换

什么是method-swizzling

method-swizzling这个的意思就是我们可以在运行的时候,将方法的实现替换成另一个方法的一个实现.

在OC中这个原理是为了实现AOP的编程思想,

其中AOP(Aspect Oriented Programming,面向切面编程)是一种编程的思想,区别于OOP(面向对象编程)

面向切面编程(Aspect-Oriented Programming,AOP)是一种通过分离横切关注点(Cross-Cutting Concerns)来增强代码模块化的编程范式。它通过动态注入逻辑的方式,将与核心业务无关的通用功能(如日志、事务、权限控制等)从代码中解耦,从而提升可维护性和复用性.

应用场景:

  1. 日志记录:自动记录方法调用参数、执行时间,无需在每个方法中手动写入日志代码
  2. 权限控制:在方法调用前检查用户角色,拦截无权限的请求
  3. 性能监控:统计方法耗时、数据库查询次数,定位性能瓶颈
  • 每个类都维护着一个方法列表 ,即methodListmethodList中有不同的方法Method,每个方法中包含了方法的selIMP,方法交换就是将sel和imp原本的对应断开,并将sel和新的IMP生成对应关系

方法交换原理

相关API

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

方法交换的风险

在load方法中保证只加载一次

所谓的一次性就是:mehod-swizzling写在load方法中,而load方法会主动调用多次,这样会导致方法的重复交换,使方法sel的指向又恢复成原来的imp的问题

解决方法采用GCD保证只加载一次:

+ (void)load{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [LGRuntimeTool lg_bestMethodSwizzlingWithClass:self oriSEL:@selector(helloword) swizzledSEL:@selector(lg_studentInstanceMethod)];
    });
}
要在当前类的方法中进行交换

被交换的方法必须是当前类的方法,不能是父类的方法,直接把父类的实现拷贝过来不会起作用。父类的方法必须在调用的时候使用,而不是方法交换时使用。方法交换只能作用于当前类的方法,不能影响父类的方法。

我先看一下结果实例:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        //ISA_MASK  0x00007ffffffffff8ULL
        CJLPerson *person = [CJLPerson alloc];
        CJLTeacher* teacher = [CJLTeacher alloc];
        [teacher sayBye];
        [person sayBye];        
    }
    return 0;
}
//方法交换:
+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        NSLog(@"123456");
        [self nanxun_MethodSwizzlingWithClass:self.class oriSEL:@selector(sayBye) swizzledSEL:@selector(sayNO)];
    });
}
- (void)sayNO{
    //是否会产生递归?--不会产生递归,原因是sayNO 会走 oriIMP,即sayBye的实现中去
    [self sayNO];
    NSLog(@"CJLTeacher分类添加的对象方法:%s",__func__);
}

打印结果:

image-20250504155225966

这里的报错也就是发现我们在这个CJLPerson类中没有找到对应的方法,因为我相当于把子类的方法交换到了父类中,父类的方法列表中找不到子类的方法,但是子类可以找到对应的方法,所以问题就是子类不可以和父类交换方法,会导致父类的方法出现问题.

如果要进行交换可以采用下面的方式

通过class_addMethod尝试添加你要交换的方法:

+ (void)nanxun_MethodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL{
    
    if (!cls) NSLog(@"传入的交换类不能为空");
    
    Method oriMethod = class_getInstanceMethod(cls, oriSEL);
    Method swiMethod = class_getInstanceMethod(cls, swizzledSEL);
   
    // 一般交换方法: 交换自己有的方法 -- 走下面 因为自己有意味添加方法失败
    // 交换自己没有实现的方法:
    //   首先第一步:会先尝试给自己添加要交换的方法 :personInstanceMethod (SEL) -> swiMethod(IMP)
    //   然后再将父类的IMP给swizzle  personInstanceMethod(imp) -> swizzledSEL 

    BOOL success = class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(oriMethod)); // 给原先的SEL添加新方法的method

    if (success) {// 自己没有 - 交换 - 没有父类进行处理 (重写一个)
        class_replaceMethod(cls, swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod)); 
    } else { // 自己有
        method_exchangeImplementations(oriMethod, swiMethod);
    }   
}
//但是这样还不能保证如果两个方法为空不会报错

//封装的method-swizzling方法
+ (void)lg_bestClassMethodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL{
    
    if (!cls) NSLog(@"传入的交换类不能为空");

    Method oriMethod = class_getClassMethod([cls class], oriSEL);
    Method swiMethod = class_getClassMethod([cls class], swizzledSEL);
    
    if (!oriMethod) { // 避免动作没有意义
        // 在oriMethod为nil时,替换后将swizzledSEL复制一个不做任何事的空实现,代码如下:
        class_addMethod(object_getClass(cls), oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
        method_setImplementation(swiMethod, imp_implementationWithBlock(^(id self, SEL _cmd){
            NSLog(@"来了一个空的 imp");
        }));
    }
    
    // 一般交换方法: 交换自己有的方法 -- 走下面 因为自己有意味添加方法失败
    // 交换自己没有实现的方法:
    //   首先第一步:会先尝试给自己添加要交换的方法 :personInstanceMethod (SEL) -> swiMethod(IMP)
    //   然后再将父类的IMP给swizzle  personInstanceMethod(imp) -> swizzledSEL
    //oriSEL:personInstanceMethod

    BOOL didAddMethod = class_addMethod(object_getClass(cls), oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
    
    if (didAddMethod) {
        class_replaceMethod(object_getClass(cls), swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
    }else{
        method_exchangeImplementations(oriMethod, swiMethod);
    }
    
}

如果两个方法都没有实现会进入无限递归也就是无限循环,导致我们的一个栈溢出:

原因是 栈溢出递归死循环了,那么为什么会发生递归呢?----主要是因为 父类方法没有实现,然后在方法交换时,始终都找不到oriMethod,然后交换了寂寞,即交换失败,当我们调用父类的(oriMethod)时,也就是oriMethod会进入LG中子类的方法,然后这个方法中又调用了自己,此时的子类方法并没有指向oriMethod ,然后导致了自己调自己,即递归死循环

如果方法依赖于cmd

如果方法依赖于cmd,交换之后,cmd就会发生改变会出现很多奇奇怪怪的问题.

方法交换的应用

这里我们可以把它运用解决我们的数组越界的一个问题

在iOS中NSNumberNSArrayNSDictionary等这些类都是类簇,一个NSArray的实现可能由多个类组成。所以如果想对NSArray进行Swizzling,必须获取到其“真身”进行Swizzling,直接对NSArray进行操作是无效的

类名真身
NSArray__NSArrayI
NSMutableArray__NSArrayM
NSDictionary__NSDictionaryI
NSMutableDictionary__NSDictionaryM
//
//  NSArray+crash.m
//  GGDebug
//
//  Created by nanxun on 2025/5/4.
//

#import "NSArray+crash.h"
#import "objc/objc-runtime.h"
@implementation NSArray (crash)
+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        NSLog(@"2222");
        Method fromMethod = class_getInstanceMethod(objc_getClass("NSConstantArray"), @selector(objectAtIndex:));
        Method toMethod = class_getInstanceMethod(objc_getClass("NSConstantArray"), @selector(new_objectAtIndex:));
            
        method_exchangeImplementations(fromMethod, toMethod);
    });
    
}
- (id)new_objectAtIndex:(NSUInteger)index {
    NSLog(@"123");
    if (index >= self.count) {
            // 越界处理
        
        NSLog(@"Index %lu out of bounds, array count is %lu.", (unsigned long)index, (unsigned long)self.count);
        return nil;
    } else {
        // 正常访问,注意这里调用的是替换后的方法,因为实现已经交换
        return [self new_objectAtIndex:index];
    }
    
}
@end

image-20250504161658842

这里的NSArray类型是NSConstantArray,这里我们已经越界了但是程序还没有退出,但是我们提供了一个报错,保证了这个函数的安全.

这里不可以用字面量的方式访问,不然会出现bug.

如果要修改字面量下标,字面量下标我们要修改这个方法objectAtIndexedSubscript

- (id)new_objectAtIndexedSubscript:(NSUInteger)idx {
    if (idx >= self.count) {
        // 越界处理
        NSLog(@"Index %lu out of bounds, array count is %lu.", (unsigned long)idx, (unsigned long)self.count);
        return nil;
    } else {
        // 正常访问,注意这里调用的是替换后的方法,因为实现已经交换
        return [self new_objectAtIndexedSubscript:idx];
    }
}

这样就可以保证他的一个安全:

image-20250504162512164

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值