iOS黑魔法-Method Swizzling

作者: 刘小壮 | 来源:发表于2016-01-21 10:52 被阅读24564次
    该文章属于<简书 — 刘小壮>原创,转载请注明:

    <简书 — 刘小壮> http://www.jianshu.com/p/ff19c04b34d0


    公司年底要在新年前发一个版本,最近一直很忙,好久没有更新博客了。正好现在新版本开发的差不多了,抽空总结一下。

    由于最近开发新版本,就避免不了在开发和调试过程中引起崩溃,以及诱发一些之前的bug导致的崩溃。而且项目比较大也很不好排查,正好想起之前研究过的Method Swizzling,考虑是否能用这个苹果的“黑魔法”解决问题,当然用好这个黑魔法并不局限于解决这些问题....


    占位图

    需求

    就拿我们公司项目来说吧,我们公司是做导航的,而且项目规模比较大,各个控制器功能都已经实现。突然有一天老大过来,说我们要在所有页面添加统计功能,也就是用户进入这个页面就统计一次。我们会想到下面的一些方法:

    手动添加

    直接简单粗暴的在每个控制器中加入统计,复制、粘贴、复制、粘贴...
    上面这种方法太Low了,消耗时间而且以后非常难以维护,会让后面的开发人员骂死的。

    继承

    我们可以使用OOP的特性之一,继承的方式来解决这个问题。创建一个基类,在这个基类中添加统计方法,其他类都继承自这个基类。

    然而,这种方式修改还是很大,而且定制性很差。以后有新人加入之后,都要嘱咐其继承自这个基类,所以这种方式并不可取。

    Category

    我们可以为UIViewController建一个Category,然后在所有控制器中引入这个Category。当然我们也可以添加一个PCH文件,然后将这个Category添加到PCH文件中。

    我们创建一个Category来覆盖系统方法,系统会优先调用Category中的代码,然后在调用原类中的代码。

    我们可以通过下面的这段伪代码来看一下:

    #import "UIViewController+EventGather.h"
    
    @implementation UIViewController (EventGather)
    
    - (void)viewDidLoad {
       NSLog(@"页面统计:%@", self);
    }
    @end
    

    Method Swizzling

    我们可以使用苹果的“黑魔法”Method SwizzlingMethod Swizzling本质上就是对IMPSEL进行交换。

    Method Swizzling原理

    Method Swizzing是发生在运行时的,主要用于在运行时将两个Method进行交换,我们可以将Method Swizzling代码写到任何地方,但是只有在这段Method Swilzzling代码执行完毕之后互换才起作用。

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

    原理分析

    首先,让我们通过两张图片来了解一下Method Swizzling的实现原理

    图一 图二

    上面图一中selector2原本对应着IMP2,但是为了更方便的实现特定业务需求,我们在图二中添加了selector3IMP3,并且让selector2指向了IMP3,而selector3则指向了IMP2,这样就实现了“方法互换”。

    OC语言的runtime特性中,调用一个对象的方法就是给这个对象发送消息。是通过查找接收消息对象的方法列表,从方法列表中查找对应的SEL,这个SEL对应着一个IMP(一个IMP可以对应多个SEL),通过这个IMP找到对应的方法调用。

    在每个类中都有一个Dispatch Table,这个Dispatch Table本质是将类中的SELIMP(可以理解为函数指针)进行对应。而我们的Method Swizzling就是对这个table进行了操作,让SEL对应另一个IMP


    Method Swizzling使用

    在实现Method Swizzling时,核心代码主要就是一个runtime的C语言API:

    OBJC_EXPORT void method_exchangeImplementations(Method m1, Method m2) __OSX_AVAILABLE_STARTING(__MAC_10_5, __IPHONE_2_0);
    

    代码示例

    就拿上面我们说的页面统计的需求来说吧,这个需求在很多公司都很常见,我们下面的Demo就通过Method Swizzling简单的实现这个需求。

    我们先给UIViewController添加一个Category,然后在Category中的+(void)load方法中添加Method Swizzling方法,我们用来替换的方法也写在这个Category中。由于load类方法是程序运行时这个类被加载到内存中就调用的一个方法,执行比较早,并且不需要我们手动调用。而且这个方法具有唯一性,也就是只会被调用一次,不用担心资源抢夺的问题。

    定义Method Swizzling中我们自定义的方法时,需要注意尽量加前缀,以防止和其他地方命名冲突,Method Swizzling的替换方法命名一定要是唯一的,至少在被替换的类中必须是唯一的。

    #import "UIViewController+swizzling.h"
    #import <objc/runtime.h>
    
    @implementation UIViewController (swizzling)
    
    + (void)load {
        // 通过class_getInstanceMethod()函数从当前对象中的method list获取method结构体,如果是类方法就使用class_getClassMethod()函数获取。
        Method fromMethod = class_getInstanceMethod([self class], @selector(viewDidLoad));
        Method toMethod = class_getInstanceMethod([self class], @selector(swizzlingViewDidLoad));
        /**
         我们在这里使用class_addMethod()函数对Method Swizzling做了一层验证,如果self没有实现被交换的方法,会导致失败。
         而且self没有交换的方法实现,但是父类有这个方法,这样就会调用父类的方法,结果就不是我们想要的结果了。
         所以我们在这里通过class_addMethod()的验证,如果self实现了这个方法,class_addMethod()函数将会返回NO,我们就可以对其进行交换了。
         */
        if (!class_addMethod([self class], @selector(swizzlingViewDidLoad), method_getImplementation(toMethod), method_getTypeEncoding(toMethod))) {
            method_exchangeImplementations(fromMethod, toMethod);
        }
    }
    
    // 我们自己实现的方法,也就是和self的viewDidLoad方法进行交换的方法。
    - (void)swizzlingViewDidLoad {
        NSString *str = [NSString stringWithFormat:@"%@", self.class];
        // 我们在这里加一个判断,将系统的UIViewController的对象剔除掉
        if(![str containsString:@"UI"]){
            NSLog(@"统计打点 : %@", self.class);
        }
        [self swizzlingViewDidLoad];
    }
    @end
    

    看到上面的代码,肯定有人会问:楼主,你太粗心了,你在swizzlingViewDidLoad方法中又调用了[self swizzlingViewDidLoad];,这难道不会产生递归调用吗?
    答:然而....并不会😏。

    还记得我们上面的图一和图二吗?Method Swizzling的实现原理可以理解为”方法互换“。假设我们将A和B两个方法进行互换,向A方法发送消息时执行的却是B方法,向B方法发送消息时执行的是A方法。

    例如我们上面的代码,系统调用UIViewControllerviewDidLoad方法时,实际上执行的是我们实现的swizzlingViewDidLoad方法。而我们在swizzlingViewDidLoad方法内部调用[self swizzlingViewDidLoad];时,执行的是UIViewControllerviewDidLoad方法。

    Method Swizzling类簇

    之前我也说到,在我们项目开发过程中,经常因为NSArray数组越界或者NSDictionarykey或者value值为nil等问题导致的崩溃,对于这些问题苹果并不会报一个警告,而是直接崩溃,感觉苹果这样确实有点“太狠了”。

    由此,我们可以根据上面所学,对NSArrayNSMutableArrayNSDictionaryNSMutableDictionary等类进行Method Swizzling,实现方式还是按照上面的例子来做。但是....你发现Method Swizzling根本就不起作用,代码也没写错啊,到底是什么鬼?

    这是因为Method SwizzlingNSArray这些的类簇是不起作用的。因为这些类簇类,其实是一种抽象工厂的设计模式。抽象工厂内部有很多其它继承自当前类的子类,抽象工厂类会根据不同情况,创建不同的抽象对象来进行使用。例如我们调用NSArrayobjectAtIndex:方法,这个类会在方法内部判断,内部创建不同抽象类进行操作。

    所以也就是我们对NSArray类进行操作其实只是对父类进行了操作,在NSArray内部会创建其他子类来执行操作,真正执行操作的并不是NSArray自身,所以我们应该对其“真身”进行操作。

    代码示例

    下面我们实现了防止NSArray因为调用objectAtIndex:方法,取下标时数组越界导致的崩溃:

    #import "NSArray+LXZArray.h"
    #import "objc/runtime.h"
    
    @implementation NSArray (LXZArray)
    
    + (void)load {
        Method fromMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(objectAtIndex:));
        Method toMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(lxz_objectAtIndex:));
        method_exchangeImplementations(fromMethod, toMethod);
    }
    
    - (id)lxz_objectAtIndex:(NSUInteger)index {
        if (self.count-1 < index) {
            // 这里做一下异常处理,不然都不知道出错了。
            @try {
                return [self lxz_objectAtIndex:index];
            }
            @catch (NSException *exception) {
                // 在崩溃后会打印崩溃信息,方便我们调试。
                NSLog(@"---------- %s Crash Because Method %s  ----------\n", class_getName(self.class), __func__);
                NSLog(@"%@", [exception callStackSymbols]);
                return nil;
        }
            @finally {}
        } else {
            return [self lxz_objectAtIndex:index];
        }
    }
    @end
    

    大家发现了吗,__NSArrayI才是NSArray真正的类,而NSMutableArray又不一样😂。我们可以通过runtime函数获取真正的类:

    objc_getClass("__NSArrayI");
    

    举例

    下面我们列举一些常用的类簇的“真身”:

    “真身”
    NSArray __NSArrayI
    NSMutableArray __NSArrayM
    NSDictionary __NSDictionaryI
    NSMutableDictionary __NSDictionaryM

    其他自行Google....

    JRSwizzle

    在项目中我们肯定会在很多地方用到Method Swizzling,而且在使用这个特性时有很多需要注意的地方。我们可以将Method Swizzling封装起来,也可以使用一些比较成熟的第三方。
    在这里我推荐Github上星最多的一个第三方-jrswizzle

    里面核心就两个类,代码看起来非常清爽。

    #import <Foundation/Foundation.h>
    @interface NSObject (JRSwizzle)
    + (BOOL)jr_swizzleMethod:(SEL)origSel_ withMethod:(SEL)altSel_ error:(NSError**)error_;
    + (BOOL)jr_swizzleClassMethod:(SEL)origSel_ withClassMethod:(SEL)altSel_ error:(NSError**)error_;
    @end
    
    // MethodSwizzle类
    #import <objc/objc.h>
    BOOL ClassMethodSwizzle(Class klass, SEL origSel, SEL altSel);
    BOOL MethodSwizzle(Class klass, SEL origSel, SEL altSel);
    

    Method Swizzling 错误剖析

    在上面的例子中,如果只是单独对NSArrayNSMutableArray中的单个类进行Method Swizzling,是可以正常使用并且不会发生异常的。如果进行Method Swizzling的类中,有两个类有继承关系的,并且Swizzling了同一个方法。例如同时对NSArrayNSMutableArray中的objectAtIndex:方法都进行了Swizzling,这样可能会导致父类Swizzling失效的问题。

    对于这种问题主要是两个原因导致的,首先是不要在+ (void)load方法中调用[super load]方法,这会导致父类的Swizzling被重复执行两次,这样父类的Swizzling就会失效。例如下面的两张图片,你会发现由于NSMutableArray调用了[super load]导致父类NSArraySwizzling代码被执行了两次。

    错误代码:

    #import "NSMutableArray+LXZArrayM.h"
    
    @implementation NSMutableArray (LXZArrayM)
    
    + (void)load {
        // 这里不应该调用super,会导致父类被重复Swizzling
        [super load];
        
        Method fromMethod = class_getInstanceMethod(objc_getClass("__NSArrayM"), @selector(objectAtIndex:));
        Method toMethod = class_getInstanceMethod(objc_getClass("__NSArrayM"), @selector(lxz_objectAtIndexM:));
        method_exchangeImplementations(fromMethod, toMethod);
    }
    

    这里由于在子类中调用了super,导致NSMutableArray执行时,父类NSArray也被执行了一次。

    第一次

    父类NSArray执行了第二次Swizzling,这时候就会出现问题,后面会讲具体原因。

    第二次

    这样就会导致程序运行过程中,子类调用Swizzling的方法是没有问题的,父类调用同一个方法就会发现Swizzling失效了.....具体原因我们后面讲!

    还有一个原因就是因为代码逻辑导致Swizzling代码被执行了多次,这也会导致Swizzling失效,其实原理和上面的问题是一样的,我们下面讲讲为什么会出现这个问题。

    问题原因

    我们上面提到过Method Swizzling的实现原理就是对类的Dispatch Table进行操作,每进行一次Swizzling就交换一次SELIMP(可以理解为函数指针),如果Swizzling被执行了多次,就相当于SELIMP被交换了多次。这就会导致第一次执行成功交换了、第二次执行又换回去了、第三次执行.....这样换来换去的结果,能不能成功就看运气了😄,这也是好多人说Method Swizzling不好用的原因之一。

    一图胜千言:

    Dispatch Table 交换流程

    从这张图中我们也可以看出问题产生的原因了,就是Swizzling的代码被重复执行,为了避免这样的原因出现,我们可以通过GCDdispatch_once函数来解决,利用dispatch_once函数内代码只会执行一次的特性。

    在每个Method Swizzling的地方,加上dispatch_once函数保证代码只被执行一次。当然在实际使用中也可以对下面代码进行封装,这里只是给一个示例代码。

    #import "NSMutableArray+LXZArrayM.h"
    
    @implementation NSMutableArray (LXZArrayM)
    
    + (void)load {
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            Method fromMethod = class_getInstanceMethod(objc_getClass("__NSArrayM"), @selector(objectAtIndex:));
            Method toMethod = class_getInstanceMethod(objc_getClass("__NSArrayM"), @selector(lxz_objectAtIndexM:));
            method_exchangeImplementations(fromMethod, toMethod);
        });
    }
    

    这里还要告诉大家一个调试小技巧,已经知道的可以略过😊。我们之前说过IMP本质上就是函数指针,所以我们可以通过打印函数指针的方式,查看SELIMP的交换流程。

    先来一段测试代码

    Method fromMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(objectAtIndex:));
    Method toMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(lxz_objectAtIndex:));
        
    NSLog(@"%p", method_getImplementation(fromMethod));
    NSLog(@"%p", method_getImplementation(toMethod));
    method_exchangeImplementations(fromMethod, toMethod);
        
    NSLog(@"%p", method_getImplementation(fromMethod));
    NSLog(@"%p", method_getImplementation(toMethod));
    method_exchangeImplementations(fromMethod, toMethod);
        
    NSLog(@"%p", method_getImplementation(fromMethod));
    NSLog(@"%p", method_getImplementation(toMethod));
    method_exchangeImplementations(fromMethod, toMethod);
        
    NSLog(@"%p", method_getImplementation(fromMethod));
    NSLog(@"%p", method_getImplementation(toMethod));
    

    看到这个打印结果,大家应该明白什么问题了吧:

    2016-04-13 14:16:33.477 [16314:4979302]      0x1851b7020
    2016-04-13 14:16:33.479 [16314:4979302]      0x1000fb3c8
    2016-04-13 14:16:33.479 [16314:4979302]      0x1000fb3c8
    2016-04-13 14:16:33.480 [16314:4979302]      0x1851b7020
    2016-04-13 14:16:33.480 [16314:4979302]      0x1851b7020
    2016-04-13 14:16:33.480 [16314:4979302]      0x1000fb3c8
    2016-04-13 14:16:33.481 [16314:4979302]      0x1000fb3c8
    2016-04-13 14:16:33.481 [16314:4979302]      0x1851b7020
    

    Method Swizzling源码分析

    下面是Method Swizzling的实现源码,从源码来看,其实内部实现很简单。核心代码就是交换两个Methodimp函数指针,这也就是方法被swizzling多次,可能会被换回去的原因,因为每次调用都会执行一次交换操作。

    void method_exchangeImplementations(Method m1, Method m2)
    {
        if (!m1  ||  !m2) return;
    
        rwlock_writer_t lock(runtimeLock);
    
        IMP m1_imp = m1->imp;
        m1->imp = m2->imp;
        m2->imp = m1_imp;
    
        flushCaches(nil);
    
        updateCustomRR_AWZ(nil, m1);
        updateCustomRR_AWZ(nil, m2);
    }
    

    Method Swizzling危险吗?

    既然Method Swizzling可以对这个类的Dispatch Table进行操作,操作后的结果对所有当前类及子类都会产生影响,所以有人认为Method Swizzling是一种危险的技术,用不好很容易导致一些不可预见的bug,这些bug一般都是非常难发现和调试的。

    这个问题可以引用念茜大神的一句话:使用 Method Swizzling 编程就好比切菜时使用锋利的刀,一些人因为担心切到自己所以害怕锋利的刀具,可是事实上,使用钝刀往往更容易出事,而利刀更为安全。


    在这个Demo中通过Method Swizzling,简单实现了一个崩溃拦截功能。实现方式就是将原方法Swizzling为自己定义的方法,在执行时先在自己方法中做判断,根据是否异常再做下一步处理。

    Demo只是来辅助读者更好的理解文章中的内容,应该博客结合Demo一起学习,只看Demo还是不能理解更深层的原理Demo中代码都会有注释,各位可以打断点跟着Demo执行流程走一遍,看看各个阶段变量的值。

    Demo地址刘小壮的Github


    简书由于排版的问题,阅读体验并不好,布局、图片显示、代码等很多问题。所以建议到我Github上,下载Runtime PDF合集。把所有Runtime文章总计九篇,都写在这个PDF中,而且左侧有目录,方便阅读。

    Runtime PDF

    下载地址:Runtime PDF
    麻烦各位大佬点个赞,谢谢!😁

    相关文章

      网友评论

      • 凉风起君子意如何:这么长,竟然还认真的看完了,写的清楚易懂,谢谢分享!
      • f94e716d03fa:反手两个赞
      • 驿路梨花处处开:楼主,如果说跳转不同的页面打点的参数不同,这个方法交换还行吗?比如说跳转到vc1 打点c1,跳转到vc2,打点c2,依此类推,用交换可行?
        刘小壮:带参数打点确实不太好弄,这块比较通用的方法就是,定义一个协议,需要传参的控制器遵循协议,并把参数以字典的形式传进去,在交换的方法中调用此方法(实际逻辑会更复杂,不会直接调用)。
      • wokenshin:感谢大神分享!
      • pFruHMXB:为什么是0评论呢?
        感谢作者!
      • IMKel:博主,你上面分类里面重写+load方法没调用下[super load]没问题吗?你不调用的理由是什么?
        刘小壮:至于为什么不用调super,请参考我Runtime文章https://www.jianshu.com/p/4fb2d7014e9e
        刘小壮:不需要调super
      • 娶什么名字呢:讲得很详细,感谢🙏
      • 铃鹿山大魔王:方法交换类簇,我说呢,我用runtime交换字典的方法,怎么换不了。原来其内部真正执行的类并非NSDictionary。
        刘小壮:文中我直接把类名写死了,这种方式还是不够灵活,如果以后升级系统版本,类簇内部的类发生改变,那就获取不到了。
        刘小壮:是的,最好是通过runtime的方式,获取真正的类。
      • 普罗格兰姆:很系统地讲解了一遍swizzling,对小白是很亲和的。而且还讲了一些容易碰的坑,以及很实用的例子。好闻收藏点赞
      • _小沫:界面打点使用method swizzling定制性太差,最好的方式应该可以用Aspects这个库
        刘小壮:嗯,Aspects挺好用的,内部做了很多容错。
      • 客三消:博主写的很好。早就保存了,今天才拿出来看。发现一个问题。
        NSArray 的属性count 是unsigned long类型
        @property (readonly) NSUInteger count;
        在做判断的时候self.count - 1。如果self.count == 0。 self.count - 1 为很大的正整数。
        所以会直接走else然后崩溃。
        刘小壮:多谢提醒,我有时间试试。
      • 塔罗师_Michael:好喜欢看你装逼
        刘小壮:关注我博客,带你飞。
      • 逆光少年:空数组越界还是会崩
        刘小壮:@逆光少年 是的,根据系统版本,判断交换不同的方法。
        逆光少年:@刘小壮 *** Terminating app due to uncaught exception 'NSRangeException', reason: '*** -[__NSArray0 objectAtIndex:]: index 2 beyond bounds for empty NSArray',需要再加两个交换方法
        刘小壮:是的,你应该是iOS11的系统。iOS11的系统,字面量不会调用objectAtIndex:方法。
      • 执拗的男孩:-(void)swizzlingViewDidLoad
        {
        NSString *str = [NSString stringWithFormat:@"%@",self.class];
        if (![str containsString:@"UI"]) {
        NSLog(@"统计:%@",self.class);
        }

        // [self swizzlingViewDidLoad];
        }我写了两个类 一个是类别 一个是继承 使用类别的时候回出现循环调用 发现把// [self swizzlingViewDidLoad];注释了就没有问题了。
        刘小壮:应该是代码逻辑的问题,你看看viewDidLoad里有没有调用swizzlingViewDidLoad方法,梳理一下逻辑。
      • YYYYYY25:method_getImplementation()审核不是会被拒吗?不知道有没有这种情况,我在犹豫要不要应用到公司项目中,求解ing!~
        YYYYYY25:@刘小壮 好的
        刘小壮:不会被拒的,这个API可以用,很多公司都用了。
      • MoussyL:谢谢作者的分享~~~ 👏
        有个问题,想问下你:
        我写了个demo ,有一个导航控制器,首页里有个tableview ,包含3个cell,点击任何一个cell都会 push到另一个 VC,现在按作者的方法,swizzling 了 viewDidLoad 进行打点,我发现,程序运行后,首先有2个打点 UINavigationController、HomeController,这都是正常的,但是当我第一次点击某一个cell的时候,会有2个打点:CustomViewController、UIViewController,再点击其他cell的时候都是正常打点只打一个。
        问题是,为什么会出现这种情况,可以解释一下原因么?只有第一次点击某个 cell push到其他VC 的时候,会有一个 UIViewController 的打点。
        万分谢谢~~:pray:
        MoussyL:@刘小壮 只有第一次点击某个Cell 的时候,除了打点要push到的那个 VC ,还会多打印一个 UIViewController 。不知道这样描述你清楚没~ :joy:
        MoussyL:@刘小壮 不是,点击cell都跳转的是不同的 VC,第一个cell ---> VC1,第二个cell ---> VC2,第三个cell ---> VC3,每个 VC 都继承于 UIViewController,当我运行demo后,如果点击第一个cell ,打印“打点:VC1;打点:UIViewController”,点击第二个就只有“打点:VC2”,点击第三个“打点:VC3”,再点击第一个cell ,就只有“打点:VC1”;
        刘小壮:点击Cell跳转控制器的代码都一样吗,都跳转同一个控制器吗?
      • SoaringHeart:如果需要给点击方法和手势添加统计方法呢?
        刘小壮:UIButton可以Swizzling下面的方法,手势不太好处理,要对私有方法做处理。
        - (void)sendAction:(SEL)action to:(nullable id)target forEvent:(nullable UIEvent *)event;
      • e2b3ec44b0ac:您好,我发现NSString为什么可以直接hook呢?不需要去swizzle“真身”?代码如下: 谢谢您的解答

        #import "NSString+Hooker.h"
        #import <objc/runtime.h>
        @Implementation NSString (Hooker)
        + (void)load {
        Method fromMethod1 = class_getInstanceMethod([NSString class], @Selector(initWithFormat:));
        Method toMethod1 = class_getInstanceMethod([NSString class], @Selector(myInitWithFormat:));
        method_exchangeImplementations(fromMethod1, toMethod1);


        Method fromMethod2 = class_getClassMethod([NSString class], @Selector(stringWithFormat:));
        Method toMethod2 = class_getClassMethod([NSString class], @Selector(myStringWithFormat:));
        method_exchangeImplementations(fromMethod2, toMethod2);
        }

        +(instancetype)myStringWithFormat:(NSString *)format, ...{
        NSLog(@"string format is :%@", format);
        return [self myStringWithFormat:format];
        }

        -(instancetype)myInitWithFormat:(NSString *)format, ... {
        NSLog(@"init format is: %@",format);
        return [self myInitWithFormat:format];
        }
        @EnD
        刘小壮:@请叫我污博 具体原因不太清楚,感觉可能是编译器有优化,或者是类簇实现方式不同吧。
        e2b3ec44b0ac:@刘小壮 emmm 我只是好奇请教一下~ 因为NSString和NSArray都是class cluster,为啥会有这样的区别呢~求指点,O(∩_∩)O谢谢
        刘小壮:我也没说NSString需要swizzling“真身”啊:sweat:
      • 风吹柳絮如花落:我有一个问题,为什么数组上内为@[1,2,3,4],下标取4的时候,取得是第三个?
        风吹柳絮如花落:我用的是文中提供的demo,看了一下下标取4取出来的是第三个
        刘小壮:如果说取数字4的话,那应该取下标3
        刘小壮:取下标4应该越界了吧
      • 小小志伟:想问下 这个黑魔法还能解决其他的什么问题
        小小志伟:@刘小壮 嗯 受教了
        刘小壮:例如NSArray、NSDictionary的合法性校验,在dealloc里统一加入removeNotification之类的
        刘小壮:主要就是方法交换,你在交换之后可以根据业务需求做任何操作。
      • 东岳哥哥:很赞:+1:

        我有个疑问:在工程中使用这黑魔法,上架会不会被拒呀
        刘小壮:不会被拒,很多应用都在使用。别因为Method Swizzling引起其他bug就行
      • 逆光少年:你好,我有几个问题请教一下。
        1.为防止多次交换,用dispatch_once解决,demo中怎么没实现呢;
        2.demo中NSArray这个分类并没有看到其他地方引入,怎么就实现了方法交换的功能。
        刘小壮:“demo中NSArray这个分类并没有看到其他地方引入,怎么就实现了方法交换的功能。”,这个问题你再看一下文章,文章里讲的很清楚。
        刘小壮:demo里是应该加上的,当时觉得代码逻辑肯定没问题,所以就没加,加上的话更严谨。
        逆光少年:还有一个问题,如何使分类在生产环境使用,非生产环境不使用
      • 路漫漫其修远兮Wzt:之前只知道用,看完这篇文章感觉对runtime swizzling的理解又加深了一层。学习永无止境,赞一个...
        刘小壮:多谢鼓励:smile:
      • a7642f69975b:您好,我有个问题请教。
        文中说用“继承”方式实现统计的时候,统计的不是基类的吗?怎么统计到子类里的呢?
        a7642f69975b:@刘小壮 知道了,谢谢,是我误解了runtime消息发送的机制
        刘小壮:假设需要统计每个页面进入和离开,就直接写在基类的viewDidAppear:和viewDidDisappear:就可以
      • wangyu2488:您好,你得Crash自动防护系统,有没有完成版的,方便给下链接不,比如kvo之类的奔溃保护。另外,看了你这篇文章demo,已经知道如何数组越界保护了(谢谢了)
        wangyu2488:@刘小壮 我github上找了一个 avoidcrash
        刘小壮:等我以后写了,第一个发给你
        刘小壮:本来想写个防崩溃机制来着,后来一直没时间。。。
      • 乐视薯片:你好,我想请教个问题,我需要捕捉到手势,因此,hook了UIGestureRecognizer的addTarget:action:方法,现在发现一个问题,点击UITextField 就会崩溃,我也没做什么处理啊,楼主可以帮忙看看吗?
        刘小壮:可能是UITextField也用了UIGestureRecognizer手势,你处理手势的时候对UITextField不兼容。
      • 神一样的男人卫:写的很好哦!谢谢大神分享!
      • IamHenry:谢谢分享!
      • 丘比龙:lxz_objectAtIndex:(NSUInteger)index这个方法为什么会调用很多次,可以解释一下吗?
        刘小壮:有的时候是系统调的
      • ce70df11c8e3:大兄弟,你那个数组越界异常捕获那样写永远捕获不到吧???因为因为你是在崩了之后才去捕获的
        刘小壮:不太明白您说的意思。swizzling是写在load方法中的,这个方法在class加载的时候,就会被系统执行的。
      • o0下一站生活0o:可NSArry打印出来是这个东西 怎么办。__NSSingleObjectArrayI
        刘小壮:在iOS10之后,NSArray会出现__NSSingleObjectArrayI。NSArray的类簇可能会出现三种类型,还可能会有__NSArray0的出现。所以建议把这三种都swizzling。
      • petter102:写的非常好,感谢分享,简书有你更精彩!:smiley:
      • small路飞:谢谢分享
      • EvilNOP:Apple官方文档说class_addMethod的返回值:
        YES if the method was added successfully, otherwise NO (for example, the class already contains a method implementation with that name).

        那么,class_addMethod([self class], @selector(viewDidLoad), method_getImplementation(toMethod), method_getTypeEncoding(toMethod)), 显然给UIViewController swizzle已存在的方法是不会返回YES的,为什么不直接调用method_exchangeImplementations呢?
        小弟不是很懂,在这里问一下 :smile:
        刘小壮:@EvilNOP 保证万无一失,万一方法名被改了,swizzling的方法名没变,不就崩了吗。 :smile:
        EvilNOP:@刘小壮 确实添加了可以验证代码,增加代码健壮性,很多时候写代码明知道不会发生还是要去验证:joy::joy:
        刘小壮:@EvilNOP 太客气了。 文中class_addMethod([self class], @selector(viewDidLoad), method_getImplementation(toMethod)方法实际上@selector是写错了的,toMethod应该对应的是swizzlingViewDidLoad(我马上更新文章,谢谢指出)。但是在swizzling通过class_addMethod()函数对被swizzling的方法进行验证还是需要的,如果被swizzling方法不存在,就可以通过class_addMethod()动态添加,否则会导致崩溃。
      • xclidongbo:文章写得不错.
        刘小壮:@xclidongbo 老李
      • Claudlit:解决了我的疑惑,谢谢...
        刘小壮:@Claudlit :blush:
      • HelloEverything:可以 可以 感谢小壮壮 解决我之前的一个疑惑
        刘小壮:@HelloEverything :smile:
      • Pusswzy:您好我们可以通过下面的这段伪代码来看一下:

        #import "UIViewController+EventGather.h"
        @Implementation UIViewController (EventGather)
        - (void)viewDidLoad {
        NSLog(@"页面统计:%@", self);
        }
        @EnD

        不是不可以在分类中写系统方法么? 会导致原有的系统方法失效的啊?
        刘小壮:@薛定谔的熊 在这里只是简单举个例子,这种Category的方式只对系统方法起作用,对自己的方法不起作用。而且Swizzling可以控制调用先后顺序,Category只能Category中的方法先执行。
        薛定谔的熊:@刘小壮 请问既然能用category重写viewDidLoad方法来实现,为什么还要用黑魔法?有什么优势?
        刘小壮:@Pusswzy 上面例子中,给UIViewController的viewDidLoad方法进行category,首先会执行category中的方法,然后再执行原类的,category的优先级更高。你可以写个demo试一下。
      • 赤脊山的豺狼人:赞,讲的通透
        刘小壮:@赤脊山的豺狼人 :smile:
      • abcc5e2391a3:然而...并不会 :smirk: 哈哈哈
        刘小壮:@StephenCurry30 过两天给所有文章配Demo,到时候可以照着Demo敲一敲 :smile:
      • ______DS___:不错 :smile:
      • Tate_code:感觉这个可以实现很多的需求,不错
      • 7c56a5b1c254:写的很好。
        刘小壮:@新地球说着一口陌生腔调 友盟可以打点。但是如果之前项目没有加打点功能,现在突然让给所有页面加上打点,每个页面都Copy代码有点太麻烦了,所以可以用Swizzling来实现这个功能。
        新地球说着一口陌生腔调:友盟不是能解决这样的需求吗
        刘小壮:@伊眸冷 谢谢 :blush:
      • 60343a0ad510: :smiley: :smiley: 用得到的黑魔法。
        刘小壮:@晋先森 加油 :+1:
        刘小壮:@晋先森 这个技术点用的好可以解决很多问题,还是应该多实践 :blush:

      本文标题:iOS黑魔法-Method Swizzling

      本文链接:https://www.haomeiwen.com/subject/detlkttx.html