iOS 不閃退的集合實踐

在iOS開發過程中,我們常見的集合類中主要包括:

  • 不可變的數組–NSArray
  • 可變的數組–NSMutableArray
  • 不可變的字典–NSDictionary
  • 可變的字典–NSMutableDictionary
  • 不可變的集合–NSSet
  • 可變的集合–NSMutableSet

我們最常用且容易出錯的集合類就是:NSArray,NSMutableArray,NSMutableDictionary!
NSArray主要的問題是數組越界問題
NSMutableArray主要的問題是數組越界,和設置了nil到數組中
NSMutableDictionary 主要是在我們網絡請求時,設置參數時若參數為nil則閃退!
為了解決以上問題,我們想出了兩種方案:

  • 為這些類添加類別,對之前的讀取方法進一步包裝,在包裝中添加錯誤判斷!這種方法入侵性過大,若工程之前已經開始了,大量的地方需要替換方法!
  • 為這些類添加類別,使用runtime的method swizzling方法,為原讀取方法添加錯誤判斷,這種方法完全無侵入,我們只需要為其添加類別即可!

為此下面我們著重討論第二種方法
按照我們的第一想法肯定是類似下面的代碼:

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [self swizzlingMethod];
    });
}

+ (void)swizzlingMethod {
    Class class = NSClassFromString(@"NSArray");
    Method originMethod = class_getInstanceMethod(class, @selector(objectAtIndex:));
    Method destMethod = class_getInstanceMethod(class, @selector(rh_objectAtIndex:));
    method_exchangeImplementations(originMethod, destMethod);
}
- (id)rh_objectAtIndex:(NSUInteger)index {
    if (self.count <= index) {
        return nil;
    } else {
        return [self rh_IobjectAtIndex:index];
    }
}

代碼寫完,模擬數組越界訪問,結果根本沒有調用!
經過資料查詢:
Foundation框架里面有很多類使用類簇(class cluster)設計模式,來實現一些類如:UIButton,NSNumber,集合類等!其本質就是工廠設計模式;思路就是比如我們有一個類有很多的子類,如果把這些子類全部暴露給用戶,用戶會很暈,不如只暴露父類給用戶,然后在父類中提供初始化方法:直接獲取子類實例!如我們的NSNumber類他提供了:

+ (NSNumber *)numberWithChar:(char)value;
+ (NSNumber *)numberWithUnsignedChar:(unsigned char)value;
+ (NSNumber *)numberWithShort:(short)value;
+ (NSNumber *)numberWithUnsignedShort:(unsigned short)value;
+ (NSNumber *)numberWithInt:(int)value;
+ (NSNumber *)numberWithUnsignedInt:(unsigned int)value;
+ (NSNumber *)numberWithLong:(long)value;
+ (NSNumber *)numberWithUnsignedLong:(unsigned long)value;
+ (NSNumber *)numberWithLongLong:(long long)value;
+ (NSNumber *)numberWithUnsignedLongLong:(unsigned long long)value;
+ (NSNumber *)numberWithFloat:(float)value;
+ (NSNumber *)numberWithDouble:(double)value;
+ (NSNumber *)numberWithBool:(BOOL)value;
+ (NSNumber *)numberWithInteger:(NSInteger)value API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
+ (NSNumber *)numberWithUnsignedInteger:(NSUInteger)value API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));

這么多子類如果全部暴露給用戶,多復雜,我們只需要通過以上方法,想要獲取哪個子類的實例,就調用不同的初始化方法,多好!

回到我們的集合類,Foundation框架對集合類的實現也是采取這種模式;
比如我們的NSArray就有__NSArray0,__NSSingleObjectArrayI,__NSArrayI!為什么要分這幾種實例呢?首先__NSArray0是個單例,表示空數組,所有的空數組都一樣,__NSSingleObjectArrayI表示單個元素的數組!__NSArrayI表示多個元素的數組!
所以我們在method swizzling的時候必須使用真正的class,否則無效果!

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [self swizzlingMethod];
    });
}

+ (void)swizzlingMethod {
    // __NSArrayI,__NSArray0,__NSSingleObjectArrayI
    [self swizzlingClass:NSClassFromString(@"__NSArray0")
                selector:@selector(objectAtIndex:)
            destSelector:@selector(rh_0objectAtIndex:)];
    [self swizzlingClass:NSClassFromString(@"__NSArray0")
                selector:@selector(objectAtIndexedSubscript:)
            destSelector:@selector(rh_0objectAtIndexedSubscript:)];
    
    [self swizzlingClass:NSClassFromString(@"__NSArrayI")
                selector:@selector(objectAtIndex:)
            destSelector:@selector(rh_1objectAtIndex:)];
    [self swizzlingClass:NSClassFromString(@"__NSArrayI")
                selector:@selector(objectAtIndexedSubscript:)
            destSelector:@selector(rh_1objectAtIndexedSubscript:)];
    
    [self swizzlingClass:NSClassFromString(@"__NSSingleObjectArrayI")
                selector:@selector(objectAtIndex:)
            destSelector:@selector(rh_IobjectAtIndex:)];
    [self swizzlingClass:NSClassFromString(@"__NSSingleObjectArrayI")
                selector:@selector(objectAtIndexedSubscript:)
            destSelector:@selector(rh_IobjectAtIndexedSubscript:)];
}

+ (void)swizzlingClass:(Class)class
              selector:(SEL)selector
          destSelector:(SEL)destSelector {
    Method originMethod = class_getInstanceMethod(class, selector);
    Method destMethod = class_getInstanceMethod(class, destSelector);
    method_exchangeImplementations(originMethod, destMethod);
}

- (id)rh_0objectAtIndexedSubscript:(NSUInteger)idx {
    if (self.count <= idx) {
        [RHExceptionHelper raiseException:@"index beyond array count"];
        return nil;
    } else {
        return [self rh_0objectAtIndexedSubscript:idx];
    }
}

- (id)rh_0objectAtIndex:(NSUInteger)index {
    if (self.count <= index) {
        [RHExceptionHelper raiseException:@"index beyond array count"];
        return nil;
    } else {
        return [self rh_0objectAtIndex:index];
    }
}

- (id)rh_1objectAtIndexedSubscript:(NSUInteger)idx {
    if (self.count <= idx) {
        [RHExceptionHelper raiseException:@"index beyond array count"];
        return nil;
    } else {
        return [self rh_1objectAtIndexedSubscript:idx];
    }
}

- (id)rh_1objectAtIndex:(NSUInteger)index {
    if (self.count <= index) {
        [RHExceptionHelper raiseException:@"index beyond array count"];
        return nil;
    } else {
        return [self rh_1objectAtIndex:index];
    }
}

- (id)rh_IobjectAtIndexedSubscript:(NSUInteger)idx {
    if (self.count <= idx) {
        [RHExceptionHelper raiseException:@"index beyond array count"];
        return nil;
    } else {
        return [self rh_IobjectAtIndexedSubscript:idx];
    }
}

- (id)rh_IobjectAtIndex:(NSUInteger)index {
    if (self.count <= index) {
        [RHExceptionHelper raiseException:@"index beyond array count"];
        return nil;
    } else {
        return [self rh_IobjectAtIndex:index];
    }
}

這里面有個知識點是:當我們使用arr[]這種語法獲取數組值的時候,我們調用的是:objectAtIndexedSubscript:方法,若該方法未method swizzling的話當你使用arr[]這種語法的時候還是會有數組越界問題的出現!

在method swizzling過程中還遇到了一個坑:就是為了偷懶只寫一個

- (id)rh_objectAtIndex:(NSUInteger)index {
    if (self.count <= index) {
        [RHExceptionHelper raiseException:@"index beyond array count"];
        return nil;
    } else {
        return [self rh_IobjectAtIndex:index];
    }
}

然后不同的類都exchange這個方法,邏輯肯定是錯的,多次交換實現,最終成功的只有一個!

此外我們還加了一個異常處理,原因就是,我們在調試的時候還是希望數組越界等這種問題暴露出來,及時的修改bug,所以我們添加了一個方法實現:

+ (void)raiseException:(NSString *)reason {
#if DEBUG
    [[NSException exceptionWithName:reason
                             reason:reason
                           userInfo:nil] raise];
#else
    
#endif
}
跪求Star
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容