iOS 避免常见崩溃(一)

级别: ★★☆☆☆
标签:「iOS 」「避免常见崩溃」
作者: WYW
审校: QiShare团队

笔者最近看了部分引起App Crash的常见情况,这次先讨论下操作集合类型(如NSArray,NSDictionary等)时,防止常见崩溃(如避免从数组中取值时越界、往字典中插入为nil的value等)的内容。

  • 为了避免崩溃,操作集合类对象时,设置值和取值的时候,可以考虑使用如下方法:
      1. 使用分类添加的安全性方法
      1. 使用交换系统方法和我们做了安全性处理的方法

安全操作集合类对象:添加分类方法

当操作集合类对象的时候,可以使用我们添加的安全取值的分类。

添加分类方法部分,先从NSString* 和NSNumber* 的integerValue谈起。

  • (1)如在NSObject的分类中添加的qi_safeIntegerValue用于替换平时用的integerValue的方法。
- (NSInteger)qi_safeIntegerValue {
    
    if ([self isKindOfClass:[NSNumber class]]) {
        return [((NSNumber *)self) integerValue];
    } else if([self isKindOfClass:[NSString class]]) {
        return [((NSString *)self) integerValue];
    } else {
        return kCustomErrorCode;
    }
}

使用qi_safeIntegerValue的方式为:

    id number = @(1);
    [number qi_safeIntegerValue];
  • (2)如在NSObject的分类中添加了qi_safeArrayObjectAtIndex用于替换NSArray* 的- (ObjectType)objectAtIndex:(NSUInteger)index;
- (id)qi_safeArrayObjectAtIndex:(NSUInteger)index {
    
    if (![self isKindOfClass:[NSArray class]]) {
        return nil;
    }
    
    if (index < 0 || index >= ((NSArray *)self).count) {
        return nil;
    }
    
    return [(NSArray *)self objectAtIndex:index];
}

使用qi_safeArrayObjectAtIndex的方式为:

    NSArray *qiArr = @[@1];
    [qiArr qi_safeArrayObjectAtIndex:0];
    [qiArr qi_safeArrayObjectAtIndex:1];
  • 更多相关内容,可查看Demo QiSafeType

不过只使用分类,不适于我们使用字面量语法取值的情况。如对于数组来说,如果我们想要使用qiArr[0]这样的字面量语法取值,使用当前的分类的方式就不适用了。此时就需要结合runtime使用交换qiArr[0]调用的系统方法和我们自己添加的安全取值的方法来达到字面量安全取值的目的。

安全操作集合类对象:方法交换

安全操作集合类对象,方法交换部分,笔者会以字面量操作NSArray聊聊相关内容。

以字面量的方式声明数组 及取值:

// 声明数组的时候 插入nil
NSString *nilValue = nil;
NSArray *qiArr = @[@"qishare0", nilValue, @"qiShare2"];

NSString *nilValue = nil;
NSArray *qiArr = @[@"qishare0", nilValue, @"qiShare2"];
NSLog(@"qiArr:%@", qiArr);

// 从数组中取值 故意越界取值
NSLog(@"qiArr[0]:%@ qiArr[1]:%@ qiArr[2]:%@",qiArr[0],qiArr[1],qiArr[2]);

// 输出结果如下:
qiArr:(
    qishare0,
    qiShare2
)

qiArr[0]:qishare0 qiArr[1]:qiShare2 qiArr[2]:(null)
  • 交换方法的操作需要引入#import <objc/runtime.h>,主要会用到下边的API。
// Returns a pointer to the data structure describing a given class method for a given class.
// 返回一个描述指定类cls和给定类方法的数据结构的指针 其实返回值也是一个Method
// 获取指定类Cls和指定方法sel的对应的 类 Method
Method class_getClassMethod(Class _Nullable cls, SEL _Nonnull name)
    OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);

// Returns a specified instance method for a given class.
// 返回指定的类cls及相应selector的实例方法
// 获取指定类Cls和指定方法sel的对应的 实例 Method
Method class_getInstanceMethod(Class _Nullable cls, SEL _Nonnull name)
    OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
   
// 交换Method m1和Method m2的实现 
void method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2) 
    OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
  • 交换方法需要待交换方法SEL,及待交换的类Class

以下内容笔者会以字面量的方式声明qiArr NSArray *qiArr = @[@"qishare0", nilValue, @"qiShare2"];为例进行分析,所需的class及要交换的SEL

  • (1)待交换的方法

就待交换的方法而言,声明qiArr的时候。首先要确定调用的NSArray哪个方法。调用的是+ (instancetype)arrayWithObjects:(const ObjectType _Nonnull [_Nonnull])objects count:(NSUInteger)cnt;;这个可以通过自己写一个数组的子类来证实这件事,也可以通过交换方法后调用的方法来反面验证。NSArray的子类的写法可以参考QiSafeType中的QiSubArray的写法。

关于继承NSArray:Any subclass of NSArray must override the primitive instance methods count and objectAtIndex:. These methods must operate on the backing store that you provide for the elements of the collection. For this backing store you can use a static array, a standard NSArray object, or some other data type or mechanism. You may also choose to override, partially or fully, any other NSArray method for which you want to provide an alternative implementation.

+ (instancetype)arrayWithObjects:(const ObjectType _Nonnull [_Nonnull])objects count:(NSUInteger)cnt;是一个类方法,所以方法交换部分,笔者也写了一个安全声明数组的类方法。+ (instancetype)qisafeArrayWithObjects:(const id _Nonnull [_Nonnull])objects count:(NSUInteger)cnt

笔者写的待交换的+ (instancetype)qisafeArrayWithObjects:(const id _Nonnull [_Nonnull])objects count:(NSUInteger)cnt的方法体如下:

+ (instancetype)qisafeArrayWithObjects:(const id _Nonnull [_Nonnull])objects count:(NSUInteger)cnt {
    
    id instance = nil;
    id safeObjs[cnt];
    NSUInteger j = 0;
    for (NSUInteger i = 0; i < cnt; i ++) {
        if (!objects[i]) {
            continue;
        }
        safeObjs[j++] = objects[i];
    }
    instance = [self qisafeArrayWithObjects:safeObjs count:j];
    return instance;
}

思想:在声明字面量数组的时候,先遍历指定的数组,过滤掉为空的对象,剩余的存放到另一个数组中。再调用自己添加的声明数组的类方法(因为我们添加的方法和系统的方法进行了方法交换,所以这里实质是调用的系统的声明数组的方法。)

  • (2)待交换的类

声明qiArr的时候待交换的方法为类方法,所以待交换的类为[NSArray class]

就待交换的方法而言。以qiArr[0]为例。要确定调用的数组的那个方法。调用的是- (ObjectType)objectAtIndex:(NSUInteger)index;这个可以通过自己写一个数组的子类来证实这件事,也可以通过交换方法后调用的方法来反面验证。

上述问题明确了之后,我们就可以在NSArray的分类的+ (void)load方法中根据指定的类[NSArray class]把+ (instancetype)arrayWithObjects:(const ObjectType _Nonnull [_Nonnull])objects count:(NSUInteger)cnt;+ (instancetype)qisafeArrayWithObjects:(const id _Nonnull [_Nonnull])objects count:(NSUInteger)cnt进行方法交换了。

关键代码如下:

    Method originMethod = class_getClassMethod([NSArray class], @selector(arrayWithObjects:count:));
    Method alterMethod = class_getClassMethod([NSArray class], @selector(qisafeArrayWithObjects:count:));
    method_exchangeImplementations(originMethod, alterMethod);

以上部分,笔者解释了,NSArray以字面量的方式声明数组的方法交换部分的内容,下边笔者将继续和大家聊一下关于NSArray字面量取值的内容。其实NSArray运作在抽象工厂设计模式下。抽象工厂模式是类簇在iOS下的的一种实现。众多常用类,如NSString,NSArray,NSDictionary,NSNumber都运作在抽象工厂模式下,引自从NSArray看类簇
以下部分,大家也可以直接看Demo中的写法自行分析。

笔者想通过以下代码说明,创建出的NSArray*的实例(qiArr)的class,和创建qiArr的方式及qiArr中包含的对象的个数有关。

NSString *nilValue = nil;
// 声明数组的时候 插入nil
NSArray *qiArr = @[@"qishare0", nilValue, @"qiShare2"];
NSLog(@"qiArr:%@", qiArr);
NSLog(@"qiArr[0]:%@ qiArr[1]:%@ qiArr[2]:%@",qiArr[0],qiArr[1],qiArr[2]);

id tempValue = nil;
    
NSArray *qiArr0 = @[];
tempValue = qiArr0[0];
tempValue = qiArr0[1];
    
NSArray *qiArr1 = @[@1];
tempValue = qiArr1[0];
tempValue = qiArr1[1];
    
NSArray *qiArr2 = @[@1, @2];
tempValue = qiArr2[1];
tempValue = qiArr2[2];
    
NSLog(@"qiArr0 class:%@", NSStringFromClass([qiArr0 class]));
NSLog(@"qiArr1 class:%@", NSStringFromClass([qiArr1 class]));
NSLog(@"qiArr2 class:%@", NSStringFromClass([qiArr2 class]));
    
NSArray *qiArr3 = [NSArray arrayWithObjects:@"1", @"2", @"3", nil];
    
NSLog(@"qiArr3 class:%@", NSStringFromClass([qiArr3 class]));

 qiArr0 class:__NSArray0
 qiArr1 class:__NSSingleObjectArrayI
 qiArr2 class:__NSArrayI
 qiArr3 class:__NSArrayI

有了上述内容,我们就可以在通过字面量语法故意越界访问数组对象的时候,根据崩溃的提示,添加上相应的方法交换。

关键代码如下:

    // __NSArray0
    Method originArr0ObjectAtIndexMethod = class_getInstanceMethod(NSClassFromString(@"__NSArray0"), @selector(objectAtIndex:));
    Method alterArr0ObjectAtIndexMethod = class_getInstanceMethod(NSClassFromString(@"__NSArray0"), @selector(qiSafeArr0ObjectAtIndex:));
    method_exchangeImplementations(originArr0ObjectAtIndexMethod, alterArr0ObjectAtIndexMethod);

   // __NSSingleObjectArrayI
    Method originSingleObjArrIObjectAtIndexMethod = class_getInstanceMethod(NSClassFromString(@"__NSSingleObjectArrayI"), @selector(objectAtIndex:));
    Method alterSingleObjArrIObjectAtIndexMethod = class_getInstanceMethod(NSClassFromString(@"__NSSingleObjectArrayI"), @selector(qiSafeSingleObjArrIObjectAtIndex:));
    method_exchangeImplementations(originSingleObjArrIObjectAtIndexMethod, alterSingleObjArrIObjectAtIndexMethod);
    
    // 注意__NSArrayI调用字面量语法的时候调用的系统方法为`- (ObjectType)objectAtIndexedSubscript:(NSUInteger)idx API_AVAILABLE(macos(10.8), ios(6.0), watchos(2.0), tvos(9.0));` 和上述类型的NSArray待交换的方法不同,这一点可以故意测试崩溃,查看崩溃的提示得出。
    
    // __NSArrayI
    Method originArrIObjAtIndexedSubMethod = class_getInstanceMethod(NSClassFromString(@"__NSArrayI"), @selector(objectAtIndexedSubscript:));
    Method alterArrIObjAtIndexedSubMethod = class_getInstanceMethod(NSClassFromString(@"__NSArrayI"), @selector(qiSafeArrIObjAtIndexedSubscript:));
    method_exchangeImplementations(originArrIObjAtIndexedSubMethod, alterArrIObjAtIndexedSubMethod);

Demo

  • 更多相关内容,可查看Demo QiSafeType

参考学习网址


推荐文章:
iOS消息转发
iOS 自定义拖拽式控件:QiDragView
iOS 自定义卡片式控件:QiCardView
iOS Wireshark抓包
iOS Charles抓包
初探TCP
IP、UDP初探

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350

推荐阅读更多精彩内容

  • 设计模式是什么? 你知道哪些设计模式,并简要叙述? 设计模式是一种编码经验,就是用比较成熟的逻辑去处理某一种类型的...
    Jt_Self阅读 738评论 0 4
  • NSArray全部API学习。 返回数组指定下标的元素 - ()objectAtIndex:(NSUInteger...
    ElvisSun阅读 786评论 0 2
  • 1. 熟悉Git的基本流程 git clone git add -A git commit -m " " git ...
    9bf19a4010ab阅读 1,620评论 0 2
  • 你是否有过如下的经历: 1. 选择的东西种类越多,越不知道该选什么。 2. 买了很多东西,当时很喜欢,可一直就没用...
    幕影心阅读 501评论 1 8
  • 怎么办? 突然有点不想结婚的念头。 人生如此之短, 青春又是多么的宝贵。 为什么要去为了一个不喜欢的人结婚, 然后...
    零九年白T恤阅读 148评论 0 0