美文网首页
使用Method swizzling方法修改常用函数行为

使用Method swizzling方法修改常用函数行为

作者: Fisher123 | 来源:发表于2018-08-08 11:13 被阅读0次

    我们知道,NSArray的objectAtIndex方法若使用了一个超出范围的index值,那么将会抛出异常导致程序终止运行。
    然而在发布的App中,其实NSArray的异常有些小题大做了。即使显示数据为空,也比Crash掉的用户体验要好的多啊。(例如初始化的Cell在填充数据时,objectAtIndex获取数据index越界导致异常)
    因此,我就写了一个NSArray的Category:

    - (id)safeObjectAtIndex:(NSUInteger)index {
        if (index < self.count) {
            return [self objectAtIndex:index];
        } else {
            return nil;
        }
    }
    
    

    解决问题是解决了,可是很不好看,也没有办法保证其他成员也一定使用这个方法啊。再者,这样是无法对新语法(Objective-C Literals)起作用的(这点是硬伤……)。
    突然想到了之前看过的Method swizzling的方法,利用Runtime特性,可以替换现有方法的实现。在这里使用在合适不过了。(深入阅读:深入浅出Cocoa之Method Swizzling
    那么试试看吧。如果你看了上面提到的那篇文章,应该发现了jrswizzle——Method swizzling方法的封装。我们就不造轮子了,用CocoaPods将它加入到我们的项目中吧。
    首先创建NSArray的Category,当DEBUG模式时,index超出范围将抛出异常,而Release版本中只会返回nil:

    @interface NSArray (SafeCategory)
    
    - (id)TKSafe_objectAtIndex:(NSUInteger)index;
    
    @end
    
    @implementation NSArray (SafeCategory)
    
    - (id)TKSafe_objectAtIndex:(NSUInteger)index {
        if (index < self.count) {
            return [self TKSafe_objectAtIndex:index];
        } else {
    #ifdef DEBUG
            NSAssert(NO, @"index %d > count %d", index, self.count);
    #endif
            return nil;
        }
    }
    
    @end
    
    
    

    然后在程序中调用替换方法:

    #import <JRSwizzle/JRSwizzle.h>
    
    NSError *error = nil;
    [NSArray jr_swizzleClassMethod:@selector(objectAtIndex:)
             withClassMethod:@selector(TKSafe_objectAtIndex:)
             error:&error];
    NSLog(@"Err: %@", error);
    
    

    悲剧,出错了:

    Err: Error Domain=NSCocoaErrorDomain Code=-1 "+[NSObject(JRSwizzle) jr_swizzleMethod:withMethod:error:]: original method objectAtIndex: not found for class NSArray" UserInfo=0x1754d770 {NSLocalizedDescription=+[NSObject(JRSwizzle) jr_swizzleMethod:withMethod:error:]: original method objectAtIndex: not found for class NSArray}
    
    

    它说没有在NSArray中找到objectAtIndex方法,Why?
    这里的NSArray是一种特殊的类,英文叫做Class cluster,中文翻译过来是类簇,在设计模式中,这个叫做工厂类,它在外层提供了很多方法接口,但是这些方法的实现是由具体的内部类来实现的。当使用NSArray生成一个对象时,初始化方法会判断哪个“自己内部的类”最适合生成这个对象,然后这个“工厂”就会生成这个具体的类对象返回给你。这种又外层类提供统一抽象的接口,然后具体实现让隐藏的,具体的内部类来实现,在设计模式中称为“抽象工厂”模式。(引用自objc’s category and class cluster)
    也就是说,对于普通的类,我们使用上述方法是没有问题的,然而对于Class cluster这种工厂类,就需要找到它的真身才行。
    通过对Class cluster的了解,我们明白了平时使用的NSArray实例其实是另外一种类型,让我们看看它们的真身:

    NSLog(@"%@", [NSArray class]);
    NSLog(@"%@", [[NSArray array] class]);
    NSLog(@"%@", [[NSMutableArray array] class]);
    
    

    运行查看输出,应该是如下内容:

    NSArray
    __NSArrayI
    __NSArrayM
    
    

    第一行调用NSArray类方法class,当然是它自己;第二行调用[NSArray array]方法返回了一个实例,类型是__NSArrayI;第三行调用了[NSMutableArray array]方法返回了可变数组的实例,类型是__NSArrayM。
    现在已经知道了待替换方法类的名称,获得它的Class就很随意了,我们有三种方法:

    • Class class = [[NSArray array] class];
    • Class class = NSClassFromString(@”__NSArrayI”);
    • Class class = objc_getClass(“__NSArrayI”);

    第二种和第三种方式基本相同,都是根据类名字符串获得Class,但第三种写法编译器会报警告:Implicitly declaring library function ‘objc_getClass’ with type ‘id (const char *)’。第一种通过实体类获得Class,从安全性角度来说,我倾向于使用第一种。
    最终代码如下,当然,我们还可以修改更多,这里有一份例子:

    [[[NSArray array] class] jr_swizzleMethod:@selector(objectAtIndex:)
                             withMethod:@selector(TKSafe_objectAtIndex:)
                             error:&error];
    
    

    这下新版本的Crash数量又降低了,想想还有点小激动呢。

    相关文章

      网友评论

          本文标题:使用Method swizzling方法修改常用函数行为

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