为 NSArray/NSDictionary 优雅地过滤 nil 值

作为一名 iOS 开发者,肯定知道 NSArray/NSDictionary 不能存储 nil 值,如果你试图往数组/字典中存储 nil,那么 App 也将毫不客气的为你闪退。

尽管在日常的编码中,我们都会小心翼翼的处理 nil,但是总会有纰漏,毕竟大部分数据都是从服务器下发的,我们很难彻底把控。作为一名码农,肯定是想着怎么偷懒的,既能自动规避 nil,又能够不影响现有代码,最好不用引入第三方方法。得益于 Objective-C 的 runtime 机制,我们可以很优雅地通过 Method Swizlling 来解决上述问题。

但是我们需要注意一点的是,我们不能直接对 NSArray/NSMutableArray、NSDictionary/NSMutableDictionary 这些类进行 method swizlling 操作,因为它们底层是通过 Class cluster 来实现的,我们需要对隐藏在它们背后的真实的类进行 method swizlling,否则没任何效果。

#import <Foundation/Foundation.h>
#import <objc/message.h>


void safe_swizzle_method(Class originalClass, Class swizzledClass, SEL originalSelector, SEL swizzledSelector)
{
    Method originalMethod = class_getInstanceMethod(originalClass, originalSelector);
    Method swizzledMethod = class_getInstanceMethod(swizzledClass, swizzledSelector);
    
    IMP originalIMP = method_getImplementation(originalMethod);
    IMP swizzledIMP = method_getImplementation(swizzledMethod);
    const char *originalType = method_getTypeEncoding(originalMethod);
    const char *swizzledType = method_getTypeEncoding(swizzledMethod);
    
    class_replaceMethod(originalClass, swizzledSelector, originalIMP, originalType);
    class_replaceMethod(originalClass, originalSelector, swizzledIMP, swizzledType);
}


#pragma mark - Array

@interface XPSafeArray : NSObject

@end

@implementation XPSafeArray

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        NSArray<NSString *> *array1 = @[
                                        NSStringFromSelector(@selector(objectAtIndex:)),
                                        NSStringFromSelector(@selector(objectAtIndexedSubscript:))
                                        ];
        for (NSString *str in array1) {
            safe_swizzle_method(NSClassFromString(@"__NSArrayI"), self,
                                NSSelectorFromString(str),
                                NSSelectorFromString([@"safe_" stringByAppendingString:str]));
        }
        
        NSArray<NSString *> *array2 = @[
                                        NSStringFromSelector(@selector(objectAtIndex:)),
                                        NSStringFromSelector(@selector(objectAtIndexedSubscript:)),
                                        NSStringFromSelector(@selector(insertObject:atIndex:)),
                                        NSStringFromSelector(@selector(setObject:atIndexedSubscript:)),
                                        NSStringFromSelector(@selector(insertObjects:atIndexes:))
                                        ];
        for (NSString *str in array2) {
            safe_swizzle_method(NSClassFromString(@"__NSArrayM"), self,
                                NSSelectorFromString(str),
                                NSSelectorFromString([@"safe_" stringByAppendingString:str]));
        }
    });
}

- (id)safe_objectAtIndex:(NSUInteger)index {
    NSUInteger count = [(NSArray*)self count];
    if (count == 0 || index >= count) {
        return nil;
    }
    return [self safe_objectAtIndex:index];
}

- (id)safe_objectAtIndexedSubscript:(NSUInteger)index {
    NSUInteger count = [(NSArray*)self count];
    if (count == 0 || index >= count) {
        return nil;
    }
    return [self safe_objectAtIndexedSubscript:index];
}

- (void)safe_insertObject:(id)anObject atIndex:(NSUInteger)index {
    if (anObject == nil) return;
    [self safe_insertObject:anObject atIndex:index];
}

- (void)safe_setObject:(id)obj atIndexedSubscript:(NSUInteger)idx {
    if (obj == nil) return;
    [self safe_setObject:obj atIndexedSubscript:idx];
}

- (void)safe_insertObjects:(NSArray *)objects atIndexes:(NSIndexSet *)indexes {
    if (objects && objects.count == indexes.count) {
        [self safe_insertObjects:objects atIndexes:indexes];
    }
}

@end


#pragma mark - Dictionary

@interface XPSafeDictionary : NSDictionary

@end

@implementation XPSafeDictionary

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        safe_swizzle_method(NSClassFromString(@"__NSPlaceholderDictionary"),
                            self,
                            @selector(initWithObjects:forKeys:count:),
                            @selector(safe_initWithObjects:forKeys:count:));
        
        safe_swizzle_method(NSClassFromString(@"__NSDictionaryM"),
                            self,
                            @selector(setObject:forKey:),
                            @selector(safe_setObject:forKey:));
        
        safe_swizzle_method(NSClassFromString(@"__NSDictionaryM"),
                            self,
                            @selector(removeObjectForKey:),
                            @selector(safe_removeObjectForKey:));
    });
}

- (id)safe_initWithObjects:(id  _Nonnull const [])objects forKeys:(id<NSCopying>  _Nonnull const [])keys count:(NSUInteger)cnt {
    id safeObjects[cnt];
    id safeKeys[cnt];
    NSUInteger count = 0;
    
    for (NSUInteger idx=0; idx<cnt; idx++) {
        id key = keys[idx];
        id obj = objects[idx];
        
        if (!key) {
            continue;
        }
        if (!obj) {
            obj = [NSNull null]; // 可根据你项目需求,将`NSString`作为默认值
        }
        
        safeKeys[count] = key;
        safeObjects[count] = obj;
        count++;
    }
    return [self safe_initWithObjects:safeObjects forKeys:safeKeys count:count];
}

- (void)safe_setObject:(id)anObject forKey:(id<NSCopying>)aKey {
    if (anObject && aKey) {
        [self safe_setObject:anObject forKey:aKey];
    }
}

- (void)safe_removeObjectForKey:(id)aKey {
    if (aKey) {
        [self safe_removeObjectForKey:aKey];
    }
}

@end

以上只给出部分实现,但是已基本满足日常使用,你可以自己根据自己的需求进行完善。