一些代码进行容错处理,如果处理的好,会减少很多 crash。尤其对于像我这样的新手,稍不注意就会
写出几十个 bug。以下是针对刚入职这段时间所做项目的一个总结,同时提醒自己不要再犯同样的错误。

美好的一天,从没有 bug 开始~


数据类型

新手(像我这样)在使用一些常用的数据类型时,例如 NSArrayNSDictionaryNSNumberNSString等,经常会遇到一些崩溃问题。如果平时写程序首先进行容错判断,会减少很多崩溃问题。下面列举一些新手需要注意的情况。

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
2
3
4
5
6
7
8
9
10

- (void)transType {

NSArray *testArray = @[@1,@2,@3];
NSDictionary *testDict = @{@"key":testArray};

NSNumber *testNumber = (NSNumber *) [testDict objectForKey:@"key"];

int iValue = [testNumber intValue]; // 程序会在此处崩溃
}

在我们初始化 NSNumber 对象时,可能会使用一些意想不到的对象进行初始化。例如上面 testNumer 实际获得的是一个 NSArray 类型的对象,并不能响应 intValue 方法,因此正确的写法应为:

1
2
3
if ([testNumber respondsToSelector:@selector(intValue)]) {
int iVaule = [testNumber 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.

简单说就是:类簇将一些私有的、具体的子类组合在一个公共的、抽象的超类下面,以这种方法来组织类可以简化一个面向对象框架的公开架构。这是一种基于 工厂模式 的实现。

NSArrayNSDictionaryNSNumberNSString 这些都是类簇。官方文档 通过 NSNumber 对类簇进行了解释。

针对 NSArray ,进行了如下测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14

- (void)classClustersTest {

id iArray1 = [NSArray alloc];
id iArray2 = [iArray1 init];

NSLog(@"%@",[[iArray1 class] description]); // __NSPlaceholderArray
NSLog(@"%@",[[iArray2 class] description]); // __NSArray0

id mArray1 = [NSMutableArray alloc];
id mArray2 = [mArray1 init];
NSLog(@"%@",[[mArray1 class] description]); // __NSPlaceholderArray
NSLog(@"%@",[[mArray2 class] description]); // __NSArrayM
}

可以看出,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
2
3
4
5
6
7
8
9
10
11
12

+ (void)load {
static dispatch_once_t onceToken;

dispatch_once(&onceToken, ^{

Method originalMethod = class_getClassMethod(NSClassFromString(@"NSArray"), @selector(objectAtIndex:));
Method newMthod = class_getClassMethod(NSClassFromString(@"NSArray"), @selector(myMethod));
method_exchangeImplementations(originalMethod, newMethod);

});
}

上面提到,NSArray 是类簇,是一个抽象类的集合。objectAtIndex: 真正所属的类应该是 __NSArrayI。因此,正确的写法应该这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

#import "SafeArray.h"
#import <objc/runtime.h>


@interface NSArray (SafeFunc)

@end

@implementation NSArray (SafeFunc)

- (id)safeObjectAtIndex:(NSUInteger)index {
if (index < self.count) {
id obj = [self safetObjectAtIndex:index];

if (obj == [NSNull null]) { // 为什么使用 [NSNull null]?
return nil;
}

return obj;
}

return nil;

}

@end

@implementation SafeArray


+ (void)load {
static dispatch_once_t onceToken;

dispatch_once(&onceToken, ^{

Method originalMethod = class_getClassMethod(NSClassFromString(@"NSArray"), @selector(objectAtIndex:));
Method newMthod = class_getClassMethod(NSClassFromString(@"NSArray"), @selector(safeObjectAtIndex:));
method_exchangeImplementations(originalMethod, newMethod);

});
}

@end

NSArray 或者 NSDictionary 中不会有 nil 对象,但是可能会有 ‘空值’,因此使用 obj == [NSNUll null] ,如果使用 obj == nil 进行判断,那么这句话等于浪费。有关 nil / Nil / NULL / NSNull,请参考这里

对于 NSNumberNSDictionary 这些有 immutable 和 mutable 类使用 Method swizzling 时都需要注意以上问题,找到真正的 具体类 进行操作。

Delegate 使用

关于 delegate 的使用,需要注意三个问题:

1.delegate 属性都要为 weak,不解释

2.‘委托方’调用代理方法时,需要通过 respondsToSelector: 进行判断,否则代理对象没有实现这个方法,会导致崩溃

3.不要在单例中使用 delegate

代理属性 delegate 是一个弱引用指针,指向的是代理对象的的内存地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

@protocol ZBDelegate <NSObject>

- (void)doSomething;

@end

@interface ClassA : NSObject

@property (nonatomic, weak) id<ZBDelegate> delegate;

@end

@implementation ClassA

- (void)delegateTest {
[self.delegate doSomething]; // self.delegate 指向 ClassB 的内存地址
}

@end

@interface ClassB : NSObject <ZBDelegate>

@end

@implementation ClassB

- (void)doSomething {
NSLog(@"hello");
}

@end

如果在单例中使用 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

@interface TimerTest : NSObject

@property (nonatomic, strong) NSTimer *timer;

@end

@implementation TimerTest

- (void)setupTimer {
self.timer = [NSTimer scheduledTimerWithTimeInterval:3.0f
target:self
selector:@selector(doSomething)
userInfo:nil
repeats:YES];
}

- (void)doSomething {
NSLog(@"hello");
}

- (void)dealloc {
[self.timer invalidate];
}

@end

dealloc 方法中,并不能将 timer 销毁,因为这个方法并不能执行。原因是:Timer 加到 Runloop 中,会被 Runloop 强引用,然后 Timerself 有一个强引用,导致 self 不能够被释放,不能执行 dealloc 方法。

要想销毁 repeat 类型的 Timer,必须要执行 invalidate 方法。可以去手动 (action)方式去调用,也可以在执行 delloc 之前去执行 invalidate 方法。如果想要在 dealloc 方法中去销毁,可以自己封装一个类,给 Timer 传一个假的 target,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51

// 创建类
@interface MTBWeakTimerTarget : NSObject

@property (nonatomic, weak) id target;
@property (nonatomic, assign) SEL selector;
@property (nonatomic, weak) NSTimer *timer;

@end

@implementation MTBWeakTimerTarget

- (void) fire:(NSTimer *) timer {
if (self.target) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[self.target performSelector:self.selector withObject:timer.userInfo];
#pragma clang diagnostic pop
} else {
[self.timer invalidate];
}
}
@end

@implementation MTBWeakTimer

+ (NSTimer *)scheduledTimerWithInterval:(NSTimeInterval)interval
target:(id)aTarget
selector:(SEL)aSelector
userInfo:(id)userInfo
repeats:(BOOL)repeats {
MTBWeakTimerTarget *timerTarget = [MTBWeakTimerTarget new];
timerTarget.target = aTarget;
timerTarget.selector = aSelector;
timerTarget.timer = [NSTimer scheduledTimerWithTimeInterval:interval
target:timerTarget selector:@selector(fire:) userInfo:userInfo
repeats:repeats];

return timerTarget.timer;
}

@end


// 使用

self.timer = [MTBWeakTimer scheduledTimerWithInterval:3.0
target:self
selector:@selector(doSomething)
userInfo:nil
repeats:YES];

当然这个解决方案不是我想的,具体请看作者原创

线程安全问题

在多线程环境中,因为线程安全问题引发的 crash 有很多,尤其是对一些数据类型进行操作时。有人可能认为使用 immutable 类型的就安全了,但是并不是你想象的那样。请看下面示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

- (void)threadSafe {
NSArray *dataArray = [NSArray array];

// in thread one
{
NSArray *otherArray = @[@1,@2,@3,@4,@5]; // array count = 5
dataArray = [otherArray copy];
}


// in thread two
{
NSArray *anotherArray = @[@1,@2,@3]; // array count = 3
dataArray = [anotherArray copy];
}

// in main thread
int a = dataArray[4]; // ????
}

上面的代码中可能会出现 crash。所以不要认为使用 immutable 类型的就线程安全了。处理线程安全问题,没有公式化的方法,不可能对所有用到的数据类型进行加锁,那样太损耗性能,只有对于一些特殊的数据对象,在读写时进行加锁。是否有必要加锁,写程序的时候还需要自己注意。

关于 ‘锁’ 的一些问题

今天写这个的时候,正好看到了南大今天发的《iOS知识小集》,讲述了一下关于锁的问题。文中这样描述:

为了保证线程安全,可能会使用 NSLock, @synchornized, pthread_mutex_t 等方法,但是加锁和解锁是非常昂贵的操作,对性能会有影响。可以用GCD提供的信号量来进行优化。如下是使用 和使用 信号量 处理相同数据所需时间的对比:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85

@implementation ViewController

- (void)viewDidLoad {
[super viewDidLoad];

for (int i = 0; i < 5; i++) {
[self lockTime];
}

NSLog(@"=========================================");

for (int i = 0; i < 5; i++) {
[self semaphoreTime];
}

}

double subtractTimes(uint64_t endTime, uint64_t startTime) {

uint64_t difference = endTime - startTime;
static double conversion = 0.0;

if(conversion == 0.0) {

mach_timebase_info_data_t info;
kern_return_t err = mach_timebase_info(&info);

if(err == 0)
conversion = 1e-9 * (double) info.numer / (double) info.denom;
}

return conversion * (double)difference;
}

// 使用锁
- (void)lockTime {


dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

NSMutableSet *items = [NSMutableSet set];

NSLock *lock = [NSLock new];

uint64_t start = mach_absolute_time();

dispatch_apply(50, queue, ^(size_t inddex) {
for (int i = 0; i < 10000; i++) {
[lock lock];
[items addObject:@"hi"];
[lock unlock];
}
});

uint64_t stop = mach_absolute_time();

NSLog(@"use lock time :%f", subtractTimes(stop, start));
}

// 使用信号量
- (void)semaphoreTime {

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

NSMutableSet *items = [NSMutableSet set];

dispatch_semaphore_t itemLock = dispatch_semaphore_create(1);

uint64_t start = mach_absolute_time();

dispatch_apply(50, queue, ^(size_t inddex) {
for (int i = 0; i < 10000; i++) {
dispatch_semaphore_wait(itemLock, DISPATCH_TIME_FOREVER);
[items addObject:@"hi"];
dispatch_semaphore_signal(itemLock);
}
});

uint64_t stop = mach_absolute_time();

NSLog(@"use semaphore time :%f", subtractTimes(stop, start));
}

@end

程序运行结果如下图 (真机上测试运行) :

锁与信号量对比输出

从上面的 Log 中可以看出,使用 和使用 信号量 处理相同的数据,时间不是一个量级的。因此,在做优化的时候,建议使用 信号量 来代替锁。

总结

以上是我作为一个 iOS 开发新手,在近期遇到的一些 crash 问题。对此做一个总结,以提醒自己今后不会再犯相同的错误。可能总结的有遗漏,或者有一些问题。如果有什么问题,还请大家指正。

参考资料

1.从NSArray看类簇

2.打造Objective-C安全的Collection类型

3.How Not to Crash #3: NSNotification

4.iOS 中的 NSTimer

5.南峰子-iOS知识小集