一次遍歷導(dǎo)致的崩潰
作者丨搜狐新聞-Augus
來源丨搜狐技術(shù)產(chǎn)品(ID:sohu-tech)
?
本文字?jǐn)?shù):10920字
預(yù)計(jì)閱讀時(shí)間:28分鐘
一次遍歷導(dǎo)致的崩潰
題記:用最通俗的語言,描述最難懂的技術(shù)
?本文是作者在對項(xiàng)目進(jìn)行調(diào)試某靜態(tài)庫的功能進(jìn)行單元測試發(fā)現(xiàn)的問題的記錄,如果有哪些論述模糊或者不準(zhǔn)確,請聯(lián)系
[email protected]
目錄表
故事背景 問題定位 解決方案 原理 copy是什么 copy如何實(shí)現(xiàn) copy底層實(shí)現(xiàn) 延展之深淺拷貝 集合類對象 非集合類對象 參考文檔 結(jié)束語
故事背景
環(huán)境及場景:
編譯環(huán)境Xcode 12.5.1
2021年8月的某一天,Augus正在調(diào)試項(xiàng)目需求A,因?yàn)锳要求需要接入一個(gè)SDK進(jìn)行實(shí)現(xiàn)某些采集功能
操作流程
在程序啟動的最開始地方,初始化
SDK,并分配內(nèi)存空間在某次的啟動中就出現(xiàn)了以下錯(cuò)誤
Trapped uncaught exception 'NSGenericException', reason: '*** Collection <__NSSetM: 0x2829f9740> was mutated while being enumerated.'
初步猜測
開始的時(shí)候,我先排除自己代碼的原因(畢竟代碼自己寫的,還是求穩(wěn)一些),因?yàn)檎{(diào)試模式下沒有開全局?jǐn)帱c(diǎn),所以本次的崩潰就這么被錯(cuò)失機(jī)會定位
為了下一次的復(fù)現(xiàn)
首先進(jìn)行了 NSMutableSet某些方法的hook開啟全局?jǐn)帱c(diǎn)
最后定位
項(xiàng)目中引入SDK導(dǎo)致的崩潰
問題定位
問題原因
被引入第三方的SDK在某個(gè)邏輯中使用的NSMutableSet遍歷中對原可變集合進(jìn)行同時(shí)讀寫的操作
復(fù)現(xiàn)同樣崩潰的場景,Let's do it
?NSMutableSet?*mutableSet?=?[NSMutableSet?setWithObjects:@"1",@"2",@"3",?nil];
????
?for?(NSString?*item?in?mutableSet)?{
?????if?([item?integerValue]?3)?{
?????????[mutableSet?removeObject:item];
??????}
??}
控制臺日志
很好,現(xiàn)在已經(jīng)知道了問題的原因,那么接下來解決問題就很容易了,讓我們繼續(xù)
解決方案
問題原因總結(jié)
不能在一個(gè)可變集合,包括NSMutableArray,NSMutableDictionary等類似對象遍歷的同時(shí)又對該對象進(jìn)行添加或者移除操作
解決問題
把遍歷中的對象進(jìn)行一次copy操作

其實(shí)其中的道理很簡單,我現(xiàn)在簡而概括
你在內(nèi)存中已經(jīng)初始化一塊區(qū)域,而且分配了地址,那么系統(tǒng)在這次的遍歷中會把這次遍歷包裝成原子操作,因?yàn)闀赡軙L問壞內(nèi)存或者越界的問題,當(dāng)然這也是出于安全原因,不同的系統(tǒng)下的實(shí)現(xiàn)方式不同,但是底層的原理是一致的,都是為了保護(hù)對象在操作過程中不受可變因素的更新
那問題來了
copy是什么?copy在底層如何實(shí)現(xiàn)?copy有哪些需要注意的?
?帶著這些疑問,我們繼續(xù)下面的閱讀,相信你讀完肯定會柳暗花明又一村...
原理
copy是什么
copy是Objective-C編程語言下的屬性修飾關(guān)鍵詞,比如修飾Block orNS*開頭的對象
copy如何實(shí)現(xiàn)
對需要實(shí)現(xiàn)的類遵守NSCopying協(xié)議
實(shí)現(xiàn)NSCopying協(xié)議,該協(xié)議只有一個(gè)方法
-?(id)copyWithZone:(NSZone?*)zone;
舉例說明,首先我們新建一個(gè)Perosn類進(jìn)行說明,下面是示例代碼
//?The?person.h?file
#import?
NS_ASSUME_NONNULL_BEGIN
@interface?Person?:?NSObject<NSCopying>
-?(instancetype)initWithName:(NSString?*)name;
@property(nonatomic,?copy)?NSString?*name;
///?To?update?internal?mutabl?set?for?adding?a?person
///?@param?person?A?instance?of?person
-?(void)addPerson:(Person?*)person;
///?To?update?internal?mutbable?set?for?removing?a?person
///?@param?person?A?instance?of?person
-?(void)removePerson:(Person?*)person;
@end
NS_ASSUME_NONNULL_END
??
//?The?person.m?file
#import?"Person.h"
@interface?Person?()
@property(nonatomic,?strong)?NSMutableSet?*friends;
@end
@implementation?Person
#pragma?mark?-?Initalizaiton?Methods
-?(instancetype)initWithName:(NSString?*)name?{
????self?=?[super?init];
????if?(!self)?{
????????return?nil;
????}
???if(!name?||?name.length?1)?{
???????name?=?@"Augus";
????}
????_name?=?name;
????
????//?Warn:?Do?not?self.persons?way?to?init.?But?do?u?know?reason?
????_friends?=?[NSMutableSet?set];
????return?self;
}
#pragma?mark?-?Private?Methods
-?(void)addPerson:(Person?*)person?{
????
????//?Check?param?safe
????if?(!person)?{
????????return;
????}
????
????[self.friends?addObject:person];
????
????
}
-?(void)removePerson:(Person?*)person?{
????
????if?(!person)?{
????????return;
????}
????
????[self.friends?removeObject:person];
}
#pragma?mark?-?Copy?Methods
-?(id)copyWithZone:(NSZone?*)zone?{
????
????//?need?copy?object
????Person?*copy?=?[[Person?allocWithZone:zone]?initWithName:_name];
????
????return?copy;
}
-?(id)deepCopy?{
????Person?*copy?=?[[[self?class]?alloc]?initWithName:_name];
????copy->_persons?=?[[NSMutableSet?alloc]?initWithSet:_friends?copyItems:YES];
????return?copy;
}
#pragma?mark?-?Lazy?Load
-?(NSMutableSet?*)friends?{
????if?(!_friends)?{
????????_friends?=?[NSMutableSet?set];
????}
????return?_friends;
}
@end
類的功能很簡單,初始化的時(shí)候需要外層傳入name進(jìn)行初始化,如果name非法則進(jìn)行默認(rèn)值的處理
類內(nèi)部維護(hù)了一個(gè)可變集合用來存放好友 外部提供了新增和移除的兩個(gè)方法 - (id)copyWithZone:(NSZone *)zone;中的實(shí)現(xiàn)就是簡單的一個(gè)copy功能而 deepCopy是對可變集合的深層復(fù)制,至于原因,我們會在延展中舉例說明,這里先擱置
copy底層實(shí)現(xiàn)
之前的文檔中說過,想要看底層的實(shí)現(xiàn)那就用clang -rewrite-objc main.m看源碼
為了方便測試和查看,我們新建一個(gè)TestCopy的類繼承NSObject,然后在TestCopy.m中只加如下代碼
#import?"TestCopy.h"
@interface?TestCopy?()
@property(nonatomic,?copy)?NSString?*augusCopy;
@end
@implementation?TestCopy
@end
然后在終端執(zhí)行$ clang -rewrite-objc TestCopy.m命令
接下來我們進(jìn)行源碼分析
//?augusCopy's?getter?function
static?NSString?*?_I_TestCopy_augusCopy(TestCopy?*?self,?SEL?_cmd)?{?return?(*(NSString?**)((char?*)self?+?OBJC_IVAR_$_TestCopy$_augusCopy));?}
//?augusCopy's?setter?function
static?void?_I_TestCopy_setAugusCopy_(TestCopy?*?self,?SEL?_cmd,?NSString?*augusCopy)?{?objc_setProperty?(self,?_cmd,?__OFFSETOFIVAR__(struct?TestCopy,?_augusCopy),?(id)augusCopy,?0,?1);?}
總結(jié):copy的getter是根據(jù)地址偏移找到對應(yīng)的實(shí)例變量進(jìn)行返回,那么objc_setProperty又是怎么實(shí)現(xiàn)的呢?
objc_setProperty在.cpp中沒有找到,在[Apple源碼](鏈接附文后)中找到了答案,我們來看下
//?self:?The?current?instance
//?_cmd:?The?setter's?function?name
//?offset:?The?offset?for?self?that?find?the?instance?property
//?newValue:?The?new?value?that?outer?input
//?atomic:?Whether?atomic?or?nonatomic,it?is?nonatomic?here
//?shouldCopy:?Whether?should?copy?or?not
void?
objc_setProperty(id?self,?SEL?_cmd,?ptrdiff_t?offset,?id?newValue,?
?????????????????BOOL?atomic,?signed?char?shouldCopy)?
{
????objc_setProperty_non_gc(self,?_cmd,?offset,?newValue,?atomic,?shouldCopy);
}
void?objc_setProperty_non_gc(id?self,?SEL?_cmd,?ptrdiff_t?offset,?id?newValue,?BOOL?atomic,?signed?char?shouldCopy)?
{
????bool?copy?=?(shouldCopy?&&?shouldCopy?!=?MUTABLE_COPY);
????bool?mutableCopy?=?(shouldCopy?==?MUTABLE_COPY);
????reallySetProperty(self,?_cmd,?newValue,?offset,?atomic,?copy,?mutableCopy);
}
看到內(nèi)部又調(diào)用了objc_setProperty_non_gc方法,這里主要看下這個(gè)方法內(nèi)部的實(shí)現(xiàn),前五個(gè)參數(shù)和開始的傳入一致,最后的兩個(gè)參數(shù)是由shouldCopy決定,shouldCopy在這里是0 or 1,我們現(xiàn)考慮當(dāng)前的情況,
如果 shouldCopy=0,那么copy=NO,mutableCopy=NO如果 shouldCopy=1,那么copy=YES,mutableCopy=NO
下面繼續(xù)reallySetProperty的實(shí)現(xiàn)
static?inline?void?reallySetProperty(id?self,?SEL?_cmd,?id?newValue,?ptrdiff_t?offset,?bool?atomic,?bool?copy,?bool?mutableCopy)
{
????id?oldValue;
????id?*slot?=?(id*)?((char*)self?+?offset);
????if?(copy)?{
????????newValue?=?[newValue?copyWithZone:NULL];
????}?else?if?(mutableCopy)?{
????????newValue?=?[newValue?mutableCopyWithZone:NULL];
????}?else?{
????????if?(*slot?==?newValue)?return;
????????newValue?=?objc_retain(newValue);
????}
????if?(!atomic)?{
????????oldValue?=?*slot;
????????*slot?=?newValue;
????}?else?{
????????spin_lock_t?*slotlock?=?&PropertyLocks[GOODHASH(slot)];
????????_spin_lock(slotlock);
????????oldValue?=?*slot;
????????*slot?=?newValue;????????
????????_spin_unlock(slotlock);
????}
????objc_release(oldValue);
}
基于本例子中的情況,copy=YES,最后還是調(diào)用了newValue = [newValue copyWithZone:NULL];,如果copy=NO and mutableCopy=NO,那么最后會調(diào)用newValue = objc_retain(newValue);
objc_retain的實(shí)現(xiàn)
id?objc_retain(id?obj)?{?return?[obj?retain];?}
總結(jié):用copy修飾的屬性,賦值的時(shí)候,不管本身是可變與不可變,賦值給屬性之后的都是不可變的
延展之深淺拷貝
非集合類對象
在iOS下我們經(jīng)常聽到深拷貝(內(nèi)容拷貝)或者淺拷貝(指針拷貝),對于這些操作,我們將針對集合類對象和非集合類對象進(jìn)行copy和 mutableCopy實(shí)驗(yàn)
類簇:Class Clusters
an architecture that groups a number of private, concrete subclasses under a public, abstract superclass. (一個(gè)在共有的抽象超類下設(shè)置一組私有子類的架構(gòu))
Class cluster是 Apple 對抽象工廠設(shè)計(jì)模式的稱呼。使用抽象類初始化返回一個(gè)具體的子類的模式的好處就是讓調(diào)用者只需要知道抽象類開放出來的API的作用,而不需要知道子類的背后復(fù)雜的邏輯。驗(yàn)證結(jié)論過程的類簇對應(yīng)關(guān)系請看這篇 [Class Clusters 文檔](鏈接附文后)。
NSString
NSString?*str?=?@"augusStr";
NSString?*copyAugus?=?[str?copy];
NSString?*mutableCopyAugus?=?[str?mutableCopy];
????
NSLog(@"str:(%@<%p>:?%p):?%@",[str?class],&str,str,str);
NSLog(@"copyAugus?str:(%@<%p>:?%p):?%@",[copyAugus?class],©Augus,copyAugus,copyAugus);
NSLog(@"mutableCopyAugus?str:(%@<%p>:?%p):?%@",[mutableCopyAugus?class],&mutableCopyAugus,mutableCopyAugus,mutableCopyAugus);
//?控制臺輸出
2021-09-03?14:51:49.263571+0800?TestBlock[4573:178396]?augus?str(__NSCFConstantString<0x7ffee30a1008>:?0x10cb63198):?augusStr
2021-09-03?14:51:49.263697+0800?TestBlock[4573:178396]?copyAugus?str(__NSCFConstantString<0x7ffee30a1000>:?0x10cb63198):?augusStr
2021-09-03?14:51:49.263808+0800?TestBlock[4573:178396]?mutableCopyAugus?str(__NSCFString<0x7ffee30a0ff8>:?0x6000036bcfc0):?augusStr
?
__NSCFConstantString是字符串常量類,可看作NSString,__NSCFString是字符串類,可看作NSMutableString
結(jié)論:str和copyAugus打印出來的內(nèi)存地址是一樣的,都是0x10cb63198且類名相同都是__NSCFConstantString,表明都是淺拷貝,都是NSString;變量mutableCopyAugus打印出來的內(nèi)存地址和類名都不一致,所以是生成了新的對象
| 類名 | 操作 | 新對象 | 拷貝類型 | 元素拷貝 | 新類名 |
|---|---|---|---|---|---|
| NSString | copy | NO | 淺拷貝 | NO | NSString |
| mutableCopy | YES | 深拷貝 | NO | NSMutableString |
NSMutableString
NSMutableString?*str?=?[NSMutableString?stringWithString:@"augusMutableStr"];
NSMutableString?*copyStr?=?[str?copy];
NSMutableString?*mutableCopyStr?=?[str?mutableCopy];
NSLog(@"str:(%@<%p>:?%p):?%@",[str?class],&str,str,str);
NSLog(@"copyStr:?(%@<%p>:?%p):?%@",[copyStr?class],©Str,copyStr,copyStr);
NSLog(@"mutableCopyStr:?(%@<%p>:?%p):?%@",[mutableCopyStr?class],&mutableCopyStr,mutableCopyStr,mutableCopyStr);
//?控制臺輸出
2021-09-03?15:31:56.105642+0800?TestBlock[4778:198224]?str:(__NSCFString<0x7ffeeaa34008>:?0x600001a85fe0):?augusMutableStr
2021-09-03?15:31:56.105804+0800?TestBlock[4778:198224]?copyStr:?(__NSCFString<0x7ffeeaa34000>:?0x600001a86400):?augusMutableStr
2021-09-03?15:31:56.105901+0800?TestBlock[4778:198224]?mutableCopyStr:?(__NSCFString<0x7ffeeaa33ff8>:?0x600001a86070):?augusMutableStr
結(jié)論:str和copyStr和mutableCopyStr打印出來的內(nèi)存地址都不一樣的,但是生成的類簇都是__NSCFString,也就是NSMutableString
| 類名 | 操作 | 新對象 | 拷貝類型 | 元素拷貝 | 新類名 |
|---|---|---|---|---|---|
| NSMutableString | copy | YES | 深拷貝 | NO | NSMutableString |
| mutableCopy | YES | 深拷貝 | NO | NSMutableString |
集合類對象
?因?yàn)楸疚膶?code style="font-size: 14px;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;background-color: rgba(27, 31, 35, 0.05);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;color: rgb(244, 138, 0);">NSMutableSet展開討論,所以只對該類進(jìn)行測試,其余的
NSArray&NSMutableArray和NSDictionary&NSMutableDictionary本質(zhì)是一樣的,請小伙伴自行參考測試就行
NSSet
Person?*p1?=?[[Person?alloc]?init];
Person?*p2?=?[[Person?alloc]?init];
Person?*p3?=?[[Person?alloc]?init];
NSSet?*set?=?[[NSSet?alloc]?initWithArray:@[p1,p2,p3]];
NSSet?*copySet?=?[set?copy];
NSSet?*mutableCopySet?=?[set?mutableCopy];
NSLog(@"set:(%@<%p>:?%p):?%@",[set?class],&set,set,set);
NSLog(@"copySet:?(%@<%p>:?%p):?%@",[copySet?class],©Set,copySet,copySet);
NSLog(@"mutableCopySet:?(%@<%p>:?%p):?%@",[mutableCopySet?class],&mutableCopySet,mutableCopySet,mutableCopySet);
????
//?控制臺輸出
2021-09-03?16:11:36.590338+0800?TestBlock[4938:219837]?set:(__NSSetI<0x7ffeef3f7fd0>:?0x6000007322b0):?{(
????0x600000931e00>,
????0x600000931e20>,
????0x600000932000>
)}
2021-09-03?16:11:36.590479+0800?TestBlock[4938:219837]?copySet:?(__NSSetI<0x7ffeef3f7fc8>:?0x6000007322b0):?{(
????0x600000931e00>,
????0x600000931e20>,
????0x600000932000>
)}
2021-09-03?16:11:36.590614+0800?TestBlock[4938:219837]?mutableCopySet:?(__NSSetM<0x7ffeef3f7fc0>:?0x600000931fa0):?{(
????0x600000931e00>,
????0x600000932000>,
????0x600000931e20>
)}
?
__NSSetI是不可變?nèi)ブ責(zé)o序集合的子類,即NSSet,__NSSetM是可變?nèi)ブ責(zé)o序集合的子類,即NSMutableSet
結(jié)論:set和copySet打印出來的內(nèi)存地址是一致的0x6000007322b0,類簇都是__NSSetI說明是淺拷貝,沒有生成新對象,也都屬于類 NSSet;mutableCopySet的內(nèi)存地址和類簇都不同,所以是深拷貝,生成了新的對象,屬于類NSMutablSet;集合里面的元素地址都是一樣的
| 類名 | 操作 | 新對象 | 拷貝類型 | 元素拷貝 | 新類名 |
|---|---|---|---|---|---|
| NSSet | copy | NO | 淺拷貝 | NO | NSSet |
| mutableCopy | YES | 深拷貝 | NO | NSMutablSet |
NSMutableSet
NSMutableSet?*set?=?[[NSMutableSet?alloc]?initWithArray:@[p1,p2,p3]];
NSMutableSet?*copySet?=?[set?copy];
NSMutableSet?*mutableCopySet?=?[set?mutableCopy];
NSLog(@"set:(%@<%p>:?%p):?%@",[set?class],&set,set,set);
NSLog(@"copySet:?(%@<%p>:?%p):?%@",[copySet?class],©Set,copySet,copySet);
NSLog(@"mutableCopySet:?(%@<%p>:?%p):?%@",[mutableCopySet?class],&mutableCopySet,mutableCopySet,mutableCopySet);
?
?//?控制臺輸出
2021-09-03?16:33:35.573557+0800?TestBlock[5043:232294]?set:(__NSSetM<0x7ffeefb78fd0>:?0x600002b99640):?{(
????0x600002b99620>,
????0x600002b99600>,
????0x600002b995e0>
)}
2021-09-03?16:33:35.573686+0800?TestBlock[5043:232294]?copySet:?(__NSSetI<0x7ffeefb78fc8>:?0x6000025e54a0):?{(
????0x600002b99620>,
????0x600002b99600>,
????0x600002b995e0>
)}
2021-09-03?16:33:35.573778+0800?TestBlock[5043:232294]?mutableCopySet:?(__NSSetM<0x7ffeefb78fc0>:?0x600002b99680):?{(
????0x600002b99620>,
????0x600002b99600>,
????0x600002b995e0>
)}
結(jié)論:set和copySet和mutableCopySet的內(nèi)存地址都不一樣,說明操作都是深拷貝;集合里面的元素地址都是一樣的
| 類名 | 操作 | 新對象 | 拷貝類型 | 元素拷貝 | 新類名 |
|---|---|---|---|---|---|
| NSMutableSet | copy | YES | 深拷貝 | NO | NSSet |
| mutableCopy | YES | 深拷貝 | NO | NSMutablSet |
結(jié)論分析
NSMutable*開頭的類不要用copy屬性去修飾,因?yàn)槊看钨x值操作拷貝出來的都是不可變集合類集合類的 copy和mutableCopy操作,對象里面的元素不會發(fā)生拷貝,只會對容器層面拷貝,也稱之為單層深拷貝
參考文檔
文檔0:https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Collections/Articles/Copying.html#//apple_ref/doc/uid/TP40010162-SW8 文檔1:https://gist.github.com/Catfish-Man/bc4a9987d4d7219043afdf8ee536beb2 文檔2:https://opensource.apple.com/source/objc4/objc4-723/runtime/objc-accessors.mm.auto.html Apple源碼:https://opensource.apple.com/source/objc4/objc4-723/runtime/objc-accessors.mm.auto.html Class Clusters 文檔:https://gist.github.com/Catfish-Man/bc4a9987d4d7219043afdf8ee536beb2
結(jié)束語
一次崩潰定位,一次源碼之旅,一系列拷貝操作,基本可以把文中提到的問題說清楚;遇到問題不要怕刨根問底,因?yàn)閱柕椎谋M頭就是無盡的光明
-End-
最近有一些小伙伴,讓我?guī)兔φ乙恍?面試題?資料,于是我翻遍了收藏的 5T 資料后,匯總整理出來,可以說是程序員面試必備!所有資料都整理到網(wǎng)盤了,歡迎下載!

面試題】即可獲取