美文网首页iOS扩展iOSiOS bug修复
iOS中UITableview频繁reloadData引起的崩溃

iOS中UITableview频繁reloadData引起的崩溃

作者: kyson老师 | 来源:发表于2019-03-21 10:52 被阅读197次

    本系列博客是本人的开发笔记。为了方便讨论,本人新建了一个微信群(iOS技术讨论群),想要加入的,请添加本人微信:zhujinhui207407,【加我前请备注:iOS 】,本人博客http://www.kyson.cn 也在不停的更新中,欢迎一起讨论

    今天一上班 看到如下崩溃日志,发现还不在少数

    libobjc.A.dylib objc_msgSend + 8
    1 CoreFoundation    -[NSDictionary descriptionWithLocale:indent:] + 340
    2 Foundation    _NSDescriptionWithLocaleFunc + 76
    3 CoreFoundation    ___CFStringAppendFormatCore + 8384
    4 CoreFoundation    _CFStringCreateWithFormatAndArgumentsAux2 + 244
    5 CoreFoundation    _CFLogvEx2 + 152
    6 CoreFoundation    _CFLogvEx3 + 156
    7 Foundation    __NSLogv + 132
    8 Foundation    NSLog + 32
    9 UIKit -[UITableView reloadData] + 1612
    10 DadaStaff    -[UITableView(MJRefresh) mj_reloadData] (UIScrollView+MJRefresh.m:146)
    11 DadaStaff    dzn_original_implementation (UIScrollView+EmptyDataSet.m:611)
    

    对于这个崩溃问题,笔者一开始研究的点在于前两个方法,即descriptionWithLocale: indent:objc_msgSend。疑问主要在以下几点:

    1. 既然objc_msgSend是崩溃前的最后一个调用的方法,那如何获取崩溃点调用的方法名/类名
    2. 如果objc_msgSend不能定位到崩溃,那是否问题可能出在descriptionWithLocale: indent:

    带着这两个疑问,笔者慢慢进行这三个方法的拆解

    objc_msgSend:

    objc_msgSend方法大家都很熟悉了,它的伪代码如下:

    id objc_msgSend(id self, SEL _cmd, ...) {
      Class class = object_getClass(self);
      IMP imp = class_getMethodImplementation(class, _cmd);
      return imp ? imp(self, _cmd, ...) : 0;
    }
    

    因为objc_msgSend是用汇编写的,针对不同架构有不同的实现。如下为 x86_64 架构下的源码,可以在 objc-msg-x86_64.s 文件中找到:

    ENTRY   _objc_msgSend
        MESSENGER_START
    
        NilTest NORMAL
        GetIsaFast NORMAL       // r11 = self->isa
        CacheLookup NORMAL      // calls IMP on success
        NilTestSupport  NORMAL
        GetIsaSupport      NORMAL
    // cache miss: go search the method lists
    LCacheMiss:
        // isa still in r11
        MethodTableLookup %a1, %a2  // r11 = IMP
        cmp %r11, %r11      // set eq (nonstret) for forwarding
        jmp *%r11           // goto *imp
        END_ENTRY   _objc_msgSend
    

    这里面包含一些有意义的宏:

    NilTest 宏,判断被发送消息的对象是否为 nil 的。如果为 nil,那就直接返回 nil。这就是为啥也可以对 nil 发消息。
    GetIsaFast宏可以『快速地』获取到对象的 isa 指针地址(放到 r11 寄存器,r10 会被重写;在 arm 架构上是直接赋值到 r9)
    CacheLookup 这个宏是在类的缓存中查找 selector 对应的 IMP(放到 r10)并执行。如果缓存没中,那就得到 Class 的方法表中查找了。
    MethodTableLookup 宏是重点,负责在缓存没命中时在方法表中负责查找 IMP:

    .macro MethodTableLookup
        MESSENGER_END_SLOW
        SaveRegisters
        // _class_lookupMethodAndLoadCache3(receiver, selector, class)
        movq    $0, %a1
        movq    $1, %a2
        movq    %r11, %a3
        call    __class_lookupMethodAndLoadCache3
        // IMP is now in %rax
        movq    %rax, %r11
        RestoreRegisters
    .endmacro
    

    从上面的代码可以看出方法查找 IMP 的工作交给了 OC 中的 _class_lookupMethodAndLoadCache3 函数,并将 IMP 返回(从 r11 挪到 rax)。最后在 objc_msgSend 中调用 IMP。
    全局搜索_class_lookupMethodAndLoadCache3可以找到其实现:

    IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
    {
        return lookUpImpOrForward(cls, sel, obj,
                                  YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
    }
    

    lookUpImpOrForward 调用时使用缓存参数传入为 NO,因为之前已经尝试过查找缓存了。IMP lookUpImpOrForward(Class cls, SEL sel, id inst, bool initialize, bool cache, bool resolver) 实现了一套查找 IMP 的标准路径,也就是在消息转发(Forward)之前的逻辑。


    后续的消息转发流程笔者就不一一赘述了。主要是之前阅读过这篇文章:
    深入iOS系统底层之crash解决方法介绍
    里面有说过通过objc_msgSend方法来找到崩溃点,但因为过程繁琐,于是放弃。

    descriptionWithLocale: indent:

    -description :当你输出一个对象时会调用该函数,如:NSLog(@"%@",model);
    -debugDescription :当你在使用 LLDB 在控制台输入 po model 时会调用该函数
    -descriptionWithLocale: indent: :存在于 NSArray 或 NSDictionary 等类中。当类中有这个函数时,它的优先级为 -descriptionWithLocale: indent: > -description

    由以上分析可知,只有在调用NSLog方法后才可能调用descriptionWithLocale: indent:方法。但从笔者的源代码分析并没有显示的调用NSLog方法,为什么会调用descriptionWithLocale: indent:并在其后发生崩溃。久寻无果后也只能放弃该中可能性。

    最后抱着试试看的态度,只能搜最后一个可能引起崩溃的方法:

    [UITableView reloadData]

    这个方法我们再熟悉不过了,UITableView的reloadData方法用于刷新UITableView。然而最不可能是崩溃的原因的方法却是发生崩溃的地方,在这里Reporting crash on UITableview reloadData,主要讲述的就是由于多次调用reloadData方法引起的崩溃:

    Hooray - finally found the reason for this elusive problem. The crash was being due to the user being in edit mode within a UITextfield within a cell in the table when the reloadData was being called (as a result of the user tapping elsewhere or rotating the iPad, which also called ReloadData).
    I fixed the issue by preceeding any [self.tableView ReloadData] with [self.view endEditing:YES] to ensure that the keyboard was dismissed and cells were not in an edit mode.
    Does make sense but what a nasty trap.

    修改完后收工,等待上线后查看效果吧。

    引用

    iOS Kingdom — 模型信息输出

    Reporting crash on UITableview reloadData

    让我们来搞崩 Cocoa 吧(黑暗代码)

    深入iOS系统底层之crash解决方法介绍

    Objective-C 消息发送与转发机制原理

    相关文章

      网友评论

        本文标题:iOS中UITableview频繁reloadData引起的崩溃

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