先上Crash堆棧信息:
Application Specific Information:
*** Terminating app due to uncaught exception 'NSGenericException', reason: '*** Collection <__NSSetM: 0x16dab7a0> was mutated while being enumerated.'
Last Exception Backtrace:
0 CoreFoundation 0x30a94f7e __exceptionPreprocess + 126
1 libobjc.A.dylib 0x3b245cca objc_exception_throw + 34
2 CoreFoundation 0x30a94a7c __NSFastEnumerationMutationHandler + 124
3 UIKit 0x3381ed76 -[UICollectionView _unhighlightAllItems] + 126
4 UIKit 0x334cc26e -[UICollectionView touchesBegan:withEvent:] + 362
5 UIKit 0x332e662c -[UIWindow _sendTouchesForEvent:] + 316
6 UIKit 0x332e16c6 -[UIWindow sendEvent:] + 754
7 UIKit 0x332b68c8 -[UIApplication sendEvent:] + 192
8 UIKit 0x332b4f72 _UIApplicationHandleEventQueue + 7098
9 CoreFoundation 0x30a60206 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 10
10 CoreFoundation 0x30a5f6d6 __CFRunLoopDoSources0 + 202
11 CoreFoundation 0x30a5deca __CFRunLoopRun + 618
12 CoreFoundation 0x309c8eba CFRunLoopRunSpecific + 518
13 CoreFoundation 0x309c8c9e CFRunLoopRunInMode + 102
14 GraphicsServices 0x358ce65e GSEventRunModal + 134
15 UIKit 0x33315148 UIApplicationMain + 1132
16 MyAppName 0x00af441a main (main.m:14)
17 libdyld.dylib 0x3b752ab2 tlv_initializer + 2
1殉摔、原因如下:
iOS7系統(tǒng),滑動(dòng)CollectionView的同時(shí)reloadData冗澈。
2钦勘、解決方案:
(1)對(duì)于只有1個(gè)section的UICollectionView陋葡,使用reloadSections代替reloadData亚亲,代碼如下:
[UIView setAnimationsEnabled:animated];
[collectionView performBatchUpdates:^{
[collectionView reloadSections:[NSIndexSet indexSetWithIndex:0]];
} completion:^(BOOL finished) {
[UIView setAnimationsEnabled:YES];
}];
(2)對(duì)于有多個(gè)section的UICollectionView,則可以使用insertSections來替換reloadData,例如類似這樣的代碼:
self.totalSections++;
[self.mCollectionView performBatchUpdates:^{
[self.mCollectionView insertSections:[NSIndexSet indexSetWithIndex:self.totalSections-1]];
} completion:^(BOOL finished) {
...
}];
3捌归、如果讀者有興趣肛响,可以閱讀以下的分析過程:
顯然,該堆棧為全系統(tǒng)棧惜索,沒有捕獲到App的任何代碼特笋,極大的增加了排查問題的范圍和難度,但比較幸運(yùn)的是巾兆,堆棧信息給出了錯(cuò)誤原因:Collection <__NSSetM: 0x16dab7a0> was mutated while being enumerated猎物。復(fù)制粘貼,谷歌一下角塑,大部分網(wǎng)友給出的原因是:在遍歷集合的同時(shí)蔫磨,對(duì)集合中的元素做了增刪改操作。類似于如下代碼:
for (NSString *obj in self.testArray) {
...
[self.testArray addObject:@"4"];
...
}
但是圃伶,如果項(xiàng)目中真有這樣的代碼堤如,在開發(fā)調(diào)試的時(shí)候就應(yīng)該很容易暴露問題,不至于到線上才出現(xiàn)窒朋。那有沒有可能是多線程操作導(dǎo)致的呢搀罢?類似于如下代碼:
// 主線程
for (NSString *obj in self.testArray) {
...
}
// 子線程
[self.testArray addObject:@"4"];
順著這個(gè)思路,開始查找代碼中是否有多線程使用NSMutableSet的地方侥猩,經(jīng)過一番辛苦的代碼走查榔至,并沒有發(fā)現(xiàn)這樣的邏輯代碼,于是有點(diǎn)懷疑這個(gè)<__NSSetM: 0x16dab7a0>并不是App代碼創(chuàng)建的對(duì)象欺劳,而是系統(tǒng)框架內(nèi)部創(chuàng)建的洛退,那具體是哪個(gè)類呢?目前看來杰标,只能從全系統(tǒng)棧中尋找答案了兵怯。
從上往下逐行分析,發(fā)現(xiàn)與UICollectionView相關(guān)腔剂,谷歌一下:[UICollectionView _unhighlightAllItems]媒区,結(jié)果顯示,遇到此問題的人不止我一個(gè)掸犬,在Stack Overflow有高人給出了Crash復(fù)現(xiàn)路徑袜漩、原因猜測(cè)分析、以及問題解決方案:
(1)復(fù)現(xiàn)路徑:calling reloadData while the user is dragging the collection view.
(2)猜測(cè)分析:My guess is that there is a weak reference to the highlighted cell's indexPath somewhere inside of the collectionView, and that calling reload will dealloc that indexPath. When the collectionView then tries to unhighlight the cell, it crashes.
(3)解決方案:使用reloadSections代替reloadData湾碎。
為了驗(yàn)證這3點(diǎn)宙攻,尤其是(1)和(3),以確保問題從根本上得到解決介褥,我做了一個(gè)demo座掘,關(guān)鍵代碼如下:
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
self.totalItems = 5;
[NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(testForCrash) userInfo:nil repeats:YES];
}
- (void)testForCrash {
self.totalItems++;
[self.cv_test reloadData];
}
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
return self.totalItems;
}
開一個(gè)定時(shí)器递惋,間隔1秒鐘增加1個(gè)item,并且reloadData溢陪。這這個(gè)過程中萍虽,頻繁滑動(dòng)CollectionView,用不了很久形真,就會(huì)出現(xiàn)Crash:[NSIndexPath section] message sent to deallocated instance XXX(前提是開啟了Zombie Objects調(diào)試選項(xiàng))杉编。很顯然,訪問了一個(gè)已釋放的對(duì)象咆霜,并且從跟蹤的異常堆棧來看邓馒,是在遍歷NSMutableOrderedSet時(shí)發(fā)生了Crash,再結(jié)合Collection <__NSSetM: XXX> was mutated while being enumerated原因蛾坯,可以確定這個(gè)<__NSSetM: XXX>是UICollectionView內(nèi)部的一個(gè)NSMutableOrderedSet對(duì)象绒净。
注意:要復(fù)現(xiàn)這個(gè)Crash,必須在iOS7設(shè)備上運(yùn)行偿衰。據(jù)我不完全統(tǒng)計(jì)挂疆,線上的這個(gè)問題都只出現(xiàn)在iOS7系統(tǒng)上,其他系統(tǒng)(8下翎,9缤言,10)并沒有發(fā)現(xiàn)這個(gè)問題,并且我在iOS10的設(shè)備上運(yùn)行這個(gè)Demo视事,相同的操作并沒有復(fù)現(xiàn)這個(gè)Crash胆萧。有興趣的同學(xué)可以在8和9的系統(tǒng)上測(cè)試驗(yàn)證,由此可以猜測(cè)這個(gè)屬于iOS7系統(tǒng)特有的問題俐东。
由于iOS并沒有開放UICollectionView的源碼跌穗,對(duì)于第2點(diǎn)在此不做任何分析,以免誤導(dǎo)讀者虏辫,其實(shí)我也不懂蚌吸。
關(guān)于第3點(diǎn)提到的解決方案,親測(cè)可行砌庄。至于穩(wěn)定性如何羹唠,還得時(shí)間來驗(yàn)證。但這里想補(bǔ)充一點(diǎn)的是娄昆,如果這個(gè)UICollectionView有多個(gè)section佩微,則可以使用insertSections來替換reloadData,例如類似這樣的代碼:
self.totalSections++;
[self.mCollectionView performBatchUpdates:^{
[self.mCollectionView insertSections:[NSIndexSet indexSetWithIndex:self.totalSections-1]];
} completion:^(BOOL finished) {
...
}];
再次說明:這個(gè)問題只有在iOS7的系統(tǒng)上才出現(xiàn)萌焰,也就是說哺眯,對(duì)于7之后的系統(tǒng)還是可以放心使用reloadData。
全文完扒俯。