iOS常见bug
前言:
对这两年修复的bug做了个简单的总结。
一、常见crash名词解释
SIGSEGV
一般情况下,SIGSEGV是由于内存地址不合法造成。因为无效的内存访问导致的,一般是指针指向不存在的地址所导致(Invalid memory reference);
SIGBUS
一般情况下,SIGBUS是因为内存地址没有对齐导致。
因为总线出错(bus error)。地址一般是先校验地址对齐再校验其他的,校验地址对齐后会放入数据总线,这时有问题就会报SIGBUS的错误。
SIGABRT
异常终止条件,例如abort()。
二、常见crash/bug分类整理
1、3、6是修复crash过程中常遇到的crash类型。
1. crashName=NSInvalidArgumentException ,crashReason=data parameter is nil
(1) [aMutableDictionary setObject:nil forKey:]; object can not be nil. [__NSDictionaryM removeObjectForKey:nil]: key cannot be nil (2) [aString hasSuffix:nil]; nil argument crash. [aString hasPrefix:nil]; nil argument crash. (3) aString = [NSMutableString stringWithString:nil];nil argument crash. aString = [[NSString alloc] initWithString:nil]; nil argument crash. (4) aURL = [NSURL fileURLWithPath:nil]; nil argument crash. (5) [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];data is nil crash (6) [aMutableArray addObject:nil], 添加nil crash。 (7) UIPasteboard *pasteboard = [UIPasteboard generalPasteboard]; pasteboard.string = nil;// setString参数nil则crash (8)if(controller.presentedViewController != nil) { [controller dismissViewControllerAnimated:NO completion:^{ //controller.presentedViewController为nil则presentViewController:方法会crash [controller presentViewController:self.imagePickerController animated:YES completion:nil]; }]; }else{ [controller presentViewController:self.imagePickerController animated:YES completion:nil]; }
2. 野指针
(1) self.dataArr = (NSMutableArray *)mopayConsumeOrderList.list;
[self.dataArr removeAllObjects];
其中mopayConsumeOrderList.list是NSArray * 类型,
运行后出现程序崩溃,原因是self.dataArr和mopayConsumeOrderList.list指向同一段内存,self.dataArr在运行多次之后mopayConsumeOrderList.list指向空时self.dataArr也变成野指针。
改成:self.dataArr 自己new一个,即:
self.dataArr = [NSMutableArray arrayWithArray:self.mopayConsumeOrderList.list];
(2)属性的内存管理语义设置的_unsafe_unretain,出现野指针;
(3) ios9.0之前Notification未注销带来的僵尸对象问题。
3. 数据类型错误带来的unrecognized selector crash
例1:常见业务调用时传参数应为NSString,业务方传入的数据为Dictionary,崩溃.
例2: 网络层中将NSData转换成Model,返回的数据和移动之家上定义的数据不一致,带来的崩溃。
例3: push 接收到的消息是string类型,当做NSNumber类型处理。
4. 数组取值越界crash
5. 关注新技术
(1)2017年6月开始苹果热修复crash
(2)ios10以后push使用UNUserNotificationCenter注册push,否则ios10、ios11都有可能crash。
(3)ios10等如果没在plist里设置key-value描述语,否则相机崩溃(蓝牙、日历、麦克风、相机、相册、通讯录等)
<key>NSPhotoLibraryUsageDescription</key>
<string>APP需要您的允许,才能访问相册</string>
6. 数据竞争带来的crash (SIGSEGV)
例1:NSMutableDictionary类型用objectForKey取数据和setValue: forKey:设置数据,有可能存在数据竞争,需要做保护。
例2: NSMutableArray存在同样的问题。遍历数组期间,不要对数组进行remove和insert操作,因为数组有保护机制,容易崩溃。可以先拷贝一份出来,对拷贝的数组遍历,对真正需要改变的数组操作。
7. selector不存在带来的crash (SIGSEGV)
[self respondsToSelector:@sel(aSel)]的判断,否则有可能带来的unrecognized selector crash。
例1. [toggle addTarget:target action:selector forControlEvents:UIControlEventValueChanged]; // selector是否存在
例2. [self performSelector:@selector(aSelector)]; // aSelector是否存在
8. 存储类型必须是对象类型
[self.dic setObject:[NSArray arrayWithArray: self.mpShopArr] forKey:@"shopArr"];
[[NSUserDefaults standardUserDefaults] setObject:self.dic forKey:[JVAccountManager sharedAccount].edper];崩溃
由于self.mpShopArr里存的数据结构包含了很多类型,其中一个数据类型是int,不是object,导致第一句没报错、但第二句就报错了。
最终存到[NSUserDefaults standardUserDefaults] 里的数据类型,无论包装多少层,所有的内容必须是对象类型。
9. 内存泄漏(bug)
(1)单例+RACObserve带来内存泄漏。
[RACObserve(self.viewModel, modulesShouldRefresh) subscribeNext:^(NSNumber * modulesShouldRefresh) {
@strongify(self);
if ([modulesShouldRefresh boolValue] == YES) {
[self setShowLoading:NO];
[self.highFrequencyTitleView requestHeadInfoForce:@(YES)];
[self refreshModules:YES];
} }];
由于viewModel设成了不恰当的单例,对其RACObserve导致self(homeVC)不能释放,也就是当切换账号的时候,原来的homeVC没释放,新的homeVC又创建了一个,导致下拉刷新时,原来的没有释放的homeVC触发了接口,现在的homeVC也触发了接口,导致接口请求多次。所以,单例不能随便乱用,除非可以确定对象与APP同生共死生命周期没有问题。
(2)performSelector: withObject: afterDelay: 必须有合适的时机cancelPreviousPerformRequestsWithTarget:selector:object:,否则会导致内存泄漏。
可以用weak类型的dispatch_after避免内存泄漏:
__weak CRMHomeViewModel *weakSelf = self;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delay * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[weakSelf requestLiveData:YES];
});
(3) NSTimer会保留其目标对象,如果不加以注意,就会持有保留环,造成内存泄露。
scheduledTimerWithTimeInterval: target:self selector: userInfo: repeats:,重复模式的计时器,必须手动调用[_aTimer invalidate]方法,才能令其停止。
self持有timer,timer的target又是self, timer不释放,导致self也不能调用dealloc,所以timer调用invalidate的时机也不好把控。ios10以后,使用block来打破保留环,定义一个弱引用,令其指向self,然后使块捕获这个引用,而不直接去捕获普通的self变量。也就是说,self不会为计时器所保留。当块开始执行时,立刻生成strong引用,以保证实例在执行期间持续存活:
__weak SLVTimerTestViewController *weakSelf = self; _aTimer = [NSTimer scheduledTimerWithTimeInterval:5.0 repeats:YES block:^(NSTimer* timer){ SLVTimerTestViewController *strongSelf = weakSelf; [strongSelf timer_invoke]; }];
三、crash防护
参考《Baymax:网易iOS App运行时Crash自动防护实践》,并进行了一些调研和实践,对以下5种常见crash进行了分析,并写出了对应的防护代码。写好的代码已经过初步测试。前进的一小步,记录下来。对应的4篇博客: