一次遍历导致的崩溃


来源:搜狐产品技术团队

故事背景

环境及场景
编译环境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导致的崩溃

问题定位

?NSMutableSet?*mutableSet?=?[NSMutableSet?setWithObjects:@"1",@"2",@"3",?nil];
????
?for?(NSString?*item?in?mutableSet)?{
?????if?([item?integerValue]?<?3)?{
?????????[mutableSet?removeObject:item];
??????}
??}
左右滑动查看完整代码

控制台日志

很好,现在已经知道了问题的原因,那么接下来解决问题就很容易了,让我们继续

解决方案

问题原因总结
不能在一个可变集合,包括NSMutableArray,NSMutableDictionary等类似对象遍历的同时又对该对象进行添加或者移除操作

解决问题
把遍历中的对象进行一次copy操作

其实其中的道理很简单,我现在简而概括
你在内存中已经初始化一块区域,而且分配了地址,那么系统在这次的遍历中会把这次遍历包装成原子操作,因为会可能会访问坏内存或者越界的问题,当然这也是出于安全原因,不同的系统下的实现方式不同,但是底层的原理是一致的,都是为了保护对象在操作过程中不受可变因素的更新

那问题来了
copy是什么?
copy在底层如何实现?
copy有哪些需要注意的?

原理

copy是什么
copy是Objective-C编程语言下的属性修饰关键词,比如修饰Block?orNS*开头的对象

copy如何实现
对需要实现的类遵守NSCopying协议
实现NSCopying协议,该协议只有一个方法
-?(id)copyWithZone:(NSZone?*)zone;
举例说明,首先我们新建一个Perosn类进行说明,下面是示例代码
//?The?person.h?file
#import?<Foundation/Foundation.h>
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<Person?*>?*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
左右滑动查看完整代码
类的功能很简单,初始化的时候需要外层传入name进行初始化,如果name非法则进行默认值的处理
类内部维护了一个可变集合用来存放好友
外部提供了新增和移除的两个方法
- (id)copyWithZone:(NSZone *)zone;中的实现就是简单的一个copy功能
而deepCopy是对可变集合的深层复制,至于原因,我们会在延展中举例说明,这里先搁置

copy底层实现
之前的文档中说过,想要看底层的实现那就用clang -rewrite-objc main.m看源码
为了方便测试和查看,我们新建一个TestCopy的类继承NSObject,然后在TestCopy.m中只加如下代码
#import?"TestCopy.h"
@interface?TestCopy?()
@property(nonatomic,?copy)?NSString?*augusCopy;
@end
@implementation?TestCopy
@end
然后在终端执行$ clang -rewrite-objc TestCopy.m命令
接下来我们进行源码分析
//?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);?}
左右滑动查看完整代码
总结:copy的getter是根据地址偏移找到对应的实例变量进行返回,那么objc_setProperty又是怎么实现的呢?
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);
}
左右滑动查看完整代码
看到内部又调用了objc_setProperty_non_gc方法,这里主要看下这个方法内部的实现,前五个参数和开始的传入一致,最后的两个参数是由shouldCopy决定,shouldCopy在这里是0 or 1,我们现考虑当前的情况,
如果shouldCopy=0,那么copy=NO,mutableCopy=NO
如果shouldCopy=1,那么copy=YES,mutableCopy=NO
下面继续reallySetProperty的实现
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,最后还是调用了newValue = [newValue copyWithZone:NULL];,如果copy=NO and mutableCopy=NO,那么最后会调用newValue = objc_retain(newValue);
objc_retain的实现
id?objc_retain(id?obj)?{?return?[obj?retain];?}
总结:用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?=?[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
左右滑动查看完整代码
结论:str和copyAugus打印出来的内存地址是一样的,都是0x10cb63198且类名相同都是__NSCFConstantString,表明都是浅拷贝,都是NSString;变量mutableCopyAugus打印出来的内存地址和类名都不一致,所以是生成了新的对象。

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
左右滑动查看完整代码
结论:str和copyStr和mutableCopyStr打印出来的内存地址都不一样的,但是生成的类簇都是__NSCFString,也就是NSMutableString。


集合类对象
本文对NSMutableSet展开讨论,所以只对该类进行测试。
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):?{(
????<Person:?0x600000931e00>,
????<Person:?0x600000931e20>,
????<Person:?0x600000932000>
)}
2021-09-03?16:11:36.590479+0800?TestBlock[4938:219837]?copySet:?(__NSSetI<0x7ffeef3f7fc8>:?0x6000007322b0):?{(
????<Person:?0x600000931e00>,
????<Person:?0x600000931e20>,
????<Person:?0x600000932000>
)}
2021-09-03?16:11:36.590614+0800?TestBlock[4938:219837]?mutableCopySet:?(__NSSetM<0x7ffeef3f7fc0>:?0x600000931fa0):?{(
????<Person:?0x600000931e00>,
????<Person:?0x600000932000>,
????<Person:?0x600000931e20>
)}
左右滑动查看完整代码
结论:set和copySet打印出来的内存地址是一致的0x6000007322b0,类簇都是__NSSetI说明是浅拷贝,没有生成新对象,也都属于类 NSSet;mutableCopySet的内存地址和类簇都不同,所以是深拷贝,生成了新的对象,属于类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):?{(
????<Person:?0x600002b99620>,
????<Person:?0x600002b99600>,
????<Person:?0x600002b995e0>
)}
2021-09-03?16:33:35.573686+0800?TestBlock[5043:232294]?copySet:?(__NSSetI<0x7ffeefb78fc8>:?0x6000025e54a0):?{(
????<Person:?0x600002b99620>,
????<Person:?0x600002b99600>,
????<Person:?0x600002b995e0>
)}
2021-09-03?16:33:35.573778+0800?TestBlock[5043:232294]?mutableCopySet:?(__NSSetM<0x7ffeefb78fc0>:?0x600002b99680):?{(
????<Person:?0x600002b99620>,
????<Person:?0x600002b99600>,
????<Person:?0x600002b995e0>
)}
左右滑动查看完整代码
结论:set和copySet和mutableCopySet的内存地址都不一样,说明操作都是深拷贝;集合里面的元素地址都是一样的


结论分析
NSMutable*开头的类不要用copy属性去修饰,因为每次赋值操作拷贝出来的都是不可变集合类
集合类的copy和mutableCopy操作,对象里面的元素不会发生拷贝,只会对容器层面拷贝,也称之为单层深拷贝
一次崩溃定位,一次源码之旅,一系列拷贝操作,基本可以把文中提到的问题说清楚;遇到问题不要怕刨根问底,因为问底的尽头就是无尽的光明。
参考文档

结识技术大咖,提升IT技能
畅谈开发梦想,拓展人脉资源
参与话题讨论,赢取互动好礼
扫码添加小助手,立即加入社群


关注公众号:拾黑(shiheibook)了解更多
[广告]赞助链接:
四季很好,只要有你,文娱排行榜:https://www.yaopaiming.com/
让资讯触达的更精准有趣:https://www.0xu.cn/
关注网络尖刀微信公众号随时掌握互联网精彩
- 1 习近平将发表二〇二六年新年贺词 7904141
- 2 2026年国补政策来了 7808738
- 3 东部战区:开火!开火!全部命中! 7712893
- 4 2026年这些民生政策将惠及百姓 7616985
- 5 小学食堂米线过期2.5小时被罚5万 7519709
- 6 解放军喊话驱离台军 原声曝光 7428214
- 7 为博流量直播踩烈士陵墓?绝不姑息 7327605
- 8 每月最高800元!多地发放养老消费券 7238391
- 9 数字人民币升级 1月1日起将计付利息 7141831
- 10 2026年1月1日起 一批新规将施行 7040675








51CTO技术栈
