【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

objc 复制代码
//通过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保证只加载一次:

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

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

我先看一下结果实例:

objc 复制代码
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__);
}

打印结果:

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

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

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

objc 复制代码
+ (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
objc 复制代码
//
//  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

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

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

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

objc 复制代码
- (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];
    }
}

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

相关推荐
酷虎软件3 小时前
分享链接+视频音频文案提取 API 接口文档
ide·macos·xcode
Hyyy8 小时前
为什么 macOS 应用一换 Bundle ID,之前授予的权限就全失效了?
macos·electron
CocoaKier8 小时前
Facebook登录的这条红色警告,我终于搞明白了,真是反人类设计!
ios·facebook
p似笑非笑9 小时前
实战验证——把 SDK 塞进一个 macOS 原生 Agent 应用
macos
humors22112 小时前
【转帖】安全企业发布iOS漏洞攻击风险检测工具
安全·ios·手机·苹果
p似笑非笑13 小时前
在Mac上完美配置VSCode的C/C++开发环境(GCC/G++详细教程)
c语言·vscode·macos
沐风___15 小时前
iOS 崩溃收集实战:从「只看到次数、看不到原因」到 Sentry 完整集成
ios·sentry
listening77716 小时前
HarmonyOS 6.1 跨端实战:用ArkUI-X把电商App同时跑在Android/iOS上
android·ios·harmonyos
韦胖漫谈IT17 小时前
Apple M3 Max 与 Apple M5 Max 对比:本地算力的新旧王者之争
网络·人工智能·macos·transformer
Tyler_117 小时前
iOS PlayWright mcp server
前端·ios·ai编程