iOS中UITableview频繁reloadData引起的崩溃
本系列博客是本人的开发笔记。为了方便讨论,本人新建了一个微信群(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
。疑问主要在以下几点:
- 既然
objc_msgSend
是崩溃前的最后一个调用的方法,那如何获取崩溃点调用的方法名/类名 - 如果
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.
修改完后收工,等待上线后查看效果吧。