因为Objective-C的runtime机制, Method Swizzling这个黑魔法解决了我们实际开发中诸多常规手段所无法解决的问题, 比如代码的插桩,Hook,Patch等等. 我们首先看看常规的Method Swizzling是怎样用的, NSHipster有一篇介绍基本用法的文章Method Swizzling, 我们就先以这篇文章中的示例开始说起吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#import

@implementation UIViewController (Tracking)

+ (void)load {    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{        Class class = [self class];

        SEL originalSelector = @selector(viewWillAppear:);
        SEL swizzledSelector = @selector(xxx_viewWillAppear:);

        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);

        BOOL didAddMethod =
            class_addMethod(class,
                originalSelector,
                method_getImplementation(swizzledMethod),
                method_getTypeEncoding(swizzledMethod));

        if (didAddMethod) {            class_replaceMethod(class,
                swizzledSelector,
                method_getImplementation(originalMethod),
                method_getTypeEncoding(originalMethod));
        } else {            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
    });
}

#pragma mark - Method Swizzling

- (void)xxx_viewWillAppear:(BOOL)animated {    [self xxx_viewWillAppear:animated];
    NSLog(@"viewWillAppear: %@", self);
}

@end

简要说明一下以上代码的几个重点:

  • 通过在Category的+ (void)load方法中添加Method Swizzling的代码,在类初始加载时自动被调用,load方法按照父类到子类,类自身到Category的顺序被调用.

  • 在dispatch_once中执行Method Swizzling是一种防护措施,以保证代码块只会被执行一次并且线程安全,不过此处并不需要,因为当前Category中的load方法并不会被多次调用.

  • 尝试先调用class_addMethod方法,以保证即便originalSelector只在父类中实现,也能达到Method Swizzling的目的.

  • xxx_viewWillAppear:方法中[self xxx_viewWillAppear:animated];代码并不会造成死循环,因为Method Swizzling之后, 调用xxx_viewWillAppear:实际执行的代码已经是原来viewWillAppear中的代码了.

其实以上的代码也可以简写为以下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
+ (void)load {    Class class = [self class];

    SEL originalSelector = @selector(viewWillAppear:);
    SEL swizzledSelector = @selector(xxx_viewWillAppear:);
    Method originalMethod = class_getInstanceMethod(class, originalSelector);
    Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
    if (!originalMethod || !swizzledMethod) {        return;
    }

    IMP originalIMP = method_getImplementation(originalMethod);
    IMP swizzledIMP = method_getImplementation(swizzledMethod);
    const char *originalType = method_getTypeEncoding(originalMethod);
    const char *swizzledType = method_getTypeEncoding(swizzledMethod);

    class_replaceMethod(class,originalSelector,swizzledIMP,swizzledType);
    class_replaceMethod(class,swizzledSelector,originalIMP,originalType);
}

这是因为class_replaceMethod方法其实能够覆盖到class_addMethodmethod_setImplementation两种场景, 对于第一个class_replaceMethod来说, 如果viewWillAppear:实现在父类, 则执行class_addMethod, 否则就执行method_setImplementation将原方法的IMP指定新的代码块; 而第二个class_replaceMethod完成的工作便只是将新方法的IMP指向原来的代码.

除了以上的场景之外,其它场景下我们如何使用Method Swizzling呢?

1.在不同类之间实现Method Swizzling

上面示例是通过Category来新增一个方法然后实现Method Swizzling的, 但有一些场景可能并不适合使用Category(比如私有的类,未获取到该类的声明), 此时我们应该如何来做Method Swizzling呢?

例如已知一个className为Car的类中有一个实例方法- (void)run:(double)speed, 目前需要Hook该方法对速度小于120才执行run的代码, 按照方法交换的流程, 代码应该是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
#import 

@interface MyCar : NSObject
@end

@implementation MyCar

+ (void)load {    Class originalClass = NSClassFromString(@"Car");
    Class swizzledClass = [self class];
    SEL originalSelector = NSSelectorFromString(@"run:");
    SEL swizzledSelector = @selector(xxx_run:);
    Method originalMethod = class_getInstanceMethod(originalClass, originalSelector);
    Method swizzledMethod = class_getInstanceMethod(swizzledClass, swizzledSelector);

    // 向Car类中新添加一个xxx_run:的方法

    BOOL registerMethod = class_addMethod(originalClass,
                                          swizzledSelector,
                                          method_getImplementation(swizzledMethod),
                                          method_getTypeEncoding(swizzledMethod));
    if (!registerMethod) {        return;
    }

    // 需要更新swizzledMethod变量,获取当前Car类中xxx_run:的Method指针

    swizzledMethod = class_getInstanceMethod(originalClass, swizzledSelector);
    if (!swizzledMethod) {        return;
    }

    // 后续流程与之前的一致

    BOOL didAddMethod = class_addMethod(originalClass,
                                        originalSelector,
                                        method_getImplementation(swizzledMethod),
                                        method_getTypeEncoding(swizzledMethod));
    if (didAddMethod) {        class_replaceMethod(originalClass,
                            swizzledSelector,
                            method_getImplementation(originalMethod),
                            method_getTypeEncoding(originalMethod));
    } else {        method_exchangeImplementations(originalMethod, swizzledMethod);
    }
}

- (void)xxx_run:(double)speed {    if (speed < 120) {        [self xxx_run:speed];
    }
}

@end

与之前的流程相比,在前面添加了两个逻辑:

  • 利用runtime向目标类Car动态添加了一个新的方法,此时Car类与MyCar类一样具备了xxx_run:这个方法,MyCar的利用价值便结束了;

  • 为了完成后续Car类中run:xxx_run:的方法交换,此时需要更新swizzledMethod变量为Car中的xxx_run:方法所对应的Method.

以上所有的逻辑也可以合并简化为以下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
+ (void)load {    Class originalClass = NSClassFromString(@"Car");
    Class swizzledClass = [self class];
    SEL originalSelector = NSSelectorFromString(@"run:");
    SEL swizzledSelector = @selector(xxx_run:);
    Method originalMethod = class_getInstanceMethod(originalClass, originalSelector);
    Method swizzledMethod = class_getInstanceMethod(swizzledClass, swizzledSelector);

    IMP originalIMP = method_getImplementation(originalMethod);
    IMP swizzledIMP = method_getImplementation(swizzledMethod);
    const char *originalType = method_getTypeEncoding(originalMethod);
    const char *swizzledType = method_getTypeEncoding(swizzledMethod);

    class_replaceMethod(originalClass,swizzledSelector,originalIMP,originalType);
    class_replaceMethod(originalClass,originalSelector,swizzledIMP,swizzledType);
}

简化后的代码便与之前使用Category的方式并没有什么差异, 这样代码就很容易覆盖到这两种场景了, 但我们需要明确此时class_replaceMethod所完成的工作却是不一样的.

  • 第一个class_replaceMethod与之前的逻辑一致, 当run:方法是实现在Car类或Car的父类, 分别执行method_setImplementationclass_addMethod;

  • 第二个class_replaceMethod则直接在Car类中注册了xxx_run:方法, 并且指定的IMP为当前run:方法的IMP;

2.如何实现类方法的Method Swizzling

以上的代码都是实现的对实例方法的交换, 那如何来实现对类方法的交换呢, 依旧直接贴代码吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@interface NSDictionary (Test)
@end

@implementation NSDictionary (Test)

+ (void)load {    Class cls = [self class];
    SEL originalSelector = @selector(dictionary);
    SEL swizzledSelector = @selector(xxx_dictionary);

    // 使用class_getClassMethod来获取类方法的Method
    Method originalMethod = class_getClassMethod(cls, originalSelector);
    Method swizzledMethod = class_getClassMethod(cls, swizzledSelector);
    if (!originalMethod || !swizzledMethod) {        return;
    }

    IMP originalIMP = method_getImplementation(originalMethod);
    IMP swizzledIMP = method_getImplementation(swizzledMethod);
    const char *originalType = method_getTypeEncoding(originalMethod);
    const char *swizzledType = method_getTypeEncoding(swizzledMethod);

    // 类方法添加,需要将方法添加到MetaClass中
    Class metaClass = objc_getMetaClass(class_getName(cls));
    class_replaceMethod(metaClass,originalSelector,swizzledIMP,swizzledType);
    class_replaceMethod(metaClass,swizzledSelector,originalIMP,originalType);
}

+ (id)xxx_dictionary {    id result = [self xxx_dictionary];
    return result;
}

@end

相比实例方法的Method Swizzling,流程有两点差异:

  • 获取Method的方法变更为class_getClassMethod(Class cls, SEL name),从函数命名便直观体现了和class_getInstanceMethod(Class cls, SEL name)的差别;

  • 对于类方法的动态添加,需要将方法添加到MetaClass中,因为实例方法记录在class的method-list中, 类方法是记录在meta-class中的method-list中的.

3.在类簇中如何实现Method Swizzling

在上面的代码中我们实现了对NSDictionary中的+ (id)dictionary方法的交换,但如果我们用类似代码尝试对- (id)objectForKey:(id)key方法进行交换后, 你便会发现这似乎并没有什么用.

这是为什么呢? 平常我们在Xcode调试时,在下方Debug区域左侧的Variables View中,常常会发现如__NSArrayI或是__NSCFConstantString这样的Class类型, 这便是在Foundation框架中被广泛使用的类簇, 详情请参看Apple文档class cluster的内容.

所以针对类簇的Method Swizzling问题就转变为如何对这些类簇中的私有类做Method Swizzling, 在上面介绍的不同类之间做Method Swizzling便已经能解决该问题, 下面一个简单的示例通过交换NSMutableDictionarysetObject:forKey:方法,让调用这个方法时当参数object或key为空的不会抛出异常:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@interface MySafeDictionary : NSObject
@end

@implementation MySafeDictionary

+ (void)load {    Class originalClass = NSClassFromString(@"__NSDictionaryM");
    Class swizzledClass = [self class];
    SEL originalSelector = @selector(setObject:forKey:);
    SEL swizzledSelector = @selector(safe_setObject:forKey:);
    Method originalMethod = class_getInstanceMethod(originalClass, originalSelector);
    Method swizzledMethod = class_getInstanceMethod(swizzledClass, swizzledSelector);

    IMP originalIMP = method_getImplementation(originalMethod);
    IMP swizzledIMP = method_getImplementation(swizzledMethod);
    const char *originalType = method_getTypeEncoding(originalMethod);
    const char *swizzledType = method_getTypeEncoding(swizzledMethod);

    class_replaceMethod(originalClass,swizzledSelector,originalIMP,originalType);
    class_replaceMethod(originalClass,originalSelector,swizzledIMP,swizzledType);
}

- (void)safe_setObject:(id)anObject forKey:(id)aKey {    if (anObject && aKey) {        [self safe_setObject:anObject forKey:aKey];
    }
    else if (aKey) {        [(NSMutableDictionary *)self removeObjectForKey:aKey];
    }
}

@end

4.在Method Swizzling之后如何恢复

使用了Method Swizzling的各种姿势之后, 是否有考虑如何恢复到交换之前的现场呢?

一种方案就是通过一个开关标识符, 如果需要从逻辑上面恢复到交换之前, 就设置一下这个标识符, 在实现中判定如果设定了该标识符, 逻辑就直接调用原方法的实现, 其它什么事儿也不干, 这是目前大多数代码的实现方法, 当然也是非常安全的方式, 只不过当交换方法过多时, 每一个交换的方法体中都需要增加这样的逻辑, 并且也需要维护大量这些标识符变量, 只是会觉得不够优雅, 所以此处也就不展开详细讨论了.

那下面来讨论一下有没有更好的方案, 以上描述的Method Swizzling各种场景和处理的技巧, 但综合总结之后最核心的其实也只做了两件事情:

  • class_addMethod 添加一个新的方法, 可能是把其它类中实现的方法添加到目标类中, 也可能是把父类实现的方法添加一份在子类中, 可能是添加的实例方法, 也可能是添加的类方法, 总之就是添加了方法.

  • 交换IMP 交换方法的实现IMP,完成这个步骤除了使用method_exchangeImplementations这个方法外, 也可以是调用了method_setImplementation方法来单独修改某个方法的IMP, 或者是采用在调用class_addMethod方法中设定了IMP而直接就完成了IMP的交换, 总之就是对IMP的交换.

那我们来分别看一下这两件事情是否都还能恢复:

  • 对于class_addMethod, 我们首先想到的可能就是有没有对应的remove方法呢, 在Objective-C 1.0的时候有class_removeMethods这个方法, 不过在2.0的时候就已经被禁用了, 也就是苹果并不推荐我们这样做, 想想似乎也是挺有道理的, 本来runtime的接口看着就挺让人心惊胆战的, 又是添加又是删除总觉得会出岔子, 所以只能放弃remove的想法, 反正方法添加在那儿倒也没什么太大的影响.

  • 针对IMP的交换, 在Method Swizzling时做的交换动作, 如果需要恢复其实要做的动作还是交换回来罢了, 所以是可以做到的, 不过需要怎样做呢? 对于同一个类, 同一个方法, 可能会在不同的地方被多次做Method Swizzling, 所以要回退某一次的Method Swizzling, 我们就需要记录下来这一次交换的时候是哪两个IMP做了交换, 恢复的时候再换回来即可. 另一个问题是如果已经经过多次交换, 我们怎样找到这两个IMP所对应的Mehod呢, 还好runtime提供了一个class_copyMethodList方法, 可以直接取出Method列表, 然后我们就可以逐个遍历找到IMP所对应的Method了, 下面是对上一个示例添加恢复之后实现的代码逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
#import 

@interface MySafeDictionary : NSObject
@end

static NSLock *kMySafeLock = nil;
static IMP kMySafeOriginalIMP = NULL;
static IMP kMySafeSwizzledIMP = NULL;

@implementation MySafeDictionary

+ (void)swizzlling {    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{        kMySafeLock = [[NSLock alloc] init];
    });

    [kMySafeLock lock];

    do {        if (kMySafeOriginalIMP || kMySafeSwizzledIMP) break;

        Class originalClass = NSClassFromString(@"__NSDictionaryM");
        if (!originalClass) break;

        Class swizzledClass = [self class];
        SEL originalSelector = @selector(setObject:forKey:);
        SEL swizzledSelector = @selector(safe_setObject:forKey:);
        Method originalMethod = class_getInstanceMethod(originalClass, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(swizzledClass, swizzledSelector);
        if (!originalMethod || !swizzledMethod) break;

        IMP originalIMP = method_getImplementation(originalMethod);
        IMP swizzledIMP = method_getImplementation(swizzledMethod);
        const char *originalType = method_getTypeEncoding(originalMethod);
        const char *swizzledType = method_getTypeEncoding(swizzledMethod);

        kMySafeOriginalIMP = originalIMP;
        kMySafeSwizzledIMP = swizzledIMP;

        class_replaceMethod(originalClass,swizzledSelector,originalIMP,originalType);
        class_replaceMethod(originalClass,originalSelector,swizzledIMP,swizzledType);
    } while (NO);

    [kMySafeLock unlock];
}

+ (void)restore {    [kMySafeLock lock];

    do {        if (!kMySafeOriginalIMP || !kMySafeSwizzledIMP) break;

        Class originalClass = NSClassFromString(@"__NSDictionaryM");
        if (!originalClass) break;

        unsigned int outCount = 0;
        Method *methodList = class_copyMethodList(originalClass, &outCount);
        for (unsigned int idx=0; idx < outCount; idx++) {            Method aMethod = methodList[idx];
            IMP aIMP = method_getImplementation(aMethod);
            if (aIMP == kMySafeSwizzledIMP) {                method_setImplementation(aMethod, kMySafeOriginalIMP);
            }
            else if (aIMP == kMySafeOriginalIMP) {                method_setImplementation(aMethod, kMySafeSwizzledIMP);
            }
        }
        kMySafeOriginalIMP = NULL;
        kMySafeSwizzledIMP = NULL;
    } while (NO);

    [kMySafeLock unlock];
}

- (void)safe_setObject:(id)anObject forKey:(id)aKey {    if (anObject && aKey) {        [self safe_setObject:anObject forKey:aKey];
    }
    else if (aKey) {        [(NSMutableDictionary *)self removeObjectForKey:aKey];
    }
}

@end

注意 这段代码的Method Swizzling和恢复都需要主动调用, 并且相比上面其它的示例, 这段代码还添加如锁机制来加之保护. 这个示例是以不同的类来实现的Method Swizzling和恢复, 如果是Category或者是类方法, 根据之前的示例也需要做相应的调整.

Method Swizzling的各种姿势相关推荐

  1. runtime实践之Method Swizzling

    利用 Objective-C 的 Runtime 特性,我们可以给语言做扩展,帮助解决项目开发中的一些设计和技术问题.这一篇,我们来探索一些利用 Objective-C Runtime 的黑色技巧.这 ...

  2. 关于Swift4.0 Method Swizzling(iOS的hook机制)使用

    2019独角兽企业重金招聘Python工程师标准>>> 关于Method Swizzling 原理什么的有很多帖子讲述的已经很清楚这里不再赘述, 这里仅仅处理Method Swizz ...

  3. 【原】iOS动态性(三) Method Swizzling以及AOP编程:在运行时进行代码注入

    概述 今天我们主要讨论iOS runtime中的一种黑色技术,称为Method Swizzling.字面上理解Method Swizzling可能比较晦涩难懂,毕竟不是中文,不过你可以理解为" ...

  4. Method Swizzling 为什么要先调用 class_addMethod?

    先上个 Swift 中的 demo:Method Swizzling Swift 中的实现 其实 Swift 中实现原理和 OC 基本一致,只是苹果爸爸不再允许在 Swift 中使用+load()和+ ...

  5. Objective-C Runtime 运行时之四:Method Swizzling

    理解Method Swizzling是学习runtime机制的一个很好的机会.在此不多做整理,仅翻译由Mattt Thompson发表于nshipster的Method Swizzling一文. Me ...

  6. Objective-C Runtime (三):Method Swizzling(方法替换)

    Objective-C Runtime (三):Method Swizzling(方法替换) Method Swizzling是一种改变改变一个'selector'的实际实现的技术.通过这一技术,我们 ...

  7. Objective-C的hook方案(一): Method Swizzling

    在没有一个类的实现源码的情况下,想改变其中一个方法的实现,除了继承它重写.和借助类别重名方法暴力抢先之外,还有更加灵活的方法吗?在Objective-C编程中,如何实现hook呢?标题有点大,计划分几 ...

  8. iOS 开发:『Runtime』详解(二)Method Swizzling

    本文用来介绍 iOS 开发中『Runtime』中的黑魔法Method Swizzling. 通过本文,您将了解到: Method Swizzling(动态方法交换)简介 Method Swizzlin ...

  9. iOS总结-Runtime篇之黑魔法Method Swizzling的滥用会有危险吗

    参考https://www.jianshu.com/p/19c5736c5d9a, http://blog.sina.com.cn/s/blog_a343f32b0101en4o.html runti ...

最新文章

  1. Android中进度条控件使用
  2. 如何使用jMeter发送两个逻辑上相关的HTTP请求
  3. 前端学习(1881)vue之电商管理系统电商系统之双层for循环渲染数据
  4. Xray使用的一些经验分享(xray+burp的使用)
  5. 快速掌握用python处理Excel
  6. IP地址自动切换脚本
  7. 一个流氓眼中的物联网
  8. 高盛报告引科技股暴跌 但如今并非 互联网泡沫2.0
  9. Slove the {Failed to load unit 'HGCM' (VERR_INVALID_PARAMETER)}
  10. html打造动画【系列2】- 可爱的蛙蛙表情
  11. HIR夏季挑战赛作品紧急优化
  12. google官方图标 Material icons 全图标一览
  13. 深夜爬虫, 我很抱歉 , 附爬取“网抑云”最详细的爬虫教程!
  14. 正睿集训数论专题【8.9】
  15. Tableau中国地图
  16. 一个程序员的自白:我为什么写博客
  17. 小程序源码:实用的智力测试智商提升
  18. pyqt_利用ScrollBar控制图片位置(实现滑动效果)
  19. Anu-Has-a-Function
  20. 原码,反码,补码,傻傻分不清?

热门文章

  1. 德国耶拿大学植物微生物组实验室急招博士生项目,申请截止19年3月20日
  2. R语言将ggplot2对象转化为plotly对象并通过shiny将可视化结果在应用程序或者网页中显示出来
  3. R语言按组聚合求和实战(sum a variable by group):使用aggregate函数按组聚合求和、使用tapply函数按组聚合求和、按组聚合求和(使用dplyr包)
  4. R语言时间序列(time series)分析实战:HoltWinters平滑法预测
  5. R计算获取决策曲线数据(Decision Curve Analysis,DCA)并使用python进行可视化
  6. python使用正则表达式验证用户输入密码的有效性
  7. Kmeans++、Mini-Batch Kmeans、Bisecting Kmeans、K中心点(K-Medoids)算法、K众数聚类、核K均值聚类
  8. 泊松回归(Poisson regression)、COX回归、分类器变回归器、回归算法注意事项、多重共线性问题
  9. 影像组学视频学习笔记(25)-查看准确度、灵敏度、特异度及混淆矩阵、Li‘s have a solution and plan.
  10. 二十五、二叉树的前序、中序、后序遍历