美文网首页程序员iOS 开发 iOS
使用 Delegate 和 NSNotification 需要注

使用 Delegate 和 NSNotification 需要注

作者: yanging | 来源:发表于2016-02-21 22:55 被阅读2648次

    这周应用上线 App Store 之后崩溃数字花花的涨,惨不忍睹。App 新增加 IM 功能,用的第三方环信的 IM SDK。最终发现的两个严重 Bug 都是在改动环信的 UI 代码时引入的,改动第三方代码最容易引发 bug。由于 Bug 只出现在 iOS 8 系统,而开发和测试都使用 iOS 9,所以问题一直没被发现。

    Delegate

    第一个 bug 在打开聊天窗口时偶尔会触发,问题出现在[EaseMessageViewController didBecomeActive]方法中的UITableView reloadData这一行代码上,在 iOS 9 没有崩溃问题。拿了 iOS 8 系统的机子,打断点,不是每次打开聊天窗口都会崩溃,得反复点开关闭窗口,几次之后就崩溃了,崩溃时 UITableView 竟然为空,对象被释放了!为什么在 didBecomeActive 时释放对象了,聊天窗口还在,为什么 UITableView 被释放了?而其实窗口对象也被释放了,所有的属性华丽丽地都变成 nil。

    #pragma mark - notification
    - (void)didBecomeActive
    {
        self.dataArray = [[self formatMessages:self.messsagesSource] mutableCopy];
        [self.tableView reloadData];
        
        //回到前台时
        if (self.isViewDidAppear)
        {
            NSMutableArray *unreadMessages = [NSMutableArray array];
            for (EMMessage *message in self.messsagesSource)
            {
                if ([self _shouldSendHasReadAckForMessage:message read:NO])
                {
                    [unreadMessages addObject:message];
                }
            }
            if ([unreadMessages count])
            {
                [self _sendHasReadResponseForMessages:unreadMessages isRead:YES];
            }
            
            [_conversation markAllMessagesAsRead:YES];
        }
    }
    

    于是在dealloc里打日志,在退出窗口的时候,dealloc并没有被调用,也就是EaseMessageViewController并没有被释放。而每次didBecomeActive的时候,就释放了。什么鬼?到底是什么把EaseMessageViewController纠缠住了,在窗口退出时对象没被释放?

    如果把[UITableView reloadData]注释掉,对象就不会被释放。也就是说 bug 出现这这,重新加载UITableView时清空了什么?问题大概出现在 dataSource 里,cellForRowAtIndexPath这里面应该有对象没被释放。代码如下:

    - (UITableViewCell *)messageViewController:(UITableView *)tableView cellForMessageModel:(id<IMessageModel>)model
    {
        if (model.bodyType == eMessageBodyType_Text) {
            if (model.xMessageType == MessageBodyType_ImageText) {
                NSString *cellIdentifier = @"MessageBodyType_ImageText";
                ServiceTableViewCell *cell = (ServiceTableViewCell *)[tableView dequeueReusableCellWithIdentifier:cellIdentifier];
                if (!cell) {
                    cell = [[ServiceTableViewCell alloc] init];
                    cell.delegate = self;
                }
                cell.model = model;
                
                return cell;
            } 
    
            NSString *CellIdentifier = [EaseBaseMessageCell cellIdentifierWithModel:model];
            //发送cell
            EaseBaseMessageCell *sendCell = (EaseBaseMessageCell *)[tableView dequeueReusableCellWithIdentifier:CellIdentifier];
            
            // Configure the cell...
            if (sendCell == nil) {
                sendCell = [[EaseBaseMessageCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier model:model];
                sendCell.selectionStyle = UITableViewCellSelectionStyleNone;
            }
            sendCell.model = model;
    //        DLog(@"%@",model.text);
            return sendCell;
        }
        return nil;
    }
    

    ServiceTableViewCell是我添加的,先把自己改动的东西注释掉,只留EaseBaseMessageCell,然后一切正常了,退出窗口时dealloc会被吊用。原来都是ServiceTableViewCell惹的祸,为什么它没被释放,delegate?EaseBaseMessageCell 也有指定 delegate,但它正常.

    - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
     
        if (_delegate && [_delegate respondsToSelector:@selector(messageViewController:cellForMessageModel:)]) {
            UITableViewCell *cell = [_delegate messageViewController:tableView cellForMessageModel:model];
            if (cell) {
                if ([cell isKindOfClass:[EaseMessageCell class]]) {
                    EaseMessageCell *emcell= (EaseMessageCell*)cell;
                    if (emcell.delegate == nil) {
                        emcell.delegate = self;
                    }
                }
                return cell;
            }
        }
    }
    

    对比两个 Cell 的 delegate 声明:

    @property (strong, nonatomic) id<ServiceMessageCellDelegate> delegate;
    
    @property (weak, nonatomic) id<EaseMessageCellDelegate> delegate;
    

    Bug 终于浮出水面,delegate 被声明为 strong,导致强引用,ServiceTableViewCell 和 UITableView 循环引用,导致窗口退出时,UITableView 无法被释放。而在 didBecomeActive中调用 [self.tableView reloadData];,释放了 cell,UITableView 也随之释放,EaseMessageViewController 对象也就被释放了。

    但不理解的是,为什么这个问题只在 iOS 8 出现?释放的对象是之前打开并退出的窗口,当前窗口是一个新的对象,为什么当前对象被释放了?这是一个问题。

    NSNotification

    第二个 Bug 是出现大量类似, [UITableViewCellContentView chatKeyboardWillChangeFrame:] unrecognized selector 的崩溃信息,每次被调用的 UI 类还都不一样,有UILabel __NSCFString UIWebSelectionAssistant _CUIThemePixelRendition…… 问题很怪异,为什么这么多的对象都会调用chatKeyboardWillChangeFrame:这个方法?

    点击每个 bug 查看 issue 详情,都有这句-[NSNotificationCenter postNotificationName:object:userInfo:]。起初怀疑chatKeyboardWillChangeFrame方法有问题。搜索代码,只有聊天窗口的自定义的 ToolBar 文件里有这个方法的实现,这个方法只有在键盘发送变化发起通知的时候才会被调用。只在这个文件里才会发送通知去调用这个方法,为什么 UITableViewCell 和 UILabel 也会去调用这个方法?对比环信的自定义 ToolBar 代码文件,才发现通知没有被 remove。不知当初出于什么原因把dealloc代码给删了,随之删除的还有removeObserver。只有用户退出聊天窗口再进来,由于通知观察者没被删除,导致每次键盘变化,就有超过 1 个通知都会被发送,系统可能随意指给了一个其他的对象,没有实现这个方法的对象,然后就 Crash 了,因为unrecognized selector

    解决方法,在dealloc中添加removeObserver

    - (void)dealloc
    {
        [[NSNotificationCenter defaultCenter] removeObserver:self name:UIKeyboardWillChangeFrameNotification object:nil];
        
    }
    

    NSKeyedUnarchiver 也有一个坑

    以下代码在 iOS 8 可能会崩溃。

     NSString *abslutePath = [NSString stringWithFormat:@"%@/%@.plist", [self pathInCacheDirectory:kPathResponseCache], [requestPath md5]];
        NSData *data = [NSData dataWithContentsOfFile:abslutePath];
        NSDictionary *jsonObject = [NSKeyedUnarchiver unarchiveObjectWithData:data];
        return jsonObject;// [NSMutableDictionary dictionaryWithContentsOfFile:abslutePath];
    

    崩溃信息:

    NSInvalidArgumentException(SIGABRT)
    *** -[NSKeyedUnarchiver initForReadingWithData:]: incomprehensible archive version (-1)

    查看苹果的开发文档

    This method raises an NSInvalidArgumentException if the file at path does not contain a valid archive.

    如果 data 存储不使用[NSKeyedArchiver archivedDataWithRootObject:data],那么调用NSKeyedUnarchiver就会触发这个异常导致程序崩溃。这个异常在 iOS 9 并不会触发,只会返回 nil。解决方法:

     NSData *data = [NSData dataWithContentsOfFile:abslutePath];
        NSDictionary *jsonObject;// = [NSKeyedUnarchiver unarchiveObjectWithData:data];
        @try {
            jsonObject = [NSKeyedUnarchiver unarchiveObjectWithData:data];
        } @catch ( NSException *ex ) {
            //do whatever you need to in case of a crash
            [[NSFileManager defaultManager] removeItemAtPath:abslutePath error:nil];
        }
    
    

    在代码中出现这个问题是由于缓存的问题,最开始时并没有使用archivedDataWithRootObject:,版本更新时没有先去清理老版本的缓存,导致调用NSKeyedUnarchiver时崩溃。更新版本要记得检查数据版本的一致性。

    总结

    1. Delegate要用声明为weak
    2. 每一个NSNotification都记得加removeObserver
    3. unarchiveObjectWithData: 加异常处理@try { } @catch ( NSException *ex ){}
    4. 版本更新时检查数据版本。

    上线前要在每个支持的系统版本测试通过。如果在提交前有检查代码内存是否有泄漏,可能就会发现第一个 Bug 了,学习用工具 Instruments。经验不足更要小心谨慎,思考周全,测试全面,这种 Bug 一次就够了,一定要长点记性!

    相关文章

      网友评论

        本文标题:使用 Delegate 和 NSNotification 需要注

        本文链接:https://www.haomeiwen.com/subject/nqppkttx.html