iOS RunTime 学习记录5_Method Swizzling

前言:我是参考 南峰子 的博客加上自己理解写的,原著专辑大家自己可看:http://southpeak.github.io/categories/objectivec/

1,Method Swizzling啥意思

Method Swizzling 直译是"方法调和",理解起来我们可以想成是方法调剂,方法调整。这样就是方法执行的不合适,我们把它调整调整,哈哈!

通过这个功能,能帮助我们更好的理解Runtime的运行机制,即是在运行时绑定方法。其实一般用在想要不修改原有方法(包括系统方法)实现的情况下,替换掉原方法的实现。其实这个功能真的还是蛮吊的,都说是黑魔法。

2,Method Swizzling常用的网络举栗子

举个例子:
实现功能就是全局在在应用中viewDidAppear插入一个统计代码,这个一般我们不用Runtime中的方法就一个一个添加,或者是所有的ViewContoller都继承一个父类,在父类中添加。但是使用RunTime的Method Swizzling就可以轻松实现这个需求

@implementation UIViewController (Swizzling)
+(void)load{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        
        SEL originalSelector = @selector(viewDidAppear:);
        SEL diySelector = @selector(diyViewDidAppear:);
        
        Method originalMethod = class_getInstanceMethod([self class], originalSelector);
        Method diyMethod = class_getInstanceMethod([self class], diySelector);
        
        //这个添加成功后 就会 用swizzingSEL的IMP指针 指向 originalSEL的 MP指针
        BOOL didAddMethod = class_addMethod([self class], originalSelector, method_getImplementation(diyMethod), method_getTypeEncoding(diyMethod));
        
        
        //下面就是 交换exchange 两个SEL 的 IMP指针
        if (didAddMethod) {
            
            //该函数的行为可以分为两种:如果类中不存在name指定的方法,则类似于class_addMethod函数一样会添加方法;如果类中已存在name指定的方法,则类似于method_setImplementation一样替代原方法的实现
            
            /*如果添加成功
             * 添加成功会发生  1,在self中 没有 originalSEL的实现(即使是父类中实现originalSEL方法也不行)
             *              2, 在self中 有 swizzingSEL的实现(即使父类中实现swizzingSEL也不行)
             * 添加成功后 用originalSEL的IMP指针 指向 swizzingSEL的 MP指针
             */
            
            class_replaceMethod([self class], diySelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
        }else{
            
            /***没有添加成功,那么就是说originalSEL 和 swizzingSEL 实现都存在
             *  直接使用method_exchangeImplementations 交换IMP指针就好
             **/
            method_exchangeImplementations(originalMethod, diyMethod);
        }
    });
}

-(void)diyViewDidAppear:(BOOL)anmiate{
    NSLog(@"ViewDidAppear重新指向");
    [self diyViewDidAppear:anmiate];
}

具体代码就是获取ViewController原生的ViewDidAppear 的Method和自定义diyViewDidAppear的Method 指针,然后交换它两 的IMP(方法实现的标准C调用首地址),通俗的讲就是交换他俩的具体实现。

在这个方法中,会发现最后又调用了自身

-(void)diyViewDidAppear:(BOOL)anmiate{
    NSLog(@"ViewDidAppear重新指向");
    [self diyViewDidAppear:anmiate];
}

是不是会循环调用了,其实不会,因为它俩的实现被交换了,所以调用的diyViewDidAppear:其实会调用它原生的viewDidAppear :。我们来验证一下,在ViewController中,我们这样写:

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
   
//    Student *s = [[Student alloc]init];
//    [s performSelector:@selector(introduceSelf)];
//    [s performSelector:@selector(swizzingIntroduceSelf)];
//    
//    NSArray *testArray = @[@"A",@"B",@"C"];
//    NSString *tStr = [testArray objectAtIndex:4];
    
    
}

-(void)viewDidAppear:(BOOL)animated{
    [super viewDidAppear:animated];
   
    NSLog(@"我是原生方法");
}

运行一下,打印结果:

2017-01-12 14:42:04.689 MethodSwizzlingTest[4892:436048] ViewDidAppear重新指向
2017-01-12 14:42:04.690 MethodSwizzlingTest[4892:436048] ViewDidAppear原生方法

3,自写类说明Method Swizzling的方法替换

为了更好的说明,这个关系,我们用一个普通自定义的类来做说明,一个是Person类 一个继承Person的Student类:
其中Person实现如下:

@implementation Person

-(void)introduceSelf{
    NSLog(@"person:原有实现");
}

-(void)swizzingIntroduceSelf{
    NSLog(@"person:调剂方法");
}

@end

Student类的实现

@implementation Student

+(void)load{
    
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        
        SEL originalSEL = @selector(introduceSelf);
        SEL swizzingSEL = @selector(swizzingIntroduceSelf);
        
        Method originalMethod = class_getInstanceMethod([self class], originalSEL);
        Method swizzingMethod = class_getInstanceMethod([self class], swizzingSEL);
        
        /******交换指针*******/
        //这个添加成功后 就会 用swizzingSEL的IMP指针 指向 originalSEL的 MP指针
        BOOL didAddMethod = class_addMethod([self class],
                                            originalSEL,
                                            method_getImplementation(swizzingMethod),
                                            method_getTypeEncoding(swizzingMethod));
        
        //下面就是 交换exchange 两个SEL 的 IMP指针
        if (didAddMethod) {
            /*如果添加成功
             * 添加成功会发生  1,在self中 没有 originalSEL的实现(即使是父类中实现originalSEL方法也不行)
             *              2, 在self中 有 swizzingSEL的实现(即使父类中实现swizzingSEL也不行)
             * 添加成功后 用originalSEL的IMP指针 指向 swizzingSEL的 MP指针
             */
            class_replaceMethod([self class],
                                swizzingSEL,
                                method_getImplementation(originalMethod),
                                method_getTypeEncoding(originalMethod));
        } else {
            /***没有添加,那么就是说originalSEL 和 swizzingSEL 实现都存在
             *  直接使用method_exchangeImplementations 交换IMP指针就好
             **/
            method_exchangeImplementations(originalMethod, swizzingMethod);
        }
        
        //其实上面这个判断可以归纳为,只要我实现了交换方法swizzingMethod,我都进行 Method Swizzling
//        if (!class_addMethod([self class], swizzingSEL, method_getImplementation(swizzingMethod), method_getTypeEncoding(swizzingMethod))) {
//            method_exchangeImplementations(originalMethod, swizzingMethod);
//        }
    });
}

-(void)introduceSelf{
    NSLog(@"Student:原有实现");
}

-(void)swizzingIntroduceSelf{
    NSLog(@"Student:调剂方法");
    [self swizzingIntroduceSelf];
    
}

@end

这里我们实现了introduceSelf 和 swizzingIntroduceSelf的方法实现交换,我们在VC中这样调用:

    Student *s = [[Student alloc]init];
    [s performSelector:@selector(introduceSelf)];

打印结果:

2017-01-12 15:07:24.333 MethodSwizzlingTest[5232:525878] Student:调剂方法
2017-01-12 15:07:24.334 MethodSwizzlingTest[5232:525878] Student:原有实现

可以看出我们调用的是introduceSelf,确打印swizzingIntroduceSelf的实现,最后我们在swizzingIntroduceSelf中调用自身,会发现它执行的确实'introduceSelf'的实现。我么可以屏蔽最后调用自身的这段代码如下:

-(void)swizzingIntroduceSelf{
    NSLog(@"Student:调剂方法");
//    [self swizzingIntroduceSelf];
}

打印输出:

2017-01-12 15:14:00.866 MethodSwizzlingTest[5323:547121] Student:调剂方法

会发现,完全实现了替换。

我们这样调用

    Student *s = [[Student alloc]init];
    [s performSelector:@selector(swizzingIntroduceSelf)];

结果:

2017-01-12 16:17:47.172 MethodSwizzlingTest[6058:706641] Student:原有实现

会发现方法两个实现方法被完全替换。

4,关于didAddMethod 的判断

这篇文章解释的非常好http://www.jianshu.com/p/f6dad8e1b848,可以看看。

交换之前做了class_addMethod判断

关于class_addMethod,这个解释比较好:
class_addMethod为一个类添加方法(包括方法名称(SEL)和方法的实现(IMP)),返回值为BOOL类型,表示方法是否成功添加。需要注意的地方是class_addMethod会添加一个覆盖父类的实现,但不会取代原有类的实现。也就是说如果class_addMethod返回YES,说明子类中没有方法originalSelector,通过class_addMethod为其添加了方法originalSelector,并使其实现(IMP)为我们想要替换的实现。

//这个添加成功后 就会 用swizzingSEL的IMP指针 指向 originalSEL的 MP指针
        BOOL didAddMethod = class_addMethod([self class],
                                            originalSEL,
                                            method_getImplementation(swizzingMethod),
                                            method_getTypeEncoding(swizzingMethod));

这个是为什么了,其实就是为了防止本类中originalSEL没有实现,人家说是一种保护,我的注释已经写上,如下:

didAddMethod = YES
添加返回(didAddMethod ==YES),必须满足下面的条件:
  1,在self中 没有 originalSEL的实现(即使是父类中实现originalSEL方法也不行)
  2, 在self中 有 swizzingSEL的实现(即使父类中实现swizzingSEL也不行)

这时候swizzingSEL方法的实现就会赋给originalSEL,同时添加originalSEL方法。在添加成功后,我们还需要把originalSEL的实现替换给swizzingSEL,所有就调用了class_replaceMethod方法,这样就完成交换。

didAddMethod = NO

说明方法未添加成功,这说明自身中已经实现originalSEL方法,所以直接调用method_exchangeImplementations来完成方法实现的交换

5,Method Swizzling 额外知识(我只是个搬运工)

为什么方法交换调用在+load方法中?
在Objective-C runtime会自动调用两个类方法,分别为+load与+ initialize。+load 方法是在类被加载的时候调用的,也就是一定会被调用。而+initialize方法是在类或它的子类收到第一条消息之前被调用的,这里所指的消息包括实例方法和类方法的调用。也就是说+initialize方法是以懒加载的方式被调用的,如果程序一直没有给某个类或它的子类发送消息,那么这个类的+initialize方法是永远不会被调用的。此外+load方法还有一个非常重要的特性,那就是子类、父类和分类中的+load方法的实现是被区别对待的。换句话说在 Objective-C runtime自动调用+load方法时,分类中的+load方法并不会对主类中的+load方法造成覆盖。综上所述,+load 方法是实现 Method Swizzling 逻辑的最佳“场所”。如需更深入理解,可参考Objective-C 深入理解 +load 和 +initialize

为什么方法交换要在dispatch_once中执行?
方法交换应该要线程安全,而且保证在任何情况下(多线程环境,或者被其他人手动再次调用+load方法)只交换一次,防止再次调用又将方法交换回来。除非只是临时交换使用,在使用完成后又交换回来。 最常用的解决方案是在+load方法中使用dispatch_once来保证交换是安全的。之前有读者反馈+load方法本身即为线程安全,为什么仍需添加dispatch_once,其原因就在于+load方法本身无法保证其中代码只被执行一次。

6,Method Swizzling 试用场景

实例一:替换ViewController生命周期方法
实例二:解决获取索引、添加、删除元素越界崩溃问题
实例三:防止按钮重复暴力点击
实例四:全局更换控件初始效果
实例五:App热修复
实例六:App异常加载占位图通用类封装
实例七:全局修改导航栏后退(返回)按钮

6.1第一个就是我们刚开始引入的时候举得例子。

6.2第二个我的Demo里面有,下有Demo下载地址

实现思路就是替换objectAtIndex:方法,在里面对索引进行判断

-(void)diyObjectAtIndex:(NSUInteger)index{
    
    if (index < self.count) {
        [self diyObjectAtIndex:index];
    }else{
        @try {
            [self diyObjectAtIndex:index];
        } @catch (NSException *exception) {
            NSLog(@"数组越界");
        } @finally {
            
        }
    }
}

搬运工补充知识:
这里没有使用self来调用,而是使用objc_getClass("__NSArrayM")来调用的。因为NSMutableArray的真实类只能通过后者来获取,而不能通过[self class]来获取,而method swizzling只对真实的类起作用。这里就涉及到一个小知识点:类簇。补充以上对象对应类簇表。


Paste_Image.png

6.3 来试试

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

推荐阅读更多精彩内容

  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,692评论 0 9
  • 转载:http://www.cocoachina.com/ios/20161102/17920.html 因为Ob...
    F麦子阅读 667评论 0 1
  • 目录 Objective-C Runtime到底是什么 Objective-C的元素认知 Runtime详解 应用...
    Ryan___阅读 1,935评论 1 3
  • 我们常常会听说 Objective-C 是一门动态语言,那么这个「动态」表现在哪呢?我想最主要的表现就是 Obje...
    Ethan_Struggle阅读 2,186评论 0 7
  • 这篇文章完全是基于南峰子老师博客的转载 这篇文章完全是基于南峰子老师博客的转载 这篇文章完全是基于南峰子老师博客的...
    西木阅读 30,548评论 33 466