iOS-底层原理18:Method-Swizzling 方法交换

method-swizzling 是什么?

Method Swizzling本质上就是对方法的IMPSEL进行交换,也是我们常说的黑魔法

方法交换的原理

  • Method Swizzing是发生在运行时的,在运行时将一个方法的实现替换成另一个方法的实现;

  • 每个类都维护着一个方法列表,即methodListmethodList中有不同的方法,每个方法中包含了方法的SELIMP,方法交换就是将原本的SEL和IMP对应断开,并将SEL和新的IMP生成对应关系

  • Method Swizzling也是iOS中AOP(面相切面编程)的一种实现方式,我们可以利用苹果这一特性来实现AOP编程。

method-swizzling涉及的相关API

方法名 作用
class_getInstanceMethod 获取实例方法
class_getClassMethod 获取类方法
method_getImplementation 获取一个方法的实现
method_setImplementation 设置一个方法的实现
method_getTypeEncoding 获取方法实现的编码类型
class_addMethod 添加方法实现
class_replaceMethod 用一个方法的实现,替换另一个方法的实现,即aIMP -> bIMP,但是bIMP !--> aIMP
method_exchangeImplementations 交换两个方法的实现,即 aIMP -> bIMP, bIMP -> aIMP

使用时需注意的坑点

坑点1 :method-swizzling 多次调用的混乱问题

mehod-swizzling写在load方法中,而load方法会被系统主动调用多次,这样会导致方法的重复交换,第1次交换,第2次又还原,第3次又交换,这不就乱套了吗?所以,我们得保证方法交换的触发有且仅有1次

解决方案

可以通过单例,使方法交换只执行一次

+ (void)load{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        //TODO:这里进行你的方法交换
    });
}
坑点2:子类无声明无实现,父类有声明有实现

part1: 新建一个LBHPerson类继承与LBHPerson类,在LBHPerson类中有一个personInstanceMethod方法的声明与实现

/**LBHPerson**/
//.h
@interface LBHPerson : NSObject

- (void)personInstanceMethod;

@end

//.m
@implementation LBHPerson

- (void)personInstanceMethod
{
    NSLog(@"%s",__func__);
}

@end

/**LBHStudent**/
//.h
@interface LBHStudent : LBHPerson

@end

//.m
@implementation LBHStudent

@end

/**调用**/
//*********调用*********
- (void)viewDidLoad {
    [super viewDidLoad];

    // 黑魔法坑点二: 子类没有实现 - 父类实现
    LGStudent *s = [[LGStudent alloc] init];
    [s personInstanceMethod];
    
    LGPerson *p = [[LGPerson alloc] init];
    [p personInstanceMethod];
}

part2: 新建一个LBHStudent+Safe

/**LBHStudent分类**/
//.h
@interface LBHStudent (Safe)

@end

//.m
@implementation LBHStudent (Safe)


+ (void)load{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [LGRuntimeTool lg_methodSwizzlingWithClass:self oriSEL:@selector(personInstanceMethod) swizzledSEL:@selector(lg_studentInstanceMethod)];
    });
}

// personInstanceMethod 我需要父类的这个方法的一些东西
// 给你加一个personInstanceMethod 方法
// imp

- (void)lg_studentInstanceMethod{
    ////是否会产生递归?--不会产生递归,原因是lg_studentInstanceMethod 会走 oriIMP,即personInstanceMethod的实现中去
    [self lg_studentInstanceMethod];
    NSLog(@"LGStudent分类添加的lg对象方法:%s",__func__);
}

@end

//封装代码
@implementation LGRuntimeTool

+ (void)lg_methodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL{
    
    if (!cls) NSLog(@"传入的交换类不能为空");

    Method oriMethod = class_getInstanceMethod(cls, oriSEL);
    Method swiMethod = class_getInstanceMethod(cls, swizzledSEL);
    method_exchangeImplementations(oriMethod, swiMethod);
}

@end

part3: 运行

问题:奔溃了 为啥?

lg_studentInstanceMethod是分类LBHStudent(Safe)的方法,它对应的实现IMP是lg_studentInstanceMethodIMP(仅已这种方法表示) ,在LBHStudent(Safe)load方法中将方法personInstanceMethod的实现IMP与方法lg_studentInstanceMethod的实现IMP进行交换,即personInstanceMethodSEL -->lg_studentInstanceMethodIMPlg_studentInstanceMethodSEL -- > personInstanceMethodIMP

LBHPerson类的实例对象p调用[p personInstanceMethod]时,此时实际上LBHPerson类的方法实现是lg_studentInstanceMethodIMPlg_studentInstanceMethodIMP属于子类的分类,所以无法找到。

解决方案

上述崩溃的根本原因是找不到方法的实现imp,假如我们在方法交换时,先判断交换的方法是否有实现imp,未实现则先实现,这样不就解决问题了。

+ (void)lg_betterMethodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL{
    
    if (!cls) NSLog(@"传入的交换类不能为空");
    // oriSEL       personInstanceMethod
    // swizzledSEL  lg_studentInstanceMethod
    
    Method oriMethod = class_getInstanceMethod(cls, oriSEL);
    Method swiMethod = class_getInstanceMethod(cls, swizzledSEL);
   
    // 尝试添加你要交换的方法 - lg_studentInstanceMethod
    BOOL success = class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(oriMethod));
    
    if (success) {// 自己没有 - 交换 - 没有父类进行处理 (重写一个)
        class_replaceMethod(cls, swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
    }else{ // 自己有
        method_exchangeImplementations(oriMethod, swiMethod);
    }
}

load方法中修改下封装的交换方法

+ (void)load{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [LGRuntimeTool lg_betterMethodSwizzlingWithClass:self oriSEL:@selector(personInstanceMethod) swizzledSEL:@selector(lg_studentInstanceMethod)];
    });
}
解决思路

LBHPerson类的实例对象p,调用方法[p personInstanceMethod]却找不到IMP,所以解决思路是不改变LBHPerson类中实例方法personInstanceMethodIMP

为了不影响父类方法,可以直接在子类中添加同名方法

// 尝试添加你要交换的方法 - lg_studentInstanceMethod
    BOOL success = class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(oriMethod));
    

case1: 添加成功,如下:

子类LBHStudent及其分类中存在两个SEL,指向同一个IMP

class_replaceMethod(cls, swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));

使用method_exchangeImplementations会交换失败,用class_replaceMethod会保持原样.

【注意】 oriMethod是临时变量还是最原始的selimp,所以在替换方法是传的IMP参数是personInstanceMethodIMP

问题: 替换时IMP指向真的是这样吗?

可以在添加方法前后、替换方法执行后打印下方法IMP的指向

添加方法前

oriMethodswiMethod都是临时变量

添加方法后,替换方法前

添加方法成功后,需要重新通过类 cls方法名 oriSEL获取新的方法,新方法的实现 IMP指向被交换方法swimethodIMP

替换方法后

替换后swiMethodIMP指向原oriMethodIMP

case2: 添加不成功,说明已经存在这个方法,可以交换

坑点3:父类有声明无实现,子类无声明无实现

step1: 直接将LBHPerson类中的实现方法注释掉,此时personInstanceMethodLBHPerson中只有声明没有实现

step2: 运行

递归循环,堆栈溢出

为什么会出现递归?

看下它几个阶段的IMP指向变化

添加方法前

添加方法后,替换方法前

替换方法后

根据IMP几个阶段的变化可以得出SELIMP关系图:

添加方法前

添加方法后,替换方法前

替换方法后

lg_studentInstanceMethod方法的实现里,调用了它本身,由于lg_studentInstanceMethodSEL指向lg_studentInstanceMethodIMP自己调用自己会产生递归。

解决方案

如果是调用[p personInstanceMethod], 类LBHPerson的实例方法personInstanceMethod没有IMP,必定会崩溃,可以在类及其分类中添加实现交换方法或通过消息转发防止崩溃

如果是调用[s personInstanceMethod]

+ (void)lg_bestMethodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL{
    
    if (!cls) NSLog(@"传入的交换类不能为空");
    
    Method oriMethod = class_getInstanceMethod(cls, oriSEL);
    Method swiMethod = class_getInstanceMethod(cls, swizzledSEL);
    
    if (!oriMethod) {
        // 在oriMethod为nil时,替换后将swizzledSEL复制一个不做任何事的空实现,代码如下:
        BOOL didAddMethod = class_addMethod(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(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
    if (didAddMethod) {
        class_replaceMethod(cls, swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
    }else{
        method_exchangeImplementations(oriMethod, swiMethod);
    }
}

//load方法修改
+ (void)load{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [LGRuntimeTool lg_bestMethodSwizzlingWithClass:self oriSEL:@selector(personInstanceMethod) swizzledSEL:@selector(lg_studentInstanceMethod)];
    });
}

看下几个阶段IMP指向变化:

初始

!oriMethod 添加方法

设置IMP

交换 IMP

由于oriMethod没有IMP,所以交换方法失败,方法的SEL指向的IMP都没有变化。

根据IMP几个阶段的变化可以得出SELIMP关系图:

初始

!oriMethod 添加方法

设置IMP

交换 IMP

为什么要给swiMethod lg_studentInstanceMethod设置一个IMP

会产生递归

如果不设置一个IMPlg_studentInstanceMethod还是指向原始IMPlg_studentInstanceMethod--> lg_studentInstanceMethodIMPlg_studentInstanceMethod方法实现中会调用自己产生递归。

method-swizzling - 类方法

类方法和实例方法的method-swizzling的原理是类似的,唯一的区别是类方法存在元类中,所以可以做如下操作

step1:LBHStudent中添加一个类方法,只声明bu实现

//.h  
@interface LBHStudent : LBHPerson
+ (void)classMethod;
@end

//.m
@implementation LBHStudent
@end

step2: LBHStudent的分类需要修改

+ (void)load{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
         [LGRuntimeTool lg_bestClassMethodSwizzlingWithClass:self oriSEL:@selector(classMethod) swizzledSEL:@selector(lg_studentClassMethod)];
    });
}

+ (void)lg_studentClassMethod{
    NSLog(@"LGStudent分类添加的lg类方法:%s",__func__);
   [[self class] lg_studentClassMethod];
}

step3: 封装方法修改

//封装的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);
    }   
}

调用

method-swizzling的应用

在项目开发过程中,经常碰到NSArray数组越界或者NSDictionary的key或者value值为nil等问题导致的crash,对于这些问题苹果爸爸并不会报一个警告,而是直接崩溃,不给你任何机会补救。

因此,我们可以根据这个方法交换,对NSArrayNSMutableArrayNSDictionaryNSMutableDictionary等类进行Method Swizzling,发现越界或值为nil时,做一些补救措施,实现方式还是按照上面的例子来做。但是,你发现Method Swizzling根本就不起作用。

为什么?因为它们是类簇,Method Swizzling对类簇不起作用

下面列举了NSArray和NSDictionary本类的类名,可以通过Runtime函数取出本类。

类名 真身
NSArray __NSArrayI
NSMutableArray __NSArrayM
NSDictionary __NSDictionaryI
NSMutableDictionary __NSDictionaryM
测试

以NSArray为例

@implementation NSArray (Safe)


//如果下面代码不起作用,造成这个问题的原因大多都是其调用了super load方法。在下面的load方法中,不应该调用父类的load方法。这样会导致方法交换无效
+ (void)load{
    
//    Method fromMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(objectAtIndex:));
    Method fromMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(objectAtIndexedSubscript:));
    Method toMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(lbh_objectAtIndexedSubscript:));
    
    method_exchangeImplementations(fromMethod, toMethod);
}

//如果下面代码不起作用,造成这个问题的原因大多都是其调用了super load方法。在下面的load方法中,不应该调用父类的load方法。这样会导致方法交换无效
- (id)lbh_objectAtIndexedSubscript:(NSUInteger)index{
    //判断下标是否越界,如果越界就进入异常拦截
    if (self.count-1 < index) {
        // 这里做一下异常处理,不然都不知道出错了。
//#ifdef DEBUG  // 调试阶段
//        return [self lbh_objectAtIndexedSubscript:index];
//#else // 发布阶段
        @try {
            return [self lbh_objectAtIndexedSubscript:index];
        } @catch (NSException *exception) {
            // 在崩溃后会打印崩溃信息,方便我们调试。
            NSLog(@"---------- %s Crash Because Method %s  ----------\n", class_getName(self.class), __func__);
            NSLog(@"%@", [exception callStackSymbols]);
            return nil;
        } @finally {
            
        }
//#endif
    }else{ // 如果没有问题,则正常进行方法调用
        return [self lbh_objectAtIndexedSubscript:index];
    }
}
@end

调用

NSArray *array = @[@"1",@"2",@"3"];
NSLog(@"== %@",array[3]);

运行结果

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,524评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,869评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,813评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,210评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,085评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,117评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,533评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,219评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,487评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,582评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,362评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,218评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,589评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,899评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,176评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,503评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,707评论 2 335

推荐阅读更多精彩内容