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则crash8if(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取数据和setValueforKey:设置数据,有可能存在数据竞争,需要做保护。

  例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篇博客:

  1.  [crash详解与防护] unrecognized selector crash
  2.  [crash详解与防护] Container类型的crash和NSString类型的crash

  3.  [crash详解与防护] NSTimer crash

  4.  [crash详解与防护] NSNotification crash

 

posted @ 2017-08-25 18:57  Xylophone  Views(2102)  Comments(0Edit  收藏  举报