美文网首页
runtime - Method Swizzling

runtime - Method Swizzling

作者: 啊啊啊啊锋 | 来源:发表于2016-07-07 11:53 被阅读121次

    在开发中,如果我们想要改变某个类的方法或者替换为自己的方法,无外乎以下几种方式:

    1. 继承这个类,然后对这个方法进行重写;

    2. 通过分类直接改变方法实现;

    3. 利用runtime的 Method Swizzling 进行方法替换。

    什么是 Method Swizzling ?

    Method Swizzling顾名思义,就是方法调和的意思,这里我们可以简单理解为方法的改写。比较正式的解释可以理解为改变一个已存在的选择器对应的实现的过程,它依赖于Objectvie-C中方法的调用能够在运行时进改变——通过改变类的调度表dispatch table中选择器到最终函数间的映射关系。

    Method Swizzling 原理

    在OC中,调用方法既是给对象发送消息,而查找消息的依据则是根据selector名称,在OC中,每个selector都对应着相应的方法实现存放在方法分发表(dispatch table)里边,如果我们在程序运行时人为地改变selector和方法实现(IMP)的对应关系,那么就达到了方法改变的目的,也就是Method Swizzling。

    selector和IMP的一一对应关系如下图所示:

    图片来自`念茜`的博客

    从上边的图片可以看出,每个selector都唯一地对应着一个IMP,利用Method Swizzling我们可以达到下图所示的效果:

    图片来自`念茜`的博客

    正常的话selectorC -> IMPc,selectorN -> IMPn,但是通过Method Swizzling,们可以达到图中的效果,即让selectorC -> IMPn,selectorN -> IMPc。其实归根结底,我们只是改变了selector和IMP的对应关系而已。

    Method Swizzling 怎么使用?

    Method Swizzling应该放在+ (void)load方法中,因为Method Swizzling影响范围是全局性的,而放在+ (void)load方法里边能够保证在类最开始加载的时候就执行,而且还可能会避免一些难以解决的bug。也有人说应该使用dispatch_once来保证线程安全,关于应不应该放在dispatch_once里边,笔者在这里并不是很清楚,如果有知道的朋友还请指教。

    可能会用到的API有以下几个:

    /**
     *  返回某个类的具体实例方法
     *  aClass:要操作的那个类
     *  aSelector:某个方法的selector
    */
    Method class_getInstanceMethod(Class aClass, SEL aSelector) 
    
    /**
     *  给某个类增加新方法,如果增加成功,返回YES;否则返回NO
     *  cls:要增加方法的类
     *  name:要增加方法的selecor
     *  imp:新方法的方法实现
     *  types:一连串的字符串用来标识方法和参数类型,这个参数在之前文章一提到过
    */
    BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types) 
    
    /**
     *  交换两个方法的实现
     *  m1:被交换的方法
     *  m2:交换的方法
    */
    void method_exchangeImplementations( Method m1, Method m2) 
    

    使用实例一

    参考这篇文章的思路,如果我们要实现用户点击某个页面时进行统计功能:

    UIViewController增加分类Swizzling,在.m文件中

    + (void)load 
    {
        [super load];
        
        // 通过class_getInstanceMethod()函数从当前对象中的method list获取method结构体,如果是类方法就使用class_getClassMethod()函数获取。
        Method fromMethod = class_getInstanceMethod([self class], @selector(viewDidLoad));
        Method toMethod = class_getInstanceMethod([self class], @selector(zf_ViewDidLoad));
        
        // 如果`class_addMethod()`返回YES,这里我们才执行方法交换的动作
        if (!class_addMethod([self class], @selector(viewDidLoad), method_getImplementation(toMethod), method_getTypeEncoding(toMethod))) {
        method_exchangeImplementations(fromMethod, toMethod);
        }
    }
    
    // 我们自己实现的方法,也就是和self的viewDidLoad方法进行交换的方法。
    - (void)zf_ViewDidLoad 
    {
        NSString *str = [NSString stringWithFormat:@"%@", self.class];
        // 我们在这里加一个判断,将系统的UIViewController的对象剔除掉
        if(![str containsString:@"UI"]){
            NSLog(@"统计打点 : %@", self.class);
        }
        
        // 注意这里,可能会有疑惑,这样不就造成循环引用了么?其实并不会,当我们调用下边这句代码的时候,其实调用的是系统的`[self ViewDidLoad]`方法,如果不明白就去看看上边那个图
        [self swizzlingViewDidLoad];
    }
    

    #import分类文件,这样当我们运行的时候就会得到我们想要的结果了:

    2016-07-07 11:04:57.582 ZFRuntime[831:72600] 统计打点 : ViewController
    

    使用实例二(通用交换IMP写法)

    因为大部分类都继承自NSObject类,为了扩展到大部分类,我们这里给它搞一个分类Swizzling,在.h文件中增加一个类方法:

    + (void)swizzleSelector:(SEL)oriSelector withSwizzledSelector:(SEL)swiSelector;
    

    .m中,我们来实现它:

    + (void)swizzleSelector:(SEL)oriSelector withSwizzledSelector:(SEL)swiSelector
    

    {
    Class class = [self class];

    // ‘class_getInstanceMethod’获取类实例方法,不要和‘class_getClassMethod’获取类方法混错
    Method oriMethod = class_getInstanceMethod(class, oriSelector);
    Method swiMethod = class_getInstanceMethod(class, swiSelector);
    
    /**
     *  BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types)
     *  说明:‘class_addMethod’为类动态添加方法
     *  cls   为哪个类添加方法
     *  name  方法名(这个名字似乎可以随便起)
     *  imp   这个方法的实现(实现函数比不至少带有两个参数(self _cmd),这个参数可用过‘method_getImplementation’方法获取)
     *  types 定义该方法返回值类型和参数类型的字符串(官方文档:由于函数至少带有有两个参数self _cmd,所以type的字符串第2、3个字符必须是‘@:’,至于第一个字符是什么,要依据返回值类型而定。这个参数的获取可以使用‘ const char * method_getTypeEncoding( Method method) ’这个方法)
        更多请参考:https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtTypeEncodings.html#//apple_ref/doc/uid/TP40008048-CH100
     *
     *  @return 如果方法添加成功,返回YES;否则返回NO
     */
    BOOL didAddMethod = class_addMethod(class, oriSelector, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
    
    // 只有当方法添加成功的时候我们才进行替换的动作
    if (didAddMethod) {
        /**
         *  ‘IMP class_replaceMethod(Class cls, SEL name, IMP imp, const char *types)’
         *  (替换某个类的方法实现)
         *  cls   要修改的类
         *  name  新的方法的方法名
         *  imp   原方法实现
         *  types 同上
         *
         *  @return 新方法实现
         */
        class_replaceMethod(class, swiSelector, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
    }else {
        /**
         *  ‘void method_exchangeImplementations( Method m1, Method m2)’
         *  交换两个方法的实现
         *  m1 原方法
         *  m2 新的方法
         */
        method_exchangeImplementations(oriMethod, swiMethod);
        }
    }
    

    开发中经常会遇到数组越界或者字典去取插入空值等情况,通过这里的学习我们就可以对其进行处理。

    我们可以为NSArray增加分类Swizzling,在.m中我们这样做:

    + (void)load
    {
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
        [self swizzleSelector:@selector(lastObject) withSwizzledSelector:@selector(zf_lastObject)];
        });
        
        // NSArray真正的类是`__NSArrayI`而不是`NSArray`
        Method fromMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(objectAtIndex:));
        Method toMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(zf_objectAtIndex:));
        method_exchangeImplementations(fromMethod, toMethod);
    }
    
    - (id)zf_lastObject
    {
        if (self.count == 0) {
            // NSLog(@"空数组 %s", __func__);
            return nil;
        }
        // 调用自己,这里因为方法交换了,其实调用的是系统的‘lastObject’方法
        return [self zf_lastObject];
    }
    
    - (id)zf_objectAtIndex:(NSUInteger)index {
        if (self.count-1 < index) {
            // 这里做一下异常处理,不然都不知道出错了。
            @try {
                return [self zf_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 zf_objectAtIndex:index];
        }
    }
    

    这样当我们对一个数组个数为0的数组进行操作时,就可以实现我们自己想要的操作了。

    还可以实现对NSMutableArray崩溃的处理,同样的我们给NSMutableArray增加分类Swizzling,在.m文件中,我们:

    + (void)load
    {
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
        
        // NSMutableArray真正的类是`__NSArrayM`,而不是`NSMutableArray`
            [objc_getClass("__NSArrayM") swizzleSelector:@selector(objectAtIndex:) withSwizzledSelector:@selector(zf_objectAtIndex:)];
        });
    }
    
    - (id)zf_objectAtIndex:(NSUInteger)index {
        if (self.count == 0) {
            NSLog(@"%s can't get any object from an empty array", __FUNCTION__);
            return nil;
        }
    
        if (index > self.count) {
            NSLog(@"%s index out of bounds in array", __FUNCTION__);
            return nil;
        }
    
        return [self zf_objectAtIndex:index];
    }
    

    这样当我们在取数据越界时就不会崩溃了,同样地还可以改变其他方法,都是类似的步骤。

    注释:上边中关于NSArray NSDictionary类簇问题,可以参考这里这里,github上也有对Method Swizzling的封装,这里推荐这个

    如果Method Swizzling用不好,可能会产生很多头疼的问题,但是用好了,会在我们项目开发中带来极大的方便。(stackoverflow上对Method Swizzling的评价)

    另:本文参考了如下博客,感谢原作者

    Demo地址

    相关文章

      网友评论

          本文标题:runtime - Method Swizzling

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