美文网首页iOSios学习资料iOS AOP
利用runtime解决数组字典的崩溃问题

利用runtime解决数组字典的崩溃问题

作者: 大熊孩子 | 来源:发表于2016-05-25 11:55 被阅读2549次

    前言

    我们在平时的工作中经常会遇到这样一种情况,当我们从后台请求到的数据,需要把其中一个插入到数组的时候,需要先判断该对象是否为空值,非空才能插入,否则会引起崩溃。

    那么有没有一种方式,可以从根本上解决,即使我插入的是空值,也不会引起崩溃呢:

    1.继承于这个类,然后通过重写方法(很常用,比如基类控制器,可以在视图加载完成时做一些公共的配置等)
    
    2.通过类别重写方法,暴力抢先(此法太暴力,尽量不要这么做)
    
    3.swizzling(本文特讲内容)
    

    Swizzling原理

    在Objective-C中调用一个方法,其实是向一个对象发送消息,而查找消息的唯一依据是selector的名字。所以,我们可以利用Objective-C的runtime机制,实现在运行时交换selector对应的方法实现以达到我们的目的。

    每个类都有一个方法列表,存放着selector的名字和方法实现的映射关系。IMP有点类似函数指针,指向具体的Method实现。

    我们先看看SEL与IMP之间的关系图:

    但是,现在我们要做的就是把链接线解开,然后连到我们自定义的函数的IMP上。当然,交换了两个SEL的IMP,还是可以再次交换回来了。交换后变成这样的,如下图:

    从图中可以看出,我们通过swizzling特性,将selectorC的方法实现IMPc与selectorN的方法实现IMPn交换了,当我们调用selectorC,也就是给对象发送selectorC消息时,所查找到的对应的方法实现就是IMPn而不是IMPc了。

    在+load方法中交换

    Swizzling应该在+load方法中实现,因为+load方法可以保证在类最开始加载时会调用。因为method swizzling的影响范围是全局的,所以应该放在最保险的地方来处理是非常重要的。+load能够保证在类初始化的时候一定会被加载,这可以保证统一性。试想一下,若是在实际时需要的时候才去交换,那么无法达到全局处理的效果,而且若是临时使用的,在使用后没有及时地使用swizzling将系统方法与我们自定义的方法实现交换回来,那么后续的调用系统API就可能出问题。

    类文件在工程中,一定会加载,因此可以保证+load会被调用。

    不要在+initialize中交换

    +initialize是类第一次初始化时才会被调用,因为这个类有可能一直都没有使用到,因此这个类可能永远不会被调用。

    类文件虽然在工程中,但是如果没有任何地方调用过,那么是不会调用+initialize方法的。

    使用dispatch_once保证只交换一次

    方法交换应该要线程安全,而且保证只交换一次,除非只是临时交换使用,在使用完成后又交换回来。

    最常用的用法是在+load方法中使用dispatch_once来保证交换是安全的。因为swizzling会改变全局,我们需要在运行时采取相应的防范措施。保证原子操作就是一个措施,确保代码即使在多线程环境下也只会被执行一次。而diapatch_once就提供这些保障,因此我们应该将其加入到swizzling的使用标准规范中。

    通用交换IMP写法

    网上有很多的版本,但是有很多是不全面的,考虑的范围不够全面。下面我们来写一个通用的写法,现在扩展到NSObject中,因为NSObject是根类,这样其它类都可以使用了:

    @interface NSObject (Swizzling) 
    
    + (void)swizzleSelector:(SEL)originalSelector withSwizzledSelector:(SEL)swizzledSelector; 
    
    @end
    
    
    #import "NSObject+Swizzling.h"
    
    #import <objc/runtime.h>
    
    // 实现代码如下
    
    @implementation NSObject (Swizzling)
    
    + (void)swizzleSelector:(SEL)originalSelector withSwizzledSelector:(SEL)swizzledSelector 
    {
        Class class = [self class];
    
        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);
        }
    }
    @end
    

    因为方法可能不是在这个类里,可能是在其父类中才有实现,因此先尝试添加方法的实现,若添加成功了,则直接替换一下实现即可。若添加失败了,说明已经存在这个方法实现了,则只需要交换这两个方法的实现就可以了。

    尽量使用method_exchangeImplementations函数来交换,因为它是原子操作的,线程安全。尽量不要自己手动写这样的代码:

    IMP imp1 = method_getImplementation(m1);
    IMP imp2 = method_getImplementation(m2);
    method_setImplementation(m1, imp2);
    method_setImplementation(m2, imp1);
    

    虽然method_exchangeImplementations函数的本质也是这么写法,但是它内部做了线程安全处理。

    简单使用swizzling

    最简单的方法实现交换如下:

    Method originalMethod = class_getInstanceMethod([NSArray class], @selector(lastObject));
    Method newMedthod = class_getInstanceMethod([NSArray class], NSSelectorFromString(@"safeLastObject"));
    method_exchangeImplementations(originalMethod, newMedthod);
    
    // NSArray提供了这样的实现
    - (id) safeLastObject {
      if (self.count == 0) {
        NSLog(@"%s 数组为空,直接返回nil", __FUNCTION__);
        return nil;
      }
      return [self safeLastObject];
    }
    

    看到safeLastObject这个方法递归调用自己了吗?为什么不是调用return [self safeLastObject]?因为我们交换了方法的实现,那么系统在调用safeLastObject方法是,找的是safeLastObject方法的实现,而手动调用safeLastObject方法时,会调用safeLastObject方法的实现。不清楚?回到前面看一看那个交换IMP的图吧!

    我们通过使用swizzling只是为了添加个打印?当然不是,我们还可以做很多事的。比如,上面我们还做了防崩溃处理。

    NSMutableArray扩展交换处理崩溃

    还记得那些调用数组的addObject:方法加入一个nil值是的崩溃情景吗?还记得[__NSPlaceholderArray initWithObjects:count:]因为有nil值而崩溃的提示吗?还记得调用objectAtIndex:时出现崩溃提示empty数组问题吗?那么通过swizzling特性,我们可以做到不让它崩溃,而只是打印一些有用的日志信息。

    我们先来看看NSMutableArray的扩展实现:

    #import "NSMutableArray+Swizzling.h"
    #import <objc/runtime.h>
    #import "NSObject+Swizzling.h"
    
    @implementation NSMutableArray (Swizzling)
    
    + (void)load {
      static dispatch_once_t onceToken;
      dispatch_once(&onceToken, ^{
        [self swizzleSelector:@selector(removeObject:)withSwizzledSelector:@selector(safeRemoveObject:)];
        [objc_getClass("__NSArrayM") swizzleSelector:@selector(addObject:) withSwizzledSelector:@selector(safeAddObject:)];
        [objc_getClass("__NSArrayM") swizzleSelector:@selector(removeObjectAtIndex:) withSwizzledSelector:@selector(safeRemoveObjectAtIndex:)];
        [objc_getClass("__NSArrayM") swizzleSelector:@selector(insertObject:atIndex:) withSwizzledSelector:@selector(safeInsertObject:atIndex:)];
        [objc_getClass("__NSPlaceholderArray") swizzleSelector:@selector(initWithObjects:count:) withSwizzledSelector:@selector(safeInitWithObjects:count:)];
        [objc_getClass("__NSArrayM") swizzleSelector:@selector(objectAtIndex:) withSwizzledSelector:@selector(safeObjectAtIndex:)];
      });
    }
    
    - (instancetype)safeInitWithObjects:(const id  _Nonnull     __unsafe_unretained *)objects count:(NSUInteger)cnt
     {
        BOOL hasNilObject = NO;
        for (NSUInteger i = 0; i < cnt; i++) {
            if ([objects[i] isKindOfClass:[NSArray class]]) {
            NSLog(@"%@", objects[i]);
        }
        if (objects[i] == nil) {
            hasNilObject = YES;
            NSLog(@"%s object at index %lu is nil, it will be     filtered", __FUNCTION__, i);
      
    //#if DEBUG
    //      // 如果可以对数组中为nil的元素信息打印出来,增加更容    易读懂的日志信息,这对于我们改bug就好定位多了
    //      NSString *errorMsg = [NSString     stringWithFormat:@"数组元素不能为nil,其index为: %lu", i];
    //      NSAssert(objects[i] != nil, errorMsg);
    //#endif
        }
     }
    
      // 因为有值为nil的元素,那么我们可以过滤掉值为nil的元素
      if (hasNilObject) {
          id __unsafe_unretained newObjects[cnt];
          NSUInteger index = 0;
          for (NSUInteger i = 0; i < cnt; ++i) {
              if (objects[i] != nil) {
                  newObjects[index++] = objects[i];
              }
          }
          return [self safeInitWithObjects:newObjects count:index];
      }
      return [self safeInitWithObjects:objects count:cnt];
    }
    
    - (void)safeAddObject:(id)obj {
        if (obj == nil) {
            NSLog(@"%s can add nil object into NSMutableArray", __FUNCTION__);
        } else {
            [self safeAddObject:obj];
        }
    }
    - (void)safeRemoveObject:(id)obj {
       if (obj == nil) {
          NSLog(@"%s call -removeObject:, but argument obj is nil", __FUNCTION__);
          return;
       }
       [self safeRemoveObject:obj];
    }
    
    - (void)safeInsertObject:(id)anObject atIndex:(NSUInteger)index {
        if (anObject == nil) {
            NSLog(@"%s can't insert nil into NSMutableArray", __FUNCTION__);
        } else if (index > self.count) {
            NSLog(@"%s index is invalid", __FUNCTION__);
        } else {
            [self safeInsertObject:anObject atIndex:index];
        }
      }
    
    - (id)safeObjectAtIndex:(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 safeObjectAtIndex:index];
    }
    
    - (void)safeRemoveObjectAtIndex:(NSUInteger)index {
        if (self.count <= 0) {
            NSLog(@"%s can't get any object from an empty array", __FUNCTION__);
            return;
        }
        if (index >= self.count) {
            NSLog(@"%s index out of bound", __FUNCTION__);
            return;
        }
        [self safeRemoveObjectAtIndex:index];
    }
    @end
    

    然后,我们测试nil值的情况,是否还会崩溃呢?

    NSMutableArray *array = [@[@"value", @"value1"]     mutableCopy];
    [array lastObject];
    
    [array removeObject:@"value"];
    [array removeObject:nil];
    [array addObject:@"12"];
    [array addObject:nil];
    [array insertObject:nil atIndex:0];
    [array insertObject:@"sdf" atIndex:10];
    [array objectAtIndex:100];
    [array removeObjectAtIndex:10];
    
    NSMutableArray *anotherArray = [[NSMutableArray alloc] init];
    [anotherArray objectAtIndex:0];
    
    NSString *nilStr = nil;
    NSArray *array1 = @[@"ara", @"sdf", @"dsfdsf", nilStr];
    NSLog(@"array1.count = %lu", array1.count);
    
    // 测试数组中有数组
    NSArray *array2 = @[@[@"12323", @"nsdf", nilStr],     @[@"sdf", @"nilsdf", nilStr, @"sdhfodf"]];
    

    哈哈,都不崩溃了,而且还打印出崩溃原因。是不是很神奇?如果充分利用这种特性,是不是可以给我们带来很多便利之处?

    上面只是swizzling的一种应用场景而已。其实利用swizzling特性还可以做很多事情的,比如处理按钮重复点击问题等。

    其它资料 [ runtime-method-swizzling]

    相关文章

      网友评论

      • 魏梓雄:用method swizzle 将NSArray和NSMutableArray的objectAtIndex:方法用自己的方法替换掉了。这个bug的复现方法是找到一个可以显示keyboard的页面,然后按home键退到后台,然后这个bug就会复现了。
        来自:http://blog.csdn.net/zyr124/article/details/72775406
      • 8e8ac5800a28:不行啊,递归crash
        大熊孩子:@PLASuperMan 文章只是举出一个用法,具体的代码你还是得自己写啊,这样才有收获
      • 笑谈红尘乱离人:比如safeObjectAtIndex:这个方法,你写的有个判断是if (index > self.count),不应该是if (index >= self.count)吗?
        魏梓雄:- (id)safeObjectAtIndex:(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 safeObjectAtIndex:index];
        }

        if (index > self.count) 应该改成 if (index >= self.count) ;
        否则
        NSMutableArray *array = [[NSMutableArray alloc] initWithArray:@[@"11",@"22"]];
        [array objectAtIndex:2];
        就报错了。没有达到规避作用了。
        大熊孩子:不对, 不能加, 这是 insert,index 可以=count,大于的时候插入才会 crash
        大熊孩子:对的,已加上

      本文标题:利用runtime解决数组字典的崩溃问题

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