最近在排查fireBase上 线上的崩溃时,发现了一个很有趣的as?引发的野指针的问题。
先上代码,主要是对YYMemoryCache 读写的一个封装
internal func set(_ data: SparkChartData) {
let cacheData = Data(
timestamp: Date().timeIntervalSince1970,
data: data
)
self.cache.setObject(cacheData, forKey: NSString(string: "\(data.tickerId)"))
}
internal func get(_ tickerId: Int64) -> SparkChartData? {
guard let cacheData = self.cache.object(forKey: NSString(string: "\(tickerId)")) as? SparkChartDataCache.Data else {
return nil
}
return cacheData.data
}
其中cache是YYMemoryCache。我们知道YYMemoryCache的所有方法包括setObject getobject都是用pthread_mutex_lock锁住线程安全的。所以即使这2个方法是高频多线程调用的我们在review代码时也没有发现问题。
但是线上的fireBase记录却实实在在的告诉我们这段代码是有野指针的。虽然概率极低。不过我们在打印的堆栈信息中发现了蛛丝马迹
0
libswiftCore.dylib
swift_getObjectType + 48
1
libswiftCore.dylib
_bridgeAnyObjectToAny(_:) + 32
2
**********
<compiler-generated>
SparkChartDataCache.get(_:) + 4307961656
_bridgeAnyObjectToAny 和 swift_getObjectType 是 as? 的内部实现。在swift中的类型转换和object-C中的类型转换实现是完全不同的。
而
self.cache.object(forKey: NSString(string: "\(tickerId)")) as? SparkChartDataCache.Data
虽然这个代码看起来只有一行。但是实际执行的时候 其实会被分解成2步来完成。
1.self.cache.object(forKey: NSString(string: "(tickerId)"))
- as ? SparkChartDataCache.Data
步骤1 是在YYMemoryCache 保障的线程安全。而步骤二其实已经离开pthread_mutex_lock的作用范围。也就是说在1,2步骤之间正好有别的线程执行了set方法,而YYMemoryCache的set的实现我们可以发现,是先delete再set的。从而导致了步骤1得到的指针在步骤2时变成了一个野指针。虽然概率极低,但是高并发时还是会出现的。
最后我们在get和set用信号量做了读写锁之后,崩溃消失。虽然我们的锁其实会覆盖YYMemoryCache内部的锁,但是对效率影响不大。
网友评论