利用runtime解決數(shù)組字典的崩潰問題

前言

我們在平時的工作中經(jīng)常會遇到這樣一種情況,當我們從后臺請求到的數(shù)據(jù),需要把其中一個插入到數(shù)組的時候,需要先判斷該對象是否為空值,非空才能插入,否則會引起崩潰。

那么有沒有一種方式,可以從根本上解決,即使我插入的是空值,也不會引起崩潰呢:

1.繼承于這個類,然后通過重寫方法(很常用,比如基類控制器,可以在視圖加載完成時做一些公共的配置等)

2.通過類別重寫方法,暴力搶先(此法太暴力,盡量不要這么做)

3.swizzling(本文特講內容)

Swizzling原理

在Objective-C中調用一個方法,其實是向一個對象發(fā)送消息,而查找消息的唯一依據(jù)是selector的名字。所以,我們可以利用Objective-C的runtime機制,實現(xiàn)在運行時交換selector對應的方法實現(xiàn)以達到我們的目的。

每個類都有一個方法列表,存放著selector的名字和方法實現(xiàn)的映射關系。IMP有點類似函數(shù)指針,指向具體的Method實現(xiàn)。

我們先看看SEL與IMP之間的關系圖:

但是,現(xiàn)在我們要做的就是把鏈接線解開,然后連到我們自定義的函數(shù)的IMP上。當然,交換了兩個SEL的IMP,還是可以再次交換回來了。交換后變成這樣的,如下圖:

從圖中可以看出,我們通過swizzling特性,將selectorC的方法實現(xiàn)IMPc與selectorN的方法實現(xiàn)IMPn交換了,當我們調用selectorC,也就是給對象發(fā)送selectorC消息時,所查找到的對應的方法實現(xiàn)就是IMPn而不是IMPc了。

在+load方法中交換

Swizzling應該在+load方法中實現(xiàn),因為+load方法可以保證在類最開始加載時會調用。因為method swizzling的影響范圍是全局的,所以應該放在最保險的地方來處理是非常重要的。+load能夠保證在類初始化的時候一定會被加載,這可以保證統(tǒng)一性。試想一下,若是在實際時需要的時候才去交換,那么無法達到全局處理的效果,而且若是臨時使用的,在使用后沒有及時地使用swizzling將系統(tǒng)方法與我們自定義的方法實現(xiàn)交換回來,那么后續(xù)的調用系統(tǒng)API就可能出問題。

類文件在工程中,一定會加載,因此可以保證+load會被調用。

不要在+initialize中交換

+initialize是類第一次初始化時才會被調用,因為這個類有可能一直都沒有使用到,因此這個類可能永遠不會被調用。

類文件雖然在工程中,但是如果沒有任何地方調用過,那么是不會調用+initialize方法的。

使用dispatch_once保證只交換一次

方法交換應該要線程安全,而且保證只交換一次,除非只是臨時交換使用,在使用完成后又交換回來。

最常用的用法是在+load方法中使用dispatch_once來保證交換是安全的。因為swizzling會改變全局,我們需要在運行時采取相應的防范措施。保證原子操作就是一個措施,確保代碼即使在多線程環(huán)境下也只會被執(zhí)行一次。而diapatch_once就提供這些保障,因此我們應該將其加入到swizzling的使用標準規(guī)范中。

通用交換IMP寫法

網(wǎng)上有很多的版本,但是有很多是不全面的,考慮的范圍不夠全面。下面我們來寫一個通用的寫法,現(xiàn)在擴展到NSObject中,因為NSObject是根類,這樣其它類都可以使用了:

@interface NSObject (Swizzling) 

+ (void)swizzleSelector:(SEL)originalSelector withSwizzledSelector:(SEL)swizzledSelector; 

@end


#import "NSObject+Swizzling.h"

#import <objc/runtime.h>

// 實現(xiàn)代碼如下

@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);

// 若已經(jīng)存在,則添加會失敗

    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

因為方法可能不是在這個類里,可能是在其父類中才有實現(xiàn),因此先嘗試添加方法的實現(xiàn),若添加成功了,則直接替換一下實現(xiàn)即可。若添加失敗了,說明已經(jīng)存在這個方法實現(xiàn)了,則只需要交換這兩個方法的實現(xiàn)就可以了。

盡量使用method_exchangeImplementations函數(shù)來交換,因為它是原子操作的,線程安全。盡量不要自己手動寫這樣的代碼:

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

雖然method_exchangeImplementations函數(shù)的本質也是這么寫法,但是它內部做了線程安全處理。

簡單使用swizzling

最簡單的方法實現(xiàn)交換如下:

Method originalMethod = class_getInstanceMethod([NSArray class], @selector(lastObject));
Method newMedthod = class_getInstanceMethod([NSArray class], NSSelectorFromString(@"safeLastObject"));
method_exchangeImplementations(originalMethod, newMedthod);

// NSArray提供了這樣的實現(xiàn)
- (id) safeLastObject {
  if (self.count == 0) {
    NSLog(@"%s 數(shù)組為空,直接返回nil", __FUNCTION__);
    return nil;
  }
  return [self safeLastObject];
}

看到safeLastObject這個方法遞歸調用自己了嗎?為什么不是調用return [self safeLastObject]?因為我們交換了方法的實現(xiàn),那么系統(tǒng)在調用safeLastObject方法是,找的是safeLastObject方法的實現(xiàn),而手動調用safeLastObject方法時,會調用safeLastObject方法的實現(xiàn)。不清楚?回到前面看一看那個交換IMP的圖吧!

我們通過使用swizzling只是為了添加個打???當然不是,我們還可以做很多事的。比如,上面我們還做了防崩潰處理。

NSMutableArray擴展交換處理崩潰

還記得那些調用數(shù)組的addObject:方法加入一個nil值是的崩潰情景嗎?還記得[__NSPlaceholderArray initWithObjects:count:]因為有nil值而崩潰的提示嗎?還記得調用objectAtIndex:時出現(xiàn)崩潰提示empty數(shù)組問題嗎?那么通過swizzling特性,我們可以做到不讓它崩潰,而只是打印一些有用的日志信息。

我們先來看看NSMutableArray的擴展實現(xiàn):

#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
//      // 如果可以對數(shù)組中為nil的元素信息打印出來,增加更容    易讀懂的日志信息,這對于我們改bug就好定位多了
//      NSString *errorMsg = [NSString     stringWithFormat:@"數(shù)組元素不能為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);

// 測試數(shù)組中有數(shù)組
NSArray *array2 = @[@[@"12323", @"nsdf", nilStr],     @[@"sdf", @"nilsdf", nilStr, @"sdhfodf"]];

哈哈,都不崩潰了,而且還打印出崩潰原因。是不是很神奇?如果充分利用這種特性,是不是可以給我們帶來很多便利之處?

上面只是swizzling的一種應用場景而已。其實利用swizzling特性還可以做很多事情的,比如處理按鈕重復點擊問題等。

其它資料 [ runtime-method-swizzling]

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容