?
本文字数: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?=?[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
非法则进行默认值的处理
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
?
__NSCFConstantString
是字符串常量类,可看作NSString
,__NSCFString
是字符串类,可看作NSMutableString
结论:str
和copyAugus
打印出来的内存地址是一样的,都是0x10cb63198
且类名相同都是__NSCFConstantString
,表明都是浅拷贝,都是NSString
;变量mutableCopyAugus
打印出来的内存地址和类名都不一致,所以是生成了新的对象
类名 | 操作 | 新对象 | 拷贝类型 | 元素拷贝 | 新类名 |
---|
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
结论:str
和copyStr
和mutableCopyStr
打印出来的内存地址都不一样的,但是生成的类簇都是__NSCFString
,也就是NSMutableString
类名 | 操作 | 新对象 | 拷贝类型 | 元素拷贝 | 新类名 |
---|
NSMutableString | copy | YES | 深拷贝 | NO | NSMutableString |
| mutableCopy | YES | 深拷贝 | NO | NSMutableString |
集合类对象
?
因为本文对NSMutableSet
展开讨论,所以只对该类进行测试,其余的NSArray&NSMutableArray
和NSDictionary&NSMutableDictionary
本质是一样的,请小伙伴自行参考测试就行
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>
)}
?
__NSSetI
是不可变去重无序集合的子类,即NSSet
,__NSSetM
是可变去重无序集合的子类,即NSMutableSet
结论:set
和copySet
打印出来的内存地址是一致的0x6000007322b0
,类簇都是__NSSetI
说明是浅拷贝,没有生成新对象,也都属于类 NSSet
;mutableCopySet
的内存地址和类簇都不同,所以是深拷贝,生成了新的对象,属于类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):?{(
????<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
的内存地址都不一样,说明操作都是深拷贝;集合里面的元素地址都是一样的
类名 | 操作 | 新对象 | 拷贝类型 | 元素拷贝 | 新类名 |
---|
NSMutableSet | copy | YES | 深拷贝 | NO | NSSet |
| mutableCopy | YES | 深拷贝 | NO | NSMutablSet |
结论分析
参考文档
文档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