使用 Delegate 和 NSNotification 需要注意的几个坑 ——记 iOS 两次线上严重 Bug

这周应用上线 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 一次就够了,一定要长点记性!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,045评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,114评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 147,120评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,902评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,828评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,132评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,590评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,258评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,408评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,335评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,385评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,068评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,660评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,747评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,967评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,406评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,970评论 2 341

推荐阅读更多精彩内容

  • *面试心声:其实这些题本人都没怎么背,但是在上海 两周半 面了大约10家 收到差不多3个offer,总结起来就是把...
    Dove_iOS阅读 27,113评论 29 470
  • 1.属性readwrite,readonly,assign,retain,copy,nonatomic 各是什么作...
    曾令伟阅读 1,040评论 0 10
  • 37.cocoa内存管理规则 1)当你使用new,alloc或copy方法创建一个对象时,该对象的保留计数器值为1...
    如风家的秘密阅读 823评论 0 4
  • 转:http://www.cocoachina.com/programmer/20151019/13746.htm...
    Style_伟阅读 1,288评论 0 3
  • 注:此文章来源:Job_Yang 的简书 1. Object-c的类可以多重继承么?可以实现多个接口么?Categ...
    广益散人阅读 1,340评论 0 13