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类型