iOS扩展iOSiOS bug修复

iOS中UITableview频繁reloadData引起的崩溃

2019-03-21  本文已影响197人  kyson老师

本系列博客是本人的开发笔记。为了方便讨论,本人新建了一个微信群(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 消息发送与转发机制原理

上一篇下一篇

猜你喜欢

热点阅读