字符串与多线程的爱恨情仇
问题的引出
做一次埋点需求的时候,需要在最后埋点 SDK 上报的时候,进行一些拦截和计算,来插入一些新的埋点属性,顺便还要记录所有埋点的数据展示给测试看,所以就存在类似的代码:
// 所有埋点上报最终调用的方法
- (void)sendEventWithParams:(Event *)event {
if (…) { // 一些计算判断
[self update:event]; // 加入一些参数
}
[self.eventString appendString:[self stringFrom:event]]; // 记录一些埋点信息
[EventSDK sendEvent:event]; // 上报埋点
}
后面考虑到性能问题,决定把一些计算操作放在子线程去处理,因为本身埋点上报 [EventSDK sendEvent:event];
也是一个异步处理,所以觉得这样做应该没什么问题。修改之后的代码大概就是这样:
// 所有埋点上报最终调用的方法
- (void)sendEventWithParams:(Event *)event {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
if (…) { // 一些计算判断
[self update:event]; // 加入一些参数
}
[self.eventString appendString:[self stringFrom:event]]; // 记录一些埋点信息
[EventSDK sendEvent:event]; // 上报埋点
})
}
一切都按照想象中的执行,没有因为埋点数量过多而存在浪费性能的问题。但是,当多次调试 app 的时候,会偶现崩溃!而且最终问题也定位到了我们修改的埋点方法,一定是存在隐藏的风险,在特殊的情况下会导致崩溃。
一番调试,最终定位问题指向:
[self.eventString appendString:[self stringFrom:event]]; // 记录一些埋点信息
// 控制台输出
malloc: Double free of object 0x101080c40
多线程对字符串操作会存在风险?
属性的赋值
为什么会释放两次呢?第一反应就是去查询方法的内部实现,appendString
的源码我们是无从得知的,但是我们可以推断出:它的内部应该也存在一个赋值过程。当我们把记录埋点信息修改成赋值时:
self.eventString = [self stringFrom:event]; // 记录一些埋点信息
同样会偶现类似的崩溃:
-[CFString release]: message sent to deallocated instance 0x100fe15b0
两次崩溃错误都是向已经释放的对象发送 release
消息。
对于属性的赋值,我们可以在 Apple 开源的 runtime 中略知一二,通过 编译的Runtime源码,属性相关赋值的方式分为下述两种:
属性赋值方式一:
void objc_storeStrong(id *location, id obj)
{
id prev = *location;
if (obj == prev) {
return;
}
objc_retain(obj);
*location = obj;
objc_release(prev);
}
属性赋值方式二:
static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
{
if (offset == 0) {
object_setClass(self, newValue);
return;
}
id oldValue;
id *slot = (id*) ((char*)self + offset);
if (copy) {
newValue = [newValue copyWithZone:nil];
} else if (mutableCopy) {
newValue = [newValue mutableCopyWithZone:nil];
} else {
if (*slot == newValue) return;
newValue = objc_retain(newValue);
}
if (!atomic) {
oldValue = *slot;
*slot = newValue;
} else {
spinlock_t& slotlock = PropertyLocks[slot];
slotlock.lock();
oldValue = *slot;
*slot = newValue;
slotlock.unlock();
}
objc_release(oldValue);
}
==ARC 环境下==
strong
@property (nonatomic, strong) NSString *string;
当我们在代码中调用到 self.string = @"like"
的时候,实际上会调用到
void objc_storeStrong(id *location, id obj)
atomic & copy
@property (atomic) NSString *string;
@property (copy) NSString *string;
当我们在代码中调用到 self.string = @"like"
的时候,实际上会调用到
static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
对于直接调用
_string = @“like”
的时候,都会调用void objc_storeStrong(id *location, id obj)
,与属性修饰符没有任何关系。
==MRC 环境下==
所有属性赋值 self.string = @"like"
都会调用
static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
解决问题
通过分析源码,崩溃的原因就无处可逃了。其实原因很简单,我们可以分析赋值的具体实现:
void objc_storeStrong(id *location, id obj)
{
id prev = *location; // 1
if (obj == prev) {
return;
}
objc_retain(obj); // 2
*location = obj;
objc_release(prev); // 3
}
异步并发执行大量的赋值操作的时候,多个线程会对同一个对象进行赋值操作,很有可能线程 A 进行到 2 的同时 线程 B 进行到 1,这个时候,当线程 A 继续执行到 3,释放了 prev,随后线程 B 也执行到 3 ,也对 prev 进行释放操作,所以就会出现对已经释放的对象又进行释放操作,当然会崩溃。
对于 copy
修饰对属性亦是如此,所以对于多线程的数据操作,我们都必须考虑安全问题。
当然解决方法其实也很简单,主要是我们要有多线程安全的意识,上面 属性赋值方式二 的相关源码已经为我们提供了一些思路:
方案一 采用 atomic
进行属性修饰
@property (atomic, strong) NSMutableString *string;
方案二 采用串行队列
dispatch_queue_t queue = dispatch_queue_create("lable", DISPATCH_QUEUE_SERIAL);
上面我们讨论的属性的赋值安全问题,但实际上对于一些其他操作,例如:
[self.stringArray addObject:@"hello"];
也会存在多线程的安全问题。对于一些我们常用的对象:NSString
,NSMutableString
,NSArray
,NSMutableArray
,NSDictionary
,NSMutableDictionary
Apple 都没为其保证多线程的安全性,毕竟加锁消耗性能,特别对这些基础数据结构对象,高性能显得格外重要,所以多线程操作这种少数情况就交给我们去管理。
NSCache 是线程安全的。
番外
尽量使用 copy 修饰不可变对象(NSString, NSArray, NSDictionary 等)
观察上述两种属性赋值情况,strong
修饰的属性可能会调用到两种不同的方法,但是不论哪一种方法,都是相同的赋值逻辑:
-
objc_retain(obj);
保留新值 -
*location = obj;
赋值 -
objc_release(prev);
释放旧值
而对于 copy
来说,赋值逻辑只有一种:
-
newValue = [newValue copyWithZone:nil];
复制新值 -
oldValue = *slot; *slot = newValue;
赋值 -
objc_release(oldValue);
释放旧值
所以对于一些不可变对象来说,作为属性的时候最好使用 copy
来修饰,这样每次赋值的时候都会 copy
一次新值,避免错误把可变对象赋值给不可变对象,这样就存在不可变对象可能会被改变的风险,举个 🌰 :
strong 修饰
@property (nonatomic, strong) NSString *string;
执行
- (void)someFunc {
self.string = @"hi";
NSMutableString *mString = @"hello".mutableCopy;
self.string = mString;
NSLog(@"time1: %@", self.string);
//... do something
[mString appendString:@" world"];
NSLog(@“time2: %@“, self.string);
}
打印
time1: hello
time2: hello world
可以看到不可变字符串被偷偷摸摸改变了!再试试 copy
修饰:
@property (nonatomic, copy) NSString *string;
打印
time1: hello
time2: hello
还是 copy
靠得住呀!
尽量使用点语法操作属性
调试源码,我们可以知道成员变量的赋值的时候会调用:
void objc_storeStrong(id *location, id obj)
{
id prev = *location;
if (obj == prev) {
return;
}
objc_retain(obj);
*location = obj;
objc_release(prev);
}
可以看到,根本没走属性修饰符语义! 所以我们应该尽可能的必须要使用点语法,这样不仅能执行属性修饰语义,而且还能避免重写 setter
或者 getter
后无效的风险!
网友评论