今天一上班 看到如下崩潰日志,發(fā)現(xiàn)還不在少數(shù)
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)
對于這個(gè)崩潰問題,筆者一開始研究的點(diǎn)在于前兩個(gè)方法刽沾,即descriptionWithLocale: indent:
和objc_msgSend
。疑問主要在以下幾點(diǎn):
- 既然
objc_msgSend
是崩潰前的最后一個(gè)調(diào)用的方法尘分,那如何獲取崩潰點(diǎn)調(diào)用的方法名/類名 - 如果
objc_msgSend
不能定位到崩潰,那是否問題可能出在descriptionWithLocale: indent:
帶著這兩個(gè)疑問丸氛,筆者慢慢進(jìn)行這三個(gè)方法的拆解
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;
}
因?yàn)?code>objc_msgSend是用匯編寫的,針對不同架構(gòu)有不同的實(shí)現(xiàn)缓窜。如下為 x86_64 架構(gòu)下的源碼定续,可以在 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
宏,判斷被發(fā)送消息的對象是否為 nil 的禾锤。如果為 nil私股,那就直接返回 nil。這就是為啥也可以對 nil 發(fā)消息恩掷。
GetIsaFast
宏可以『快速地』獲取到對象的 isa 指針地址(放到 r11 寄存器倡鲸,r10 會(huì)被重寫;在 arm 架構(gòu)上是直接賦值到 r9)
CacheLookup
這個(gè)宏是在類的緩存中查找 selector 對應(yīng)的 IMP(放到 r10)并執(zhí)行黄娘。如果緩存沒中峭状,那就得到 Class 的方法表中查找了。
MethodTableLookup
宏是重點(diǎn)逼争,負(fù)責(zé)在緩存沒命中時(shí)在方法表中負(fù)責(zé)查找 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
函數(shù)宁炫,并將 IMP 返回(從 r11 挪到 rax)。最后在 objc_msgSend 中調(diào)用 IMP氮凝。
全局搜索_class_lookupMethodAndLoadCache3
可以找到其實(shí)現(xiàn):
IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
return lookUpImpOrForward(cls, sel, obj,
YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}
lookUpImpOrForward
調(diào)用時(shí)使用緩存參數(shù)傳入為 NO羔巢,因?yàn)橹耙呀?jīng)嘗試過查找緩存了。IMP lookUpImpOrForward(Class cls, SEL sel, id inst, bool initialize, bool cache, bool resolver)
實(shí)現(xiàn)了一套查找 IMP 的標(biāo)準(zhǔn)路徑罩阵,也就是在消息轉(zhuǎn)發(fā)(Forward)之前的邏輯竿秆。
后續(xù)的消息轉(zhuǎn)發(fā)流程筆者就不一一贅述了。主要是之前閱讀過這篇文章:
深入iOS系統(tǒng)底層之crash解決方法介紹
里面有說過通過objc_msgSend
方法來找到崩潰點(diǎn)稿壁,但因?yàn)檫^程繁瑣幽钢,于是放棄。
descriptionWithLocale: indent:
-description :當(dāng)你輸出一個(gè)對象時(shí)會(huì)調(diào)用該函數(shù)傅是,如:NSLog(@"%@",model);
-debugDescription :當(dāng)你在使用 LLDB 在控制臺(tái)輸入 po model 時(shí)會(huì)調(diào)用該函數(shù)
-descriptionWithLocale: indent: :存在于 NSArray 或 NSDictionary 等類中匪燕。當(dāng)類中有這個(gè)函數(shù)時(shí),它的優(yōu)先級(jí)為 -descriptionWithLocale: indent: > -description
由以上分析可知喧笔,只有在調(diào)用NSLog
方法后才可能調(diào)用descriptionWithLocale: indent:
方法帽驯。但從筆者的源代碼分析并沒有顯示的調(diào)用NSLog
方法,為什么會(huì)調(diào)用descriptionWithLocale: indent:
并在其后發(fā)生崩潰书闸。久尋無果后也只能放棄該中可能性尼变。
最后抱著試試看的態(tài)度,只能搜最后一個(gè)可能引起崩潰的方法:
[UITableView reloadData]
這個(gè)方法我們再熟悉不過了浆劲,UITableView的reloadData方法用于刷新UITableView嫌术。然而最不可能是崩潰的原因的方法卻是發(fā)生崩潰的地方哀澈,在這里Reporting crash on UITableview reloadData,主要講述的就是由于多次調(diào)用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.
修改完后收工度气,等待上線后查看效果吧割按。