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

method-swizzling是什么?

  • method-swizzling的含义是方法交换,其主要作用是在运行时将一个方法的实现替换成另一个方法的实现,这就是我们常说的iOS黑魔法
  • 在OC中就是利用method-swizzling实现AOP,其中AOP(Aspect Oriented Programming,面向切面编程)是一种编程的思想,区别于OOP(面向对象编程)
    • OOPAOP都是一种编程的思想
    • OOP编程思想更加倾向于对业务模块的封装,划分出更加清晰的逻辑单元
    • AOP面向切面进行提取封装,提取各个模块中的公共部分,提高模块的复用率,降低业务之间的耦合性
  • 每个类都维护着一个方法列表,即methodListmethodList中有不同的方法(Method),每个方法中包含了方法的selIMP,方法交换就是将sel和imp原本的对应断开,并将sel和新的imp生成对应关系

如下图所示,交换前后的sel和imp的对应关系


image

method-swizzling涉及的相关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

method-swizzling使用过程中的问题分析

坑点1:method-swizzling使用过程中的一次性问题

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

解决方案

可以通过单例设计原则,使方法交换只执行一次,在OC中可以通过dispatch_once实现单例

+ (void)load{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [HTRuntimeTool ht_bestMethodSwizzlingWithClass:self oriSEL:@selector(helloword) swizzledSEL:@selector(ht_studentInstanceMethod)];
    });
}

坑点2:子类没有实现,父类实现了

  • 创建HTPerson类,并且实现personInstanceMethod方法
@interface HTPerson : NSObject

- (void)personInstanceMethod;

@end

@implementation HTPerson

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

@end
  • 创建HTTeacher类,继承子HTPerson,没有实现personInstanceMethod,代码如下
@interface HTTeacher : HTPerson

@end

/** HTTeacher.m 文件*/
#import "HTTeacher.h"
#import "HTRuntimeTool.h"

@implementation HTTeacher

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

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

@end
  • HTRuntimeTool.m中的方法如下
+ (void)ht_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);
}
  • ViewController.m中调用如下
#import "ViewController.h"
#import "HTTeacher.h"

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    HTTeacher *t = [HTTeacher alloc];
    [t personInstanceMethod];
    
    HTPerson *p = [HTPerson alloc];
    [p personInstanceMethod];
}

@end
  • 通过实际代码的调试,发现会在p调用personInstanceMethod方法时崩溃,下面来进行详细说明
image
  • [t personInstanceMethod];中不报错是因为 HTTeacher类在+ (void)load方法中将personInstanceMethod交换成了teacherInstanceMethod,对于HTTeacher的对象来说,这两个方法都是可以找到的,所以不会报错
  • 崩溃的点在于[p personInstanceMethod];,其本质原因:
    • HTTeacher类在+ (void)load方法中将personInstanceMethod交换成了teacherInstanceMethod
    • [p personInstanceMethod];相当于调用的是HTTeacherteacherInstanceMethod方法,然后需要取HTPerson类中查找teacherInstanceMethod方法,但是HTPerson中没有方法实现,即相关的imp找不到,所以就崩溃了
image

优化:避免imp找不到

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

  • 如果添加成功,即类中没有这个方法,则通过class_replaceMethod进行替换,其内部会调用class_addMethod进行添加
  • 如果添加不成功,即类中有这个方法,则通过method_exchangeImplementations进行交换
+ (void)ht_betterMethodSwizzlingWithClass:(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));

    if (success) {// 自己没有 - 交换 - 没有父类进行处理 (重写一个)
        class_replaceMethod(cls, swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
    } else { // 自己有
        method_exchangeImplementations(oriMethod, swiMethod);
    }
}
  • 运行程序,发现不会报错了
image
  • 下面是class_replaceMethodclass_addMethodmethod_exchangeImplementations的源码实现
image

image

image

其中class_replaceMethodclass_addMethod中都调用了addMethod方法,区别在于bool值的判断,下面是addMethod的源码实现

/**********************************************************************
* addMethod
* fixme
* Locking: runtimeLock must be held by the caller
**********************************************************************/
static IMP 
addMethod(Class cls, SEL name, IMP imp, const char *types, bool replace)
{
    IMP result = nil;

    runtimeLock.assertLocked();

    checkIsKnownClass(cls); // 检查是否是已知类
    
    ASSERT(types);
    ASSERT(cls->isRealized());

    method_t *m;
    if ((m = getMethodNoSuper_nolock(cls, name))) {
        // already exists 已经存在
        if (!replace) {
            // 添加方法:直接返回要添加的方法实现
            result = m->imp(false);
        } else {
            // 替换方法:返回替换之前的方法实现
            result = _method_setImplementation(cls, m, imp);
        }
    } else { // 不存在
        // fixme optimize
        // 新建一个method_list,添加方法
        method_list_t *newlist;
        newlist = (method_list_t *)calloc(method_list_t::byteSize(method_t::bigSize, 1), 1);
        newlist->entsizeAndFlags = 
            (uint32_t)sizeof(struct method_t::big) | fixed_up_method_list;
        newlist->count = 1;
        auto &first = newlist->begin()->big();
        first.name = name;
        first.types = strdupIfMutable(types);
        first.imp = imp;
        // 对rwe进行处理,将增的newlist加进去
        addMethods_finish(cls, newlist);
        result = nil;
    }

    return result;
}

坑点3:子类没有实现,父类也没有实现,下面的调用有什么问题?

在调用personInstanceMethod方法时,父类HTPerson中只有声明,没有实现,子类HTTeacher中既没有声明,也没有实现

  • 运行代码,报错结果如下所示
image

原因是 栈溢出,递归死循环了,那么为什么会发生递归呢?----主要是因为

  • personInstanceMethod没有实现,即oriMethod空,调用class_addMethod方法后,oriSELswizzledSEL的方法实现都是swiMethod,然后在方法替class_replaceMethod时,替换失败
  • 当我们调用personInstanceMethod(oriSEL)时,也就是swiMethod,会进入HTTeacher中的teacherInstanceMethod方法,然后这个方法中又调用了teacherInstanceMethod,此时的teacherInstanceMethod并没有指向oriMethod,然后导致了自己调自己,即递归死循环
image

优化:避免递归死循环

如果oriMethod为空,为了避免方法交换没有意义,而被废弃,需要做一些事情

  • 通过class_addMethodoriSEL添加swiMethod方法
  • 通过method_setImplementationswiMethodIMP指向不做任何事的空实现
+ (void)ht_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复制一个不做任何事的空实现,代码如下:
        class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
        method_setImplementation(swiMethod, imp_implementationWithBlock(^(id self, SEL _cmd){ }));
    }
    
    // 一般交换方法: 交换自己有的方法 -- 走下面 因为自己有意味添加方法失败
    // 交换自己没有实现的方法:
    //   首先第一步:会先尝试给自己添加要交换的方法 :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);
    }
    
}

method-swizzling的应用

method-swizzling最常用的应用是防止数组、字典等越界崩溃

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

  • 下面列举了NSArrayNSDictionary本类的类名,可以通过Runtime函数取出本类。
类名 真身
NSArray __NSArrayI
NSMutableArray __NSArrayM
NSDictionary __NSDictionaryI
NSMutableDictionary __NSDictionaryM

NSArray防崩溃处理

  • 创建NSArray的一个分类HT
#import "NSArray+HT.h"
#import <objc/runtime.h>

@implementation NSArray (HT)

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

- (id)ht_objectAtIndex:(NSUInteger)index{
    //判断下标是否越界,如果越界就进入异常拦截
    if (self.count-1 < index) {
        // 这里做一下异常处理,不然都不知道出错了。
#ifdef DEBUG  // 调试阶段
        return [self ht_objectAtIndex:index];
#else // 发布阶段
        @try {
            return [self ht_objectAtIndex: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 ht_objectAtIndex:index];
    }
}

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

推荐阅读更多精彩内容