iOS 简单容错处理
对一些代码进行容错处理,如果处理的好,会减少很多 crash。尤其对于像我这样的新手,稍不注意就会
写出几十个 bug。以下是针对刚入职这段时间所做项目的一个总结,同时提醒自己不要再犯同样的错误。
美好的一天,从没有 bug 开始~
数据类型
新手(像我这样)在使用一些常用的数据类型时,例如 NSArray 、NSDictionary 、NSNumber 、NSString等,经常会遇到一些崩溃问题。如果平时写程序首先进行容错判断,会减少很多崩溃问题。下面列举一些新手需要注意的情况。
1.NSArray & NSMutableArray
- +(instancetype)arrayWithObject:(ObjectType)anObject;
提前判断对象是否为 nil,传入 nil 会引起崩溃
- -(ObjectType)objectAtIndex:(NSUInteger)index;
提前判断 index 是否小于数组个数,否则会因数组越界引起崩溃
- -(NSArray
*)arrayByAddingObject:(ObjectType)anObject;
提前判断传入的对象是否为 nil,传入 nil 会引起崩溃
- -(void)addObject:(ObjectType)anObject;
提前判断传入的对象是否为 nil,传入 nil 会引起崩溃
- -(void)insertObject:(ObjectType)anObject atIndex:(NSUInteger)index;
提前对 anyObject 进行非空判断 && 对 index 进行越界判断,否则可能引起崩溃
- -(void)removeObjectAtIndex:(NSUInteger)index;
提前对 index 进行越界判断,否则可能会因数组越界引起崩溃
- -(void)replaceObjectAtIndex:(NSUInteger)index withObject:(ObjectType)anObject;
提亲对 index进行越界判断 && 对 anyObject 进行非空判断,否则可能引起崩溃
2.NSDictionary & NSMutableDictionary
- +(instancetype)dictionaryWithObject:(ObjectType)object forKey:(KeyType
)key;
提前判断 object 和 key 是否为 nil,如有一个为 nil 则会崩溃
- -(nullable id)objectForKey:(NSString *)anAttribute;
提前判断传入参数是否为 nil,传入 nil 会引起崩溃
- -(void)setObject:(ObjectType)anObject forKey:(KeyType
)aKey;
提前判断 anyObject 和 aKey 是否为 nil,有一个为空都会崩溃
- -(void)removeObjectForKey:(KeyType)aKey;
提前判断 akey 是否为 nil, 传入 nil 会引起崩溃
3.NSNumber
NSNumber 在进行类型转换时,需要先判断是否响应转换方法。在 NSNumber 的实例,可以转化为的类型有:
char、unsigned char、short、unsigned short、int、unsigned int、long、unsigned long、long long、unsigned long long、float、double、BOOL、NSInteger、NSUInteger
例如下面这种情况,如果不提前进行判断,会引起崩溃:
1 |
|
在我们初始化 NSNumber 对象时,可能会使用一些意想不到的对象进行初始化。例如上面 testNumer 实际获得的是一个 NSArray 类型的对象,并不能响应 intValue 方法,因此正确的写法应为:
1 | if ([testNumber respondsToSelector:@selector(intValue)]) { |
4.NSString
NSString 类使用时需要注意两点:
在使用一些字符串长度操作的方法,例如
- (NSString *)stringByReplacingCharactersInRange:(NSRange)range withString:(NSString *)replacement时,需要判断传入的range是否越界。在使用类似
NSNumber的模糊类型转换方法时,首先进行respondsToSelector:判断。
以上实例方法的容错判断限于实例对象不为空的情况下,如果实例对象都为空了,即使传入空值也不会崩溃。
数据类型番外篇
在项目开发过程中,很多数据都是依赖服务端返回。如果服务端不靠谱,你不知道服务端会返回给你什么乱七八糟的东西。在加上自己粗心忘记进行了 nil 判断,很容易造成崩溃。如果每次都去判断,会很麻烦,我们需要一个统一的方法进行非空判断。
你可能会想到 Category ,我开始也是想到使用 Category ,但是写到一半你会发现有很多问题。如果使用 Category 方式去重写 objectAtIndex: 方法,你可能无法处理通过下标[]访问数据的问题;另外 NSArray 是一个 类簇 ,重写起来十分麻烦,工作量很大。
类簇
Class clusters are a design pattern that the Foundation framework makes extensive use of. Class clusters group a number of private concrete subclasses under a public abstract superclass. The grouping of classes in this way simplifies the publicly visible architecture of an object-oriented framework without reducing its functional richness. Class clusters are based on the Abstract Factory design pattern.
简单说就是:类簇将一些私有的、具体的子类组合在一个公共的、抽象的超类下面,以这种方法来组织类可以简化一个面向对象框架的公开架构。这是一种基于 工厂模式 的实现。
NSArray 、NSDictionary 、NSNumber 、 NSString 这些都是类簇。官方文档 通过 NSNumber 对类簇进行了解释。
针对 NSArray ,进行了如下测试:
1 |
|
可以看出,iArray1 与 mArray1 为同一个类,都为 __NSPlaceholderArray。但是 iArray2 为 ___NSArray0 (NSArray) ,mArray2 为 __NSArrayM (NSMutableArray) 类。因此对于类簇,在使用 alloc + init 方法进行初始化时,alloc 方法先生成一个中间类,在 init 方法时,生成对应的具体类型。具体在执行 init 方法时是如何区分 immutable 还是 mutable 未搞清楚。
使用 Method swizzling 进行方法交换
上面说了,使用 Category 会很麻烦,而且移植性较差。因此想到了使用 Method swizzling。在使用 Method swizzling 时,有一步是根据 类名 和 selector 获取响应的方法,即使用 class_getInstanceMethod(Class cls, SEL name)。如果你像下面这样写,就会出现问题了:
1 |
|
上面提到,NSArray 是类簇,是一个抽象类的集合。objectAtIndex: 真正所属的类应该是 __NSArrayI。因此,正确的写法应该这样:
1 |
|
NSArray 或者 NSDictionary 中不会有 nil 对象,但是可能会有 ‘空值’,因此使用
obj == [NSNUll null],如果使用obj == nil进行判断,那么这句话等于浪费。有关nil / Nil / NULL / NSNull,请参考这里
对于 NSNumber 、 NSDictionary 这些有 immutable 和 mutable 类使用 Method swizzling 时都需要注意以上问题,找到真正的 具体类 进行操作。
Delegate 使用
关于 delegate 的使用,需要注意三个问题:
1.delegate 属性都要为 weak,不解释
2.‘委托方’调用代理方法时,需要通过 respondsToSelector: 进行判断,否则代理对象没有实现这个方法,会导致崩溃
3.不要在单例中使用 delegate
代理属性 delegate 是一个弱引用指针,指向的是代理对象的的内存地址。
1 |
|
如果在单例中使用 delegate ,因为单例对象使用都是一个对象,这样 self.delegate 就会不断被从新赋值,只保留最后一个,这样最终只有一个对象响应代理方法,其他对象都不会响应。
NSnotification 使用
关于 NSnotification 的使用,需要注意一下几个问题:
1.注册问题
如果一个对象注册了一个通知,然后又注册了一次,这两次不会合并,通知回调会被调用两次。因此在注册通知的时候,需要在 init 或者 viewDidload 这些一般整个生命周期只执行一次的方法里注册,不要在一些可重入的方法里面注册。避免重复注册问题。
2.发送通知
建议所有的通知都要在 主线程 中发送,没有例外。如果在其他线程运行,需要发送通知时,回到主线程发送,否则注销通知时,因为发送通知和注销通知不在同一个线程,造成一些意想不到的结果(竞态条件)。
3.注销通知
如果在一个对象销毁时,不注销当前对象注册的通知,对象销毁后,再次向这个对象发送通知,会造成 crash。因此在类的 dealloc 方法中需要注销对象。
建议使用 [[NSNotificationCenter defaultCenter] removeObserver:self]; 这种整体注销的方式,避免遗漏。
NSTimer 使用
使用 NSTimer 时需要注意 ‘repeat timer’ 的释放问题。如果你想在 - (void)dealloc 中执行 [self.timer invalidate];,一般情况下都是释放不了的。原因如下:
1 |
|
在 dealloc 方法中,并不能将 timer 销毁,因为这个方法并不能执行。原因是:Timer 加到 Runloop 中,会被 Runloop 强引用,然后 Timer 对 self 有一个强引用,导致 self 不能够被释放,不能执行 dealloc 方法。
要想销毁 repeat 类型的 Timer,必须要执行 invalidate 方法。可以去手动 (action)方式去调用,也可以在执行 delloc 之前去执行 invalidate 方法。如果想要在 dealloc 方法中去销毁,可以自己封装一个类,给 Timer 传一个假的 target,如下:
1 |
|
当然这个解决方案不是我想的,具体请看作者原创。
线程安全问题
在多线程环境中,因为线程安全问题引发的 crash 有很多,尤其是对一些数据类型进行操作时。有人可能认为使用 immutable 类型的就安全了,但是并不是你想象的那样。请看下面示例:
1 |
|
上面的代码中可能会出现 crash。所以不要认为使用 immutable 类型的就线程安全了。处理线程安全问题,没有公式化的方法,不可能对所有用到的数据类型进行加锁,那样太损耗性能,只有对于一些特殊的数据对象,在读写时进行加锁。是否有必要加锁,写程序的时候还需要自己注意。
关于 ‘锁’ 的一些问题
今天写这个的时候,正好看到了南大今天发的《iOS知识小集》,讲述了一下关于锁的问题。文中这样描述:
为了保证线程安全,可能会使用 NSLock, @synchornized, pthread_mutex_t 等方法,但是加锁和解锁是非常昂贵的操作,对性能会有影响。可以用GCD提供的信号量来进行优化。如下是使用 锁 和使用 信号量 处理相同数据所需时间的对比:
1 |
|
程序运行结果如下图 (真机上测试运行) :

从上面的 Log 中可以看出,使用 锁 和使用 信号量 处理相同的数据,时间不是一个量级的。因此,在做优化的时候,建议使用 信号量 来代替锁。
总结
以上是我作为一个 iOS 开发新手,在近期遇到的一些 crash 问题。对此做一个总结,以提醒自己今后不会再犯相同的错误。可能总结的有遗漏,或者有一些问题。如果有什么问题,还请大家指正。
参考资料
2.打造Objective-C安全的Collection类型