OC底层原理15-Method Swizzling

iOS--OC底层原理文章汇总

Method Swizzling

方法交换,这是发生在Runtime中的一种处理两个方法交换的手段,它处理的是什么?怎么处理的?今天就来探究一下。
method swizzling在runtime将一个方法的实现替换为另一个方法的实现。在前面的篇章中,我们知道,一个方法的实现是需要sel、imp对应的,通过sel就能找到impmethod swizzling正是通过改变sel、imp指向来实现方法的交换。这也是Runtime的一种应用。
下面是sel、imp交换前后示意图

交换前

经过method swizzling,也可以理解为imp swizzling,将二者的sel、imp,通过更改实现

交换后

我们是在什么时间去处理这个调用转换呢?我们可以选择在+load调用时(也有用在initialize)。由前面的篇章我们了解到,如果类实现+load,使得该类成为非懒加载类,这个方法系统调用的时间会很早,所以在runtime过程中,放在此处是很合适的。
先来看看处理方法交换的相关API,苹果文档显示右如下的方法是处理方法可能用到的:

method_invoke 调用指定方法的实现。
method_invoke_stret 调用返回数据结构的指定方法的实现。
method_getName 返回方法的名称。
method_getTypeEncoding 返回描述方法参数和返回类型的字符串。
method_copyReturnType 返回描述方法返回类型的字符串。
method_copyArgumentType 返回描述方法的单个参数类型的字符串。
method_getReturnType 通过引用返回描述方法返回类型的字符串。
method_getNumberOfArguments 返回方法接受的参数数量。
method_getArgumentType 通过引用返回描述方法的单个参数类型的字符串。
method_getDescription 返回指定方法的方法描述结构。
method_setImplementation 设置方法的实现。
method_exchangeImplementations 交换两种方法的实现。
method_getImplementation 返回方法的实现。

实操

由于选择在+load中处理方法交换,+load可能被多次调用,那就得保证方法交换仅仅执行一次,所以采用单例模式处理:

+ (void)load
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{  
        /** 保证执行一次交换
         *  定义一个Runtime工具类,在其中实现交换
         * animalInstanceMethod:原始待替换方法
         * smallCatInstanceMethod:替换之后的方法
         */
        [TLRuntimeTool runtimeMethodSwizzlingWithClass:self originSEL:@selector(animalInstanceMethod) swizzlingSEL:@selector(smallCatInstanceMethod)];
    });
}

以下是TLRuntimeTool的实现

//--------TLRuntimeTool.h----------
@interface TLRuntimeTool : NSObject

+(void)runtimeMethodSwizzlingWithClass:(Class)targetCls originSEL:(SEL)oriSel swizzlingSEL:(SEL)swizzlingSel;

@end
//--------TLRuntimeTool.m ----------
#import "TLRuntimeTool.h"
#import <objc/runtime.h>

@implementation TLRuntimeTool

+(void)runtimeMethodSwizzlingWithClass:(Class)targetCls originSEL:(SEL)oriSel swizzlingSEL:(SEL)swizzlingSel
{
    if (!targetCls) {
        NSLog(@"传入的类不能为空");
        return;
    }
    Method originMethod = class_getInstanceMethod(targetCls, oriSel);
    Method swizzlingMethod = class_getInstanceMethod(targetCls, swizzlingSel);
    method_exchangeImplementations(originMethod, swizzlingMethod);   
}

现在对方法替换做一个测试,定义一个Animal类、Cat类、Cat+small分类,并在VC页面对其调用

测试工程结构

以下是各类实现

  • Animal
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface Animal : NSObject
-(void)animalInstanceMethod;

+(void)animalClassMethod;

@end

NS_ASSUME_NONNULL_END
// -----------------------
#import "Animal.h"
@implementation Animal
- (void)animalInstanceMethod{
    NSLog(@"animal instance method:%s",__func__);
}
+ (void)animalClassMethod{
    NSLog(@"animal class metohd: %s",__func__);
}
@end
  • Cat
#import "Animal.h"

NS_ASSUME_NONNULL_BEGIN

@interface Cat : Animal

@end

NS_ASSUME_NONNULL_END

#import "Cat.h"

@implementation Cat
- (void)animalInstanceMethod{
    NSLog(@"cat instance method:%s",__func__);
}
@end
  • Cat+small
#import "Cat.h"

NS_ASSUME_NONNULL_BEGIN

@interface Cat (small)

@end

NS_ASSUME_NONNULL_END
//----------------
#import "Cat+small.h"
#import "TLRuntimeTool.h"
@implementation Cat (small)

+ (void)load
{
        /** 保证执行一次交换
         *  定义一个Runtime工具类,在其中实现交换
         * animalInstanceMethod:原始待替换方法
         * smallCatInstanceMethod:替换之后的方法
         */
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [TLRuntimeTool runtimeMethodSwizzlingWithClass:self originSEL:@selector(animalInstanceMethod) swizzlingSEL:@selector(smallCatInstanceMethod)];
    });
}
- (void)smallCatInstanceMethod
{
    NSLog(@"给 small cat 分类添加了实例方法:%s",__func__);
  [self smallCatInstanceMethod]; 
}
@end

正常情况下,这样就能处理运行时对方法的替换,不会有什么问题。但会有一些坑点,这样是不完善的,下面来看一看

坑点1:[self smallCatInstanceMethod]是否会产生递归

这里可能会有一个面试点,这样是否会产生递归呢?答案是不会,因为经过方法替换,自己调用自己的 smallCatInstanceMethod,其实该originSel指向的是swizzlingIMP,而swizzlingIMP指向的又会是originIMP,不会产生递归。

循环调用是否递归?

方法替换结果

坑点2:父类的方法,父类实现了,子类未实现

Animal类实现了一个方法animalInstanceMethod(),继承它的一个子类Cat未实现该方法animalInstanceMethod(),然后需要替换的就是该子类Cat的animalInstanceMethod(),而子类就会因为未实现而奔溃。

子类未实现父类方法奔溃

奔溃提示-[Animal smallCatInstanceMethod]找不到,就是因为在TLRuntimTool在方法替换过程中,分类替换方法中去执行[self smallCatInstanceMethod],Animal 的animalInstanceMethod的实现已经是指向Cat类的smallCatInstanceMethod,而分类中未实现animalInstanceMethod,当Animal去查找对应IMP时,是查找不到smallCatInstanceMethod,所以就崩溃了。为了解决该情况,我们就需要在TLRuntimTool中对特殊情况做处理:

//--------TLRuntimeTool.h----------
@interface TLRuntimeTool : NSObject

+(void)runtimeMethodSwizzlingWithClass:(Class)targetCls originSEL:(SEL)oriSel swizzlingSEL:(SEL)swizzlingSel;

@end
//--------TLRuntimeTool.m ----------
#import "TLRuntimeTool.h"
#import <objc/runtime.h>

@implementation TLRuntimeTool

+(void)runtimeMethodSwizzlingWithClass:(Class)targetCls originSEL:(SEL)oriSel swizzlingSEL:(SEL)swizzlingSel
{
    if (!targetCls) {
        NSLog(@"传入的类不能为空");
        return;
    }
    
    Method originMethod = class_getInstanceMethod(targetCls, oriSel);
    Method swizzlingMethod = class_getInstanceMethod(targetCls, swizzlingSel);
    
    BOOL swizzlingResult = class_addMethod(targetCls,
                                           oriSel,
                                           method_getImplementation(swizzlingMethod),
                                           method_getTypeEncoding(originMethod));
    /**
     * 判断是否能添加成功;YES->表明对象类没有方法,重写一个实现方法
     */
    if (swizzlingResult) {
        class_replaceMethod(targetCls,
                            swizzlingSel,
                            method_getImplementation(originMethod),
                            method_getTypeEncoding(originMethod));
    }else{
        // 原有类有实现方法
        method_exchangeImplementations(originMethod, swizzlingMethod);
    }
}

先获取到原有方法和将替换方法,对二者做一个操作,向具有给定名称和实现的目标类中添加新方法.
1.如果返回YES,表明目标类没有方法,重写一个实现方法,替换给定类的方法的实现。即originSEL_A -> originIMP_B -> swizzlingSEL - > originIMP_B.
2.如果返回NO,则该类有给定方法的实现,则替换两个方法的实现;即originSEL_B -> oringIMP_A.
这样就处理之后优化了

坑点3:父类子类都不曾实现需要替换的方法

假设需要替换Cat类的一个animalInstanceMethod,父类声明了方法animalInstanceMethod,子类父类都未实现,按坑点2优化后的结果依然是有问题的。
按照

- (void)smallCatInstanceMethod
{
    NSLog(@"给 small cat 分类添加了实例方法:%s",__func__);
  [self smallCatInstanceMethod]; 
}

这个时候就会出现递归

递归

原因是,子类父类都未实现animalInstanceMethod,在Cat交换smallCatInstanceMethod时再次调用自身就会产生递归。为了避免这个情况,那就还需要优化:


@implementation TLRuntimeTool

+(void)runtimeMethodSwizzlingWithClass:(Class)targetCls originSEL:(SEL)oriSel swizzlingSEL:(SEL)swizzlingSel
{
    if (!targetCls) {
        NSLog(@"传入的类不能为空");
        return;
    }
    
    Method originMethod = class_getInstanceMethod(targetCls, oriSel);
    Method swizzlingMethod = class_getInstanceMethod(targetCls, swizzlingSel);
    
    if (!originMethod) {
        // 在oriMethod为nil时,替换后将swizzledSEL复制一个不做任何事的空实现
        class_addMethod(targetCls, oriSel, method_getImplementation(swizzlingMethod), method_getTypeEncoding(swizzlingMethod));
        method_setImplementation(swizzlingMethod, imp_implementationWithBlock(^(id self, SEL _cmd){ }));
    }
    
    BOOL swizzlingResult = class_addMethod(targetCls,
                                           oriSel,
                                           method_getImplementation(swizzlingMethod),
                                           method_getTypeEncoding(originMethod));
    /**
     * 判断是否能添加成功;如果成功,表明对象类没有方法,重写一个实现方法
     */
    if (swizzlingResult) {
        class_replaceMethod(targetCls,
                            swizzlingSel,
                            method_getImplementation(originMethod),
                            method_getTypeEncoding(originMethod));
    }else{
        // 原有类有实现方法
        method_exchangeImplementations(originMethod, swizzlingMethod);
    }
}

添加了一个对原有方法的判断,避免对没有实现的方法进行替换而出错,在oriMethod为nil时,替换后将swizzledSEL复制一个不做任何事的空实现。

这样就非常棒的解决了几个特殊情况下导致的错误。

方法替换的应用

method-swizzling最常用的应用是防止数组越界奔溃、字典取值崩溃等情况。
在iOS中NSNumber、NSArray、NSDictionary等这些类都是类簇,NSArray的实现时可能会由多个类组成。所以如果想对NSArray进行替换,必须获取到其本类(__NSArrayM)进行替换,直接对NSArray进行操作是无效的。类簇详情请参看Apple文档class cluster的内容.

// 开发过程中断点时留意下就知道其底层的类,如 mutA __NSArrayM * @"2 elements" 0x00006000036fd770
类簇                              “真身”
NSArray               --->    __NSArrayI
NSMutableArray        --->    __NSArrayM
NSDictionary          --->    __NSDictionaryI
NSMutableDictionary   --->    __NSDictionaryM
NSNumber              --->    __NSCFNumber

NSArray为例,新建一个NSArray分类,在里面添加以下方法,区分开发模式发布模式


#import "NSArray+AvoidCrash.h"
#import "TLRuntimeTool.h"
#import <objc/runtime.h>
@implementation NSArray (AvoidCrash)
+ (void)load
{
        /** 保证执行一次交换
         *  定义一个Runtime工具类,在其中实现交换
         * animalInstanceMethod:原始待替换方法
         * smallCatInstanceMethod:替换之后的方法
         */
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        
        [TLRuntimeTool runtimeMethodSwizzlingWithClass:NSClassFromString(@"__NSArrayI")
                                             originSEL:@selector(objectAtIndex:)
                                          swizzlingSEL:@selector(avoidCrashObjectsAtIndexes:)];

    });
}

//__NSArrayI  objectAtIndex:
- (id)avoidCrashObjectsAtIndexes:(NSUInteger)index {

#ifdef DEBUG  // 开发模式
      return  [self avoidCrashObjectsAtIndexes:index];
#else // 发布模式
    id object = nil;
    @try {
       object = [self avoidCrashObjectsAtIndexes:index];
   }
   @catch (NSException *exception) {
       // 捕捉到的错误
       
       NSLog(@"** Exception class :%s ** Exception Method: %s \n", class_getName(self.class), __func__);
       
       NSLog(@"Uncaught exception description: %@", exception);
       
       NSLog(@"%@", [exception callStackSymbols]);
       
   }
   @finally {
       return object;
   }
#endif
}
@end

写一个数组,取值测试如下

    NSArray * mutA = @[@"3",@"2"];
    NSLog(@"%@",[mutA objectAtIndex:3]);
  • 开发模式


    该崩还是让它崩
  • 模拟发布模式


    读取崩溃错误

    打印了错误日志,但程序不会崩溃

GitHub工程地址

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