close

本文字數:10920字

預計閱讀時間:28分鐘

一次遍歷導致的崩潰

題記:用最通俗的語言,描述最難懂的技術

本文是作者在對項目進行調試某靜態庫的功能進行單元測試發現的問題的記錄,如果有哪些論述模糊或者不準確,請聯繫weiniu@sohu-inc.com

目錄表
故事背景
問題定位
解決方案
原理
copy是什麼
copy如何實現
copy底層實現
延展之深淺拷貝
集合類對象
非集合類對象
參考文檔
結束語
故事背景

環境及場景:

編譯環境Xcode 12.5.1

2021年8月的某一天,Augus正在調試項目需求A,因為A要求需要接入一個SDK進行實現某些採集功能

操作流程

在程序啟動的最開始地方,初始化SDK,並分配內存空間

在某次的啟動中就出現了以下錯誤

Trapped uncaught exception 'NSGenericException', reason: '*** Collection <__NSSetM: 0x2829f9740> was mutated while being enumerated.'

初步猜測

開始的時候,我先排除自己代碼的原因(畢竟代碼自己寫的,還是求穩一些),因為調試模式下沒有開全局斷點,所以本次的崩潰就這麼被錯失機會定位

為了下一次的復現

首先進行了NSMutableSet某些方法的hook
開啟全局斷點

最後定位

項目中引入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在底層如何實現?
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非法則進行默認值的處理

類內部維護了一個可變集合用來存放好友
外部提供了新增和移除的兩個方法
- (id)copyWithZone:(NSZone *)zone;中的實現就是簡單的一個copy功能
而deepCopy是對可變集合的深層複製,至於原因,我們會在延展中舉例說明,這裡先擱置
copy底層實現

之前的文檔中說過,想要看底層的實現那就用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,我們現考慮當前的情況,

如果shouldCopy=0,那麼copy=NO,mutableCopy=NO
如果shouldCopy=1,那麼copy=YES,mutableCopy=NO

下面繼續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],&copyAugus,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打印出來的內存地址和類名都不一致,所以是生成了新的對象

類名操作新對象拷貝類型元素拷貝新類名NSStringcopyNO淺拷貝NONSStringmutableCopyYES深拷貝NONSMutableString

NSMutableString

NSMutableString*str=[NSMutableStringstringWithString:@"augusMutableStr"];NSMutableString*copyStr=[strcopy];NSMutableString*mutableCopyStr=[strmutableCopy];NSLog(@"str:(%@<%p>:%p):%@",[strclass],&str,str,str);NSLog(@"copyStr:(%@<%p>:%p):%@",[copyStrclass],&copyStr,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

類名操作新對象拷貝類型元素拷貝新類名NSMutableStringcopyYES深拷貝NONSMutableStringmutableCopyYES深拷貝NONSMutableString
集合類對象❝

因為本文對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],&copySet,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;集合裡面的元素地址都是一樣的

類名操作新對象拷貝類型元素拷貝新類名NSSetcopyNO淺拷貝NONSSetmutableCopyYES深拷貝NONSMutablSet

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],&copySet,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的內存地址都不一樣,說明操作都是深拷貝;集合裡面的元素地址都是一樣的

類名操作新對象拷貝類型元素拷貝新類名NSMutableSetcopyYES深拷貝NONSSetmutableCopyYES深拷貝NONSMutablSet

結論分析

NSMutable*開頭的類不要用copy屬性去修飾,因為每次賦值操作拷貝出來的都是不可變集合類
集合類的copy和mutableCopy操作,對象裡面的元素不會發生拷貝,只會對容器層面拷貝,也稱之為單層深拷貝
參考文檔
文檔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
結束語

一次崩潰定位,一次源碼之旅,一系列拷貝操作,基本可以把文中提到的問題說清楚;遇到問題不要怕刨根問底,因為問底的盡頭就是無盡的光明




也許你還想看

(▼點擊文章標題或封面查看)

小小的宏 大大的世界

2021-12-09

iOS下的閉包上篇-Block

2021-11-04

你真的了解符號化麼?

2021-09-16

乾貨:探秘WKWebView

2021-10-21

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

2022-01-13

arrow
arrow
    全站熱搜
    創作者介紹
    創作者 鑽石舞台 的頭像
    鑽石舞台

    鑽石舞台

    鑽石舞台 發表在 痞客邦 留言(0) 人氣()