
本文字數:10920字
預計閱讀時間:28分鐘
題記:用最通俗的語言,描述最難懂的技術
❝本文是作者在對項目進行調試某靜態庫的功能進行單元測試發現的問題的記錄,如果有哪些論述模糊或者不準確,請聯繫weiniu@sohu-inc.com
目錄表環境及場景:
編譯環境Xcode 12.5.1
2021年8月的某一天,Augus正在調試項目需求A,因為A要求需要接入一個SDK進行實現某些採集功能
操作流程
在程序啟動的最開始地方,初始化SDK,並分配內存空間
在某次的啟動中就出現了以下錯誤
Trapped uncaught exception 'NSGenericException', reason: '*** Collection <__NSSetM: 0x2829f9740> was mutated while being enumerated.'
初步猜測
開始的時候,我先排除自己代碼的原因(畢竟代碼自己寫的,還是求穩一些),因為調試模式下沒有開全局斷點,所以本次的崩潰就這麼被錯失機會定位
為了下一次的復現
最後定位
項目中引入SDK導致的崩潰
問題定位問題原因
被引入第三方的SDK在某個邏輯中使用的NSMutableSet遍歷中對原可變集合進行同時讀寫的操作
復現同樣崩潰的場景,Let's do it
NSMutableSet*mutableSet=[NSMutableSetsetWithObjects:@"1",@"2",@"3",nil];for(NSString*iteminmutableSet){if([itemintegerValue]<3){[mutableSetremoveObject:item];}}控制台日誌
很好,現在已經知道了問題的原因,那麼接下來解決問題就很容易了,讓我們繼續
解決方案問題原因總結
不能在一個可變集合,包括NSMutableArray,NSMutableDictionary等類似對象遍歷的同時又對該對象進行添加或者移除操作
解決問題
把遍歷中的對象進行一次copy操作
其實其中的道理很簡單,我現在簡而概括
你在內存中已經初始化一塊區域,而且分配了地址,那麼系統在這次的遍歷中會把這次遍歷包裝成原子操作,因為會可能會訪問壞內存或者越界的問題,當然這也是出於安全原因,不同的系統下的實現方式不同,但是底層的原理是一致的,都是為了保護對象在操作過程中不受可變因素的更新
那問題來了
帶着這些疑問,我們繼續下面的閱讀,相信你讀完肯定會柳暗花明又一村...
原理copy是什麼copy是Objective-C編程語言下的屬性修飾關鍵詞,比如修飾Block orNS*開頭的對象
copy如何實現對需要實現的類遵守NSCopying協議
實現NSCopying協議,該協議只有一個方法
-(id)copyWithZone:(NSZone*)zone;舉例說明,首先我們新建一個Perosn類進行說明,下面是示例代碼
//Theperson.hfile#import<Foundation/Foundation.h>NS_ASSUME_NONNULL_BEGIN@interfacePerson:NSObject<NSCopying>-(instancetype)initWithName:(NSString*)name;@property(nonatomic,copy)NSString*name;///Toupdateinternalmutablsetforaddingaperson///@parampersonAinstanceofperson-(void)addPerson:(Person*)person;///Toupdateinternalmutbablesetforremovingaperson///@parampersonAinstanceofperson-(void)removePerson:(Person*)person;@endNS_ASSUME_NONNULL_END//Theperson.mfile#import"Person.h"@interfacePerson()@property(nonatomic,strong)NSMutableSet<Person*>*friends;@end@implementationPerson#pragmamark-InitalizaitonMethods-(instancetype)initWithName:(NSString*)name{self=[superinit];if(!self){returnnil;}if(!name||name.length<1){name=@"Augus";}_name=name;//Warn:Donotself.personswaytoinit.Butdouknowreason?_friends=[NSMutableSetset];returnself;}#pragmamark-PrivateMethods-(void)addPerson:(Person*)person{//Checkparamsafeif(!person){return;}[self.friendsaddObject:person];}-(void)removePerson:(Person*)person{if(!person){return;}[self.friendsremoveObject:person];}#pragmamark-CopyMethods-(id)copyWithZone:(NSZone*)zone{//needcopyobjectPerson*copy=[[PersonallocWithZone:zone]initWithName:_name];returncopy;}-(id)deepCopy{Person*copy=[[[selfclass]alloc]initWithName:_name];copy->_persons=[[NSMutableSetalloc]initWithSet:_friendscopyItems:YES];returncopy;}#pragmamark-LazyLoad-(NSMutableSet*)friends{if(!_friends){_friends=[NSMutableSetset];}return_friends;}@end類的功能很簡單,初始化的時候需要外層傳入name進行初始化,如果name非法則進行默認值的處理
之前的文檔中說過,想要看底層的實現那就用clang -rewrite-objc main.m看源碼
為了方便測試和查看,我們新建一個TestCopy的類繼承NSObject,然後在TestCopy.m中只加如下代碼
#import"TestCopy.h"@interfaceTestCopy()@property(nonatomic,copy)NSString*augusCopy;@end@implementationTestCopy@end然後在終端執行$ clang -rewrite-objc TestCopy.m命令
接下來我們進行源碼分析
//augusCopy'sgetterfunctionstaticNSString*_I_TestCopy_augusCopy(TestCopy*self,SEL_cmd){return(*(NSString**)((char*)self+OBJC_IVAR_$_TestCopy$_augusCopy));}//augusCopy'ssetterfunctionstaticvoid_I_TestCopy_setAugusCopy_(TestCopy*self,SEL_cmd,NSString*augusCopy){objc_setProperty(self,_cmd,__OFFSETOFIVAR__(structTestCopy,_augusCopy),(id)augusCopy,0,1);}總結:copy的getter是根據地址偏移找到對應的實例變量進行返回,那麼objc_setProperty又是怎麼實現的呢?
objc_setProperty在.cpp中沒有找到,在[Apple源碼](鏈接附文後)中找到了答案,我們來看下
//self:Thecurrentinstance//_cmd:Thesetter'sfunctionname//offset:Theoffsetforselfthatfindtheinstanceproperty//newValue:Thenewvaluethatouterinput//atomic:Whetheratomicornonatomic,itisnonatomichere//shouldCopy:Whethershouldcopyornotvoidobjc_setProperty(idself,SEL_cmd,ptrdiff_toffset,idnewValue,BOOLatomic,signedcharshouldCopy){objc_setProperty_non_gc(self,_cmd,offset,newValue,atomic,shouldCopy);}voidobjc_setProperty_non_gc(idself,SEL_cmd,ptrdiff_toffset,idnewValue,BOOLatomic,signedcharshouldCopy){boolcopy=(shouldCopy&&shouldCopy!=MUTABLE_COPY);boolmutableCopy=(shouldCopy==MUTABLE_COPY);reallySetProperty(self,_cmd,newValue,offset,atomic,copy,mutableCopy);}看到內部又調用了objc_setProperty_non_gc方法,這裡主要看下這個方法內部的實現,前五個參數和開始的傳入一致,最後的兩個參數是由shouldCopy決定,shouldCopy在這裡是0 or 1,我們現考慮當前的情況,
下面繼續reallySetProperty的實現
staticinlinevoidreallySetProperty(idself,SEL_cmd,idnewValue,ptrdiff_toffset,boolatomic,boolcopy,boolmutableCopy){idoldValue;id*slot=(id*)((char*)self+offset);if(copy){newValue=[newValuecopyWithZone:NULL];}elseif(mutableCopy){newValue=[newValuemutableCopyWithZone: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,最後還是調用了newValue = [newValue copyWithZone:NULL];,如果copy=NO and mutableCopy=NO,那麼最後會調用newValue = objc_retain(newValue);
objc_retain的實現
idobjc_retain(idobj){return[objretain];}總結:用copy修飾的屬性,賦值的時候,不管本身是可變與不可變,賦值給屬性之後的都是不可變的
延展之深淺拷貝非集合類對象在iOS下我們經常聽到深拷貝(內容拷貝)或者淺拷貝(指針拷貝),對於這些操作,我們將針對集合類對象和非集合類對象進行copy和 mutableCopy實驗
類簇:Class Clusters
an architecture that groups a number of private, concrete subclasses under a public, abstract superclass. (一個在共有的抽象超類下設置一組私有子類的架構)
Class cluster 是 Apple 對抽象工廠設計模式的稱呼。使用抽象類初始化返回一個具體的子類的模式的好處就是讓調用者只需要知道抽象類開放出來的API的作用,而不需要知道子類的背後複雜的邏輯。驗證結論過程的類簇對應關係請看這篇 [Class Clusters 文檔](鏈接附文後)。
NSString
NSString*str=@"augusStr";NSString*copyAugus=[strcopy];NSString*mutableCopyAugus=[strmutableCopy];NSLog(@"str:(%@<%p>:%p):%@",[strclass],&str,str,str);NSLog(@"copyAugusstr:(%@<%p>:%p):%@",[copyAugusclass],©Augus,copyAugus,copyAugus);NSLog(@"mutableCopyAugusstr:(%@<%p>:%p):%@",[mutableCopyAugusclass],&mutableCopyAugus,mutableCopyAugus,mutableCopyAugus);//控制台輸出2021-09-0314:51:49.263571+0800TestBlock[4573:178396]augusstr(__NSCFConstantString<0x7ffee30a1008>:0x10cb63198):augusStr2021-09-0314:51:49.263697+0800TestBlock[4573:178396]copyAugusstr(__NSCFConstantString<0x7ffee30a1000>:0x10cb63198):augusStr2021-09-0314:51:49.263808+0800TestBlock[4573:178396]mutableCopyAugusstr(__NSCFString<0x7ffee30a0ff8>:0x6000036bcfc0):augusStr❝__NSCFConstantString是字符串常量類,可看作NSString,__NSCFString是字符串類,可看作NSMutableString
結論:str和copyAugus打印出來的內存地址是一樣的,都是0x10cb63198且類名相同都是__NSCFConstantString,表明都是淺拷貝,都是NSString;變量mutableCopyAugus打印出來的內存地址和類名都不一致,所以是生成了新的對象
NSMutableString
NSMutableString*str=[NSMutableStringstringWithString:@"augusMutableStr"];NSMutableString*copyStr=[strcopy];NSMutableString*mutableCopyStr=[strmutableCopy];NSLog(@"str:(%@<%p>:%p):%@",[strclass],&str,str,str);NSLog(@"copyStr:(%@<%p>:%p):%@",[copyStrclass],©Str,copyStr,copyStr);NSLog(@"mutableCopyStr:(%@<%p>:%p):%@",[mutableCopyStrclass],&mutableCopyStr,mutableCopyStr,mutableCopyStr);//控制台輸出2021-09-0315:31:56.105642+0800TestBlock[4778:198224]str:(__NSCFString<0x7ffeeaa34008>:0x600001a85fe0):augusMutableStr2021-09-0315:31:56.105804+0800TestBlock[4778:198224]copyStr:(__NSCFString<0x7ffeeaa34000>:0x600001a86400):augusMutableStr2021-09-0315:31:56.105901+0800TestBlock[4778:198224]mutableCopyStr:(__NSCFString<0x7ffeeaa33ff8>:0x600001a86070):augusMutableStr結論:str和copyStr和mutableCopyStr打印出來的內存地址都不一樣的,但是生成的類簇都是__NSCFString,也就是NSMutableString
因為本文對NSMutableSet展開討論,所以只對該類進行測試,其餘的NSArray&NSMutableArray和NSDictionary&NSMutableDictionary本質是一樣的,請小夥伴自行參考測試就行
NSSet
Person*p1=[[Personalloc]init];Person*p2=[[Personalloc]init];Person*p3=[[Personalloc]init];NSSet*set=[[NSSetalloc]initWithArray:@[p1,p2,p3]];NSSet*copySet=[setcopy];NSSet*mutableCopySet=[setmutableCopy];NSLog(@"set:(%@<%p>:%p):%@",[setclass],&set,set,set);NSLog(@"copySet:(%@<%p>:%p):%@",[copySetclass],©Set,copySet,copySet);NSLog(@"mutableCopySet:(%@<%p>:%p):%@",[mutableCopySetclass],&mutableCopySet,mutableCopySet,mutableCopySet);//控制台輸出2021-09-0316:11:36.590338+0800TestBlock[4938:219837]set:(__NSSetI<0x7ffeef3f7fd0>:0x6000007322b0):{(<Person:0x600000931e00>,<Person:0x600000931e20>,<Person:0x600000932000>)}2021-09-0316:11:36.590479+0800TestBlock[4938:219837]copySet:(__NSSetI<0x7ffeef3f7fc8>:0x6000007322b0):{(<Person:0x600000931e00>,<Person:0x600000931e20>,<Person:0x600000932000>)}2021-09-0316:11:36.590614+0800TestBlock[4938:219837]mutableCopySet:(__NSSetM<0x7ffeef3f7fc0>:0x600000931fa0):{(<Person:0x600000931e00>,<Person:0x600000932000>,<Person:0x600000931e20>)}❝__NSSetI是不可變去重無序集合的子類,即NSSet,__NSSetM是可變去重無序集合的子類,即NSMutableSet
結論:set和copySet打印出來的內存地址是一致的0x6000007322b0,類簇都是__NSSetI說明是淺拷貝,沒有生成新對象,也都屬於類 NSSet;mutableCopySet的內存地址和類簇都不同,所以是深拷貝,生成了新的對象,屬於類NSMutablSet;集合裡面的元素地址都是一樣的
NSMutableSet
NSMutableSet*set=[[NSMutableSetalloc]initWithArray:@[p1,p2,p3]];NSMutableSet*copySet=[setcopy];NSMutableSet*mutableCopySet=[setmutableCopy];NSLog(@"set:(%@<%p>:%p):%@",[setclass],&set,set,set);NSLog(@"copySet:(%@<%p>:%p):%@",[copySetclass],©Set,copySet,copySet);NSLog(@"mutableCopySet:(%@<%p>:%p):%@",[mutableCopySetclass],&mutableCopySet,mutableCopySet,mutableCopySet);//控制台輸出2021-09-0316:33:35.573557+0800TestBlock[5043:232294]set:(__NSSetM<0x7ffeefb78fd0>:0x600002b99640):{(<Person:0x600002b99620>,<Person:0x600002b99600>,<Person:0x600002b995e0>)}2021-09-0316:33:35.573686+0800TestBlock[5043:232294]copySet:(__NSSetI<0x7ffeefb78fc8>:0x6000025e54a0):{(<Person:0x600002b99620>,<Person:0x600002b99600>,<Person:0x600002b995e0>)}2021-09-0316:33:35.573778+0800TestBlock[5043:232294]mutableCopySet:(__NSSetM<0x7ffeefb78fc0>:0x600002b99680):{(<Person:0x600002b99620>,<Person:0x600002b99600>,<Person:0x600002b995e0>)}結論:set和copySet和mutableCopySet的內存地址都不一樣,說明操作都是深拷貝;集合裡面的元素地址都是一樣的
結論分析
一次崩潰定位,一次源碼之旅,一系列拷貝操作,基本可以把文中提到的問題說清楚;遇到問題不要怕刨根問底,因為問底的盡頭就是無盡的光明

也許你還想看
(▼點擊文章標題或封面查看)
小小的宏 大大的世界
2021-12-09

iOS下的閉包上篇-Block
2021-11-04

你真的了解符號化麼?
2021-09-16

乾貨:探秘WKWebView
2021-10-21

前端工程化-打造企業通用腳手架
2022-01-13
