一個(gè)線上bug ,一直沒法重現(xiàn),但是崩潰率不低,這就問題了有點(diǎn)頭疼啦

上述就是具體的崩潰信息,原本以為定位這么仔細(xì)就可以立馬找出原因,然而并沒有。
這邊用了 DZNEmptyDataSet 和 CHTCollectionViewWaterfallLayout ,崩潰的點(diǎn)也是在此處出現(xiàn)的。。。
目前沒法重現(xiàn)這一個(gè) Bug, 只能通過圖中返回的情況,定位到代碼中:
- (CGSize)collectionViewContentSize {
NSInteger numberOfSections = [self.collectionView numberOfSections];
if (numberOfSections == 0) {
return CGSizeZero;
}
CGSize contentSize = self.collectionView.bounds.size;
contentSize.height = [self.columnHeights[0] floatValue];
return contentSize;
}
- (BOOL)dzn_canDisplay {
if (self.emptyDataSetSource && [self.emptyDataSetSource conformsToProtocol:@protocol(DZNEmptyDataSetSource)]) {
if ([self isKindOfClass:[UITableView class]] || [self isKindOfClass:[UICollectionView class]] || [self isKindOfClass:[UIScrollView class]]) {
return YES;
}
}
return NO;
}
可以確定是 崩在 dzn_canDisplay 這里,但是嘗試好多遍都不知道問題在哪里...
一、 猜
猜測,是否調(diào)用 [self.collectionView numberOfSections] 的時(shí)候, self.collectionView 的時(shí)候已經(jīng)被提前釋放或者說已經(jīng)被干掉啦。
首先可以肯定的是,當(dāng) self.collectionView 被干掉此處肯定會(huì)崩,但是此種情況基本不存在,在 CHTCollectionViewWaterfallLayout 中它是不可被修改的,而外部的collectionView 又是在生命周期中,不會(huì)被干掉,所以此處排除。。。
另外,如果是 self.collectionView 的問題,那么 上述圖中只會(huì)截止崩在該位置,不會(huì)繼續(xù)走 dzn_canDisplay等方法。
二、理一下常見的 Carsh
再判斷,可以肯定的是什么造成啦 collectionViewContentSize 和 dzn_canDisplay方法有問題,而又沒有頭緒...
回過頭來,先看看分析iOS Crash文件:符號(hào)化iOS Crash文件的3種方法,需要使用Xcode符號(hào)化 crash log,我們需要下面所列的3個(gè)文件:
- crash報(bào)告(.crash文件)
- 符號(hào)文件 (.dsymb文件)
- 應(yīng)用程序文件 (appName.app文件,把IPA文件后綴改為zip,然后解壓,Payload目錄下的appName.app文件), 這里的appName是我們的應(yīng)用程序的名稱。
現(xiàn)在關(guān)鍵問題,這bug 根本一直不能在重現(xiàn),可以單純的看到堆棧信息的崩潰日志,再想想一般是什么原因會(huì)造成崩潰,回顧下 常見的Crash類型:
-
2-1、看門狗
看門狗也就是 Watchdog 機(jī)制,它是iOS為了保持用戶界面的響應(yīng)引入的一種機(jī)制, 。如果我們的應(yīng)用未能及時(shí)的響應(yīng)一些用戶界面事件,如啟動(dòng)、暫停、恢復(fù)和終止,Watchdog就會(huì)殺死程序并生成一個(gè)Watchdog超時(shí)崩潰報(bào)告。Watchdog超時(shí)時(shí)間并沒有明文規(guī)定,但通常會(huì)少于網(wǎng)絡(luò)超時(shí)。(5秒不一定正確)
場景:
- 主線程執(zhí)行同步的網(wǎng)絡(luò)請(qǐng)求,而且請(qǐng)求時(shí)間特別長。
- 主線程死鎖。
- 長時(shí)間讀寫本地文件
...
// 放在 AppDelegate didFinishLaunchingWithOptions
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"永遠(yuǎn)不會(huì)調(diào)用");
});
NSLog(@"永遠(yuǎn)不會(huì) run");

-
2-2、用戶強(qiáng)制退出
類似強(qiáng)制關(guān)機(jī)的情況。
-
2-3、內(nèi)存不夠
在我們App運(yùn)行的過程中,系統(tǒng)內(nèi)存緊張時(shí)通常會(huì)先發(fā)警告,同時(shí)把后臺(tái)掛起的程序終止掉,最終如果還是內(nèi)存不夠的話就會(huì)終止掉當(dāng)前前臺(tái)的進(jìn)程。
也提醒我們要及時(shí)的殺掉不用的內(nèi)存,否則內(nèi)存占用越來越高,一旦超過系統(tǒng)限制就會(huì)被系統(tǒng)殺死,然后就Carsh 啦。
-
2-4、自己產(chǎn)生的 bug
最常見的數(shù)組越界,或者其他五花八門的,反正就是自己產(chǎn)生的問題,像上述遇到的問題。。。
來自 常見的Crash類型。
此處提醒,去看看 DZNEmptyDataSet 和 CHTCollectionViewWaterfallLayout 中的 issues, 是不是里面的問題,到它們的 github 上的 issues 轉(zhuǎn)了一大圈,也沒有類似的問題...
三、盡量讓其重現(xiàn)
線上的崩潰率不低,但是為什么我們自己測試不出來啦,暫時(shí)還是只能去分析具體崩潰的位置, 在又一次認(rèn)真的看堆棧崩潰信息發(fā)現(xiàn), crash 在 objc_msgSend(),而發(fā)生這種情況的原因可能是:
- 向一個(gè)已經(jīng)釋放的對(duì)象發(fā)送消息,野指針之類的
- 接收者的內(nèi)存錯(cuò)誤。
反正就是接收者的問題,而我上面圖中的那就是 self.emptyDataSetSource 啦,此時(shí)在想難道是它被提前釋放掉了,但是下面這個(gè) view ,無論如何都是在是會(huì)返回的啊
- (UIView *)customViewForEmptyDataSet:(UIScrollView *)scrollView
而且對(duì)于 CollectionView 或者 viewModel 都是強(qiáng)引用啊,在生命周期內(nèi)不會(huì)自己釋放掉啊。
PS一個(gè)點(diǎn):編譯優(yōu)化會(huì)使調(diào)用堆棧中指向第二段的調(diào)用點(diǎn)(call site)可能并不是真正導(dǎo)致崩潰的調(diào)用。
此時(shí)突然想到了我們的崩潰軌跡,有著大量的 KVO痕跡
1、1秒前 DiscoveryViewController viewDidAppear:(,)
2、1秒前 HomeViewController viewDidAppear:(,)
3、2秒前 NSKVONotifying_UICollectionView handlePan:(UIScrollViewPanGestureRecognizer,)
4、2秒前 NSKVONotifying_UICollectionView handlePan:(UIScrollViewPanGestureRecognizer,)
是否和 KVO 有關(guān)???然而此處是沒有用 KVO的啊,而且用了地方都是處理過的,此時(shí)我們只能先再了解下KVO一個(gè)點(diǎn):
-
NSKVONotifying_UICollectionView 的由來
當(dāng)某個(gè)類的實(shí)例對(duì)象的key第一次被觀察時(shí),系統(tǒng)就會(huì)在運(yùn)行期動(dòng)態(tài)地創(chuàng)建該類的一個(gè)派生類NSKVONotifying_類名,在這個(gè)派生類中重寫該類中被觀察的屬性的 setter 方法。
@property(nonatomic, readonly) UIPanGestureRecognizer *panGestureRecognizer NS_AVAILABLE_IOS(5_0);
@property(nonatomic) CGPoint contentOffset;
在添加KVO觀察后,我們?cè)?ObserveValueForKeyPath 打上斷點(diǎn),看一下Object 。

此時(shí)isa指針被系統(tǒng)動(dòng)態(tài)的指向了派生類NSKVONotifying_UICollectionView
注意:KVO的本質(zhì)就是監(jiān)聽對(duì)象的屬性進(jìn)行賦值的時(shí)候有沒有調(diào)用
setter方法。
本頁面沒有用到,那只能再次猜測:是不是其他頁面(有用到空頁面,瀑布流)返回過來的,并且用來了KVO 而沒提前釋放 —— 一般是沒有的,臨走前就算沒有釋放,也不會(huì)調(diào)用dzn_canDisplay 方法的。
所以,此處穩(wěn)妥一點(diǎn)讓Bug重現(xiàn)的方法就是 找到一個(gè)操作,會(huì)涉及方法
- (BOOL)dzn_canDisplay,
- (CGSize)collectionViewContentSize
而且又歷經(jīng) HomeViewController,DiscoveryViewController,暫時(shí)符合該系列行為的就是:
- 啟動(dòng) app 的操作。
KVO 那塊可以理解是 contentOffset 的改變。接下來是重點(diǎn)測試這塊啦,但是一直還是無法重現(xiàn)該崩潰。
四、偽解決它
測試了一整天,就沒有崩潰一次,從來沒有想到過有一天居然想讓自己的項(xiàng)目崩掉。。。
回顧一下,我們之前版本 和 這一版本在啟動(dòng)中做的改變,然后我更懵啦,最后覺的一種可能是 這邊 self.viewModel 被提前釋放掉了,但我這是強(qiáng)引用??!。。。(項(xiàng)目中用的 是MVVM)
暫時(shí)的做法: 增加更多的防空處理。。。
??!同時(shí)我們這個(gè)項(xiàng)目被暫停下來啦,暫時(shí)都不會(huì)重新發(fā)版本啦,更不知道去如何解決它啦。。。
PS更新:再次看聽云,這幾天這個(gè) Bug 居然不重現(xiàn)了,讓我更懵啦,只是出現(xiàn)一個(gè)類似這個(gè)bug的,就是具體崩潰軌跡有點(diǎn)不同,真的懵了......
PS: 最有可能的原因
[self.collectionView performBatchUpdates:^{
[self.collectionView insertItemsAtIndexPaths:indexPaths];
} completion:NULL];
根據(jù)崩潰信息,后來一朋友立馬想到是這個(gè)問題,就是UICollectionView插入 insertItemsAtIndexPaths的時(shí)候必須用一個(gè)方法,用到這個(gè) performBatchUpdates 的方法。

- (void)performBatchUpdates:(void (^ __nullable)(void))updates completion:(void (^ __nullable)(BOOL finished))completion; // allows multiple insert/delete/reload/move calls to be animated simultaneously. Nestable.
allows multiple insert/delete/reload/move calls to be animated simultaneously. Nestable.
畢竟這個(gè)是蘋果推薦的,之前沒有用,還是不對(duì)的。雖說無法證實(shí),無法重現(xiàn),但看后面那個(gè)注釋以及崩潰信息,感覺還是比較可靠的。
五、 總結(jié)
暫時(shí)這個(gè)問題沒有解決它,沒法重現(xiàn),但是還是先理一下這個(gè)過程的問題和收獲。
- 解決 bug 的思路歷程,需要再優(yōu)化;
- 進(jìn)一步了解 Carsh 文件,以及常見原因;
- 對(duì) KVO 的實(shí)現(xiàn),有了新的認(rèn)識(shí)。
同時(shí),如有朋友知道上述類似問題的解決方案,歡迎告之。
PS: 個(gè)人再次遇到這個(gè)BUG,有了些新理解。