写在前面

OC上常见崩溃一般不亚于 数组越界以及字典设置为nil。

虽然大家基本都知道这些情况下,程序会表示抱歉,我要崩溃的,但是大多数情况下传入进来的都是一个变量,变量真正的值有时候就会正在的出乎程序员的意料,比如过大导致越界,或是尽然是空的。如果说这种意外情况无法避免,那么只能从侧面采取保护措施。

我先整理下常见的崩溃方法:

NSArray:
    objectAtIndex:
NSMutableArray:
    addObject:
    insertObject:atIndex:
    removeObjectAtIndex:
    replaceObjectAtIndex:withObject:
NSMutableDictionary:
    setObject:forKey:
  1. 取值:index超出array的索引范围
  2. 添加:插入的object为nil或者Null
  3. 插入:index大于count、插入的object为nil或者Null
  4. 删除:index超出array的索引范围
  5. 替换:index超出array的索引范围、替换的object为nil或者Null

现在市面上主流的两种结局方法是:

  • 使用runtime hook系统的这几个方法
  • 使用分类的形式重新封装一层方法

两种方法当然各有利弊,hack方便,但是风险不可控,使用分类的方式反锁,风险较易控制。

捕捉到错误之后,可以使用Crashlytics上传崩溃日志,这样就可以自我捕捉这些错误而不让系统抛出exception而崩溃。

个人推荐还是使用稳妥的方法,用分类重新封装一层方法的形式。


代替exception使用日志系统上报错误

日志上报使用的是fabric,有关fabric的使用,自行去google,这里不做过多讲解。

关键技术是使用fabric的方法recordCustomExceptionName:reason:frameArray:

/**
 *  This method can be used to record a single exception structure in a report. This is particularly useful
 *  when your code interacts with non-native languages like Lua, C#, or Javascript. This call can be
 *  expensive and should only be used shortly before process termination. This API is not intended be to used
 *  to log NSException objects. All safely-reportable NSExceptions are automatically captured by
 *  Crashlytics.
 *
 *  @param name       The name of the custom exception
 *  @param reason     The reason this exception occured
 *  @param frameArray An array of CLSStackFrame objects
 */
- (void)recordCustomExceptionName:(NSString *)name reason:(nullable NSString *)reason frameArray:(CLS_GENERIC_NSARRAY(CLSStackFrame *) *)frameArray;

注释大致就是说可以自行发送日志到fabric,所以就有了下面的使用方法:

static void Log(NSString *fmt, ...) {
    NSMutableString *reportStr = [NSMutableString string];
    va_list ap;
    va_start(ap, fmt);
    NSString *content = [[NSString alloc] initWithFormat:fmt arguments:ap];
    [reportStr appendString:@"***Terminating app due to uncaught exception\r\n"];
    [reportStr appendFormat:@"***reason:-%@\r\n", content];
    va_end(ap);
    [reportStr appendFormat:@"*** First throw call stack:\n%@", [NSThread callStackSymbols]];
    NSArray<NSString *> *callStackSymbols = [NSThread callStackSymbols];
    NSMutableArray<CLSStackFrame *> *stackFrames = [NSMutableArray arrayWithCapacity:callStackSymbols.count];
    for (NSString *callStachSymbol in callStackSymbols) {
        [stackFrames addObject:[CLSStackFrame stackFrameWithSymbol:callStachSymbol]];
    }
#ifdef DEBUG
    [NSException raise:NSInvalidArgumentException format:@"***reason%@", content];
#else
    [CrashlyticsKit recordCustomExceptionName:@"自捕获崩溃" reason:content frameArray:stackFrames];
#endif
    YXLogError(@"%@", reportStr);
}

最后fabirc上捕捉到的日志就是non-fatals

objc_release 崩溃 iOS oc老是崩溃_崩溃



objc_release 崩溃 iOS oc老是崩溃_崩溃_02


使用RunTime hack系统方法的形式实现

工具类继承至NSObject,在load类方法中实现hack技术,分别hook住相应的方法

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        //        NSArray
        [self swizzleInstanceMethodWithClass:NSClassFromString(@"__NSArrayI") originalSelector:@selector(objectAtIndex:) swizzledMethod:@selector(safeObjectAtIndex:)];

        //        NSMutableArray
        [self swizzleInstanceMethodWithClass:NSClassFromString(@"__NSArrayM") originalSelector:@selector(objectAtIndex:) swizzledMethod:@selector(safeObjectAtIndex:)];
        //        [self swizzleInstanceMethodWithClass:NSClassFromString(@"__NSArrayM") originalSelector:@selector(addObject:) swizzledMethod:@selector(safeAddObject:)];
        [self swizzleInstanceMethodWithClass:NSClassFromString(@"__NSArrayM") originalSelector:@selector(replaceObjectAtIndex:withObject:) swizzledMethod:@selector(safeReplaceObjectAtIndex:withObject:)];
        [self swizzleInstanceMethodWithClass:NSClassFromString(@"__NSArrayM") originalSelector:@selector(insertObject:atIndex:) swizzledMethod:@selector(safeInsertObject:atIndex:)];

        //        NSDictionary
        //        [self swizzleClassMethodWithClass:[NSDictionary class] originalSelector:@selector(dictionaryWithObjects:forKeys:count:) swizzledMethod:@selector(safeDictionaryWithObjects:forKeys:count:)];

        //        NSMutableDictionary
        [self swizzleInstanceMethodWithClass:NSClassFromString(@"__NSDictionaryM") originalSelector:@selector(setObject:forKey:) swizzledMethod:@selector(safeSetObject:forKey:)];

    });
}

NSArray

@interface NSArray (Safte)

@end

@implementation NSArray (Safte)

- (id)safeObjectAtIndex:(NSUInteger)index {
    @autoreleasepool {
        if (self.count > index) {
            return [self safeObjectAtIndex:index];
        }else {

            Log(@"[%@ %@] index %lu beyond bounds [0 .. %lu]",
                    NSStringFromClass([self class]),
                    NSStringFromSelector(_cmd),
                    (unsigned long)index,
                    MAX((unsigned long)self.count - 1, 0));
            return nil;
        }
    }
}

@end

NSMutableArray

@interface NSMutableArray (Safte)

@end

@implementation NSMutableArray (Safte)

- (id)safeObjectAtIndex:(NSUInteger)index {
    @autoreleasepool {
        if (self.count > index) {
            return [self safeObjectAtIndex:index];
        }else {

            Log(@"[%@ %@] index %lu beyond bounds [0 .. %lu]",
                    NSStringFromClass([self class]),
                    NSStringFromSelector(_cmd),
                    (unsigned long)index,
                    MAX((unsigned long)self.count - 1, 0));
            return nil;
        }
    }
}

- (void)safeAddObject:(id)anObject {
    @autoreleasepool {
        if (anObject) {
            [self safeAddObject:anObject];
        }else {
            Log(@"[%@ %@], nil object. object cannot be nil", NSStringFromClass([self class]), NSStringFromSelector(_cmd));
        }
    }
}

- (void)safeReplaceObjectAtIndex:(NSUInteger)index withObject:(id)anObject {
    @autoreleasepool {
        if (index >= self.count) {
            Log(@"[%@ %@] index %lu beyond bounds [0 .. %lu].",
                    NSStringFromClass([self class]),
                    NSStringFromSelector(_cmd),
                    (unsigned long)index,
                    MAX((unsigned long)self.count - 1, 0));
            return;
        }else if (!anObject) {
            Log(@"[%@ %@] nil object. object cannot be nil", NSStringFromClass([self class]), NSStringFromSelector(_cmd));
            return;

        }
        [self safeReplaceObjectAtIndex:index withObject:anObject];
    }
}

- (void)safeInsertObject:(id)anObject atIndex:(NSUInteger)index {
    @autoreleasepool {
        if (index > self.count)
        {
            Log(@"[%@ %@] index %lu beyond bounds [0...%lu].",
                    NSStringFromClass([self class]),
                    NSStringFromSelector(_cmd),
                    (unsigned long)index,
                    MAX((unsigned long)self.count - 1, 0));
            return;
        }

        if (!anObject)
        {
            Log(@"[%@ %@] nil object. object cannot be nil", NSStringFromClass([self class]), NSStringFromSelector(_cmd));
            return;
        }

        [self safeInsertObject:anObject atIndex:index];
    }
}

@end

NSDictionary

@interface NSDictionary (Safte)

@end

@implementation NSDictionary (Safte)


+ (instancetype)safeDictionaryWithObjects:(const id  _Nonnull __unsafe_unretained *)objects forKeys:(const id<NSCopying>  _Nonnull __unsafe_unretained *)keys count:(NSUInteger)cnt {
    @autoreleasepool {
        id validObjects[cnt];
        id<NSCopying> validKeys[cnt];
        NSUInteger count = 0;
        for (NSUInteger i = 0; i < cnt; i++)
        {
            if (objects[i] && keys[i])
            {
                validObjects[count] = objects[i];
                validKeys[count] = keys[i];
                count ++;
            }
            else
            {
                Log(@"[%@ %@] NIL object or key at index%lu.",
                        NSStringFromClass(self),
                        NSStringFromSelector(_cmd),
                        (unsigned long)i);
            }
        }

        return [self safeDictionaryWithObjects:validObjects forKeys:validKeys count:count];
    }
}
@end

NSMuatbleDictionary

@interface NSMutableDictionary (Safte)

@end

@implementation NSMutableDictionary (Safte)

- (void)safeSetObject:(id)anObject forKey:(id<NSCopying>)aKey {
    @autoreleasepool {
        if (!aKey)
        {
            Log(@"[%@ %@] nil key. key cannot be nil", NSStringFromClass([self class]), NSStringFromSelector(_cmd));
            return;
        }
        if (!anObject)
        {
            Log(@"[%@ %@] nil object. object cannot be nil", NSStringFromClass([self class]), NSStringFromSelector(_cmd));
            return;
        }
        [self safeSetObject:anObject forKey:aKey];        
    }
}

@end

安全容器方法

分装一层分类,规定以后团队都使用这个分类方法

NSArray

@implementation NSArray (Safety)

- (id)at:(NSUInteger)i {
    if (i < self.count) {
        return self[i];
    } else {
        Log(@"NSArray objectAtIndex beyond the bound: %lu is beyond the count:%lu!", (unsigned long)i, (unsigned long)self.count);
        return nil;
    }
}

+ (instancetype)create:(id)firstObj, ... {
    NSMutableArray *array = [NSMutableArray new];

    va_list args;
    va_start(args, firstObj);
    for (id arg = firstObj; arg != nil; arg = va_arg(args, id)) {
        [array add:arg];
    }
    va_end(args);

    if ([self isEqual:NSMutableDictionary.class]) {
        return array;
    } else {
        return [array copy];
    }
}

@end

NSDictionary

@implementation NSDictionary (Safety)

+ (instancetype)kv:(id)firstObj, ... {
    NSMutableDictionary *dic = [NSMutableDictionary new];

    va_list args;
    va_start(args, firstObj);
    id key, value;
    int i = 0;
    for (id arg = firstObj; arg != nil; arg = va_arg(args, id)) {
        if (i % 2 == 0) {
            key = arg;
        } else {
            value = arg;
            [dic key:key value:value];
        }
        i++;
    }
    va_end(args);

    if ([self isEqual:NSMutableDictionary.class]) {
        return dic;
    } else  {
        return dic.copy;
    }
}

@end

NSMutableArray

@implementation NSMutableArray (Safety)

- (void)add:(id)object {
    if (object == nil) {
        Log(@"NSMutableArray added a nil object!");
        return;
    }

    [self addObject:object];
}

- (void)insert:(id)o at:(NSUInteger)i {
    if (o == nil) {
        Log(@"NSMutableArray inserted a nil object!");
    } else if (i > self.count) { //这里得是> 而不能>=,因为insert能插入到最后一个
        Log(@"NSArray insertObject:atIndex: beyond the bound: %lu is beyond the count:%lu!", (unsigned long)i, (unsigned long)self.count);
    } else {
        [self insertObject:o atIndex:i];
    }
}

- (void)removeAt:(NSUInteger)i {
    if (i < self.count) {
        [self removeObjectAtIndex:i];
    } else {
        Log(@"NSArray removeObjectAtIndex beyond the bound: %lu is beyond the count:%lu!", (unsigned long)i, (unsigned long)self.count);
    }
}

@end

NSMutableDictionary

@implementation NSMutableDictionary (Safety)

- (void)key:(id)k value:(id)v {
    if (k == nil || ![k conformsToProtocol:@protocol(NSCopying)]) {
        Log(@"NSMutableDictionary setObject:forKey:, key %@ is invalid!", k);
        return;
    }

    self[k] = v;
}

@end