背景
处理 oncall 问题时,发现了一类共性问题,iOS 12 使用 KVO 就有概率触发崩溃。堆栈非常分散,任意使用 KVO 的场景都有可能触发。
崩溃堆栈分析
崩溃原因:
Fatal error: Should never be reached: file /BuildRoot/Library/Caches/com.apple.xbs/Sources/swiftlang_overlay_Foundation_Device/swiftlang-1001.2.63.13/swift/stdlib/public/SDK/Foundation/NSObject.swift, line 42
崩溃堆栈:
0 libswiftCore.dylib _$ss17_assertionFailure__4file4line5flagss5NeverOs12StaticStringV_SSAHSus6UInt32VtFTf4xxnnn_n()
1 libswiftCore.dylib _$ss17_assertionFailure__4file4line5flagss5NeverOs12StaticStringV_SSAHSus6UInt32VtFTf4xxnnn_n()
2 libswiftCore.dylib _$ss17_assertionFailure__4file4line5flagss5NeverOs12StaticStringV_SSAHSus6UInt32VtF()
3 libswiftFoundation.dylib _$sSo8NSObjectC10FoundationE48__old_unswizzled_keyPathsForValuesAffectingValue33_6DA0945A07226B3278459E9368612FF4LL6forKeyShySSGSSSg_tFZTo()
4 libswiftFoundation.dylib _$s10Foundation27__KVOKeyPathBridgeMachinery33_6DA0945A07226B3278459E9368612FF4LLC31keyPathsForValuesAffectingValue6forKeyShySSGSSSg_tFZ()
5 libswiftFoundation.dylib _$s10Foundation27__KVOKeyPathBridgeMachinery33_6DA0945A07226B3278459E9368612FF4LLC31keyPathsForValuesAffectingValue6forKeyShySSGSSSg_tFZTo()
6 Foundation -[NSKeyValueUnnestedProperty _givenPropertiesBeingInitialized:getAffectingProperties:]()
7 Foundation -[NSKeyValueUnnestedProperty _initWithContainerClass:key:propertiesBeingInitialized:]()
8 Foundation _NSKeyValuePropertyForIsaAndKeyPathInner.llvm.8205242823520915647()
9 Foundation _NSKeyValuePropertyForIsaAndKeyPath()
10 Foundation -[NSObject(NSKeyValueObserverRegistration) addObserver:forKeyPath:options:context:]()
栈顶进行 demangle:
function signature specialization <Arg[0] = Exploded, Arg[1] = Exploded> of Swift._assertionFailure(_: Swift.StaticString, _: Swift.String, file: Swift.StaticString, line: Swift.UInt, flags: Swift.UInt32) -> Swift.Never()
function signature specialization <Arg[0] = Exploded, Arg[1] = Exploded> of Swift._assertionFailure(_: Swift.StaticString, _: Swift.String, file: Swift.StaticString, line: Swift.UInt, flags: Swift.UInt32) -> Swift.Never()
Swift._assertionFailure(_: Swift.StaticString, _: Swift.String, file: Swift.StaticString, line: Swift.UInt, flags: Swift.UInt32) -> Swift.Never()
@objc static (extension in Foundation):__C.NSObject.(__old_unswizzled_keyPathsForValuesAffectingValue in _6DA0945A07226B3278459E9368612FF4)(forKey: Swift.String?) -> Swift.Set<Swift.String>()
static Foundation.(__KVOKeyPathBridgeMachinery in _6DA0945A07226B3278459E9368612FF4).keyPathsForValuesAffectingValue(forKey: Swift.String?) -> Swift.Set<Swift.String>()
@objc static Foundation.(__KVOKeyPathBridgeMachinery in _6DA0945A07226B3278459E9368612FF4).keyPathsForValuesAffectingValue(forKey: Swift.String?) -> Swift.Set<Swift.String>()
分析栈顶,在调用 NSObject 的 __old_unswizzled_keyPathsForValuesAffectingValue 方法时,触发了 assert。
比较幸运的是 Swift 的代码是开源的, iOS 12 对应的 Swift 版本 4.2。
__old_unswizzled_keyPathsForValuesAffectingValue 实现,调用即触发 fatalError:
fileprivate extension NSObject {
@objc class func _old_unswizzled_automaticallyNotifiesObservers(forKey key: String?) -> Bool {
fatalError("Should never be reached")
}
@objc class func _old_unswizzled_keyPathsForValuesAffectingValue(forKey key: String?) -> Set<String> {
fatalError("Should never be reached")
}
}
__old_unswizzled_keyPathsForValuesAffectingValue 根据命名和上下语境可以推测,该方法用来保存未替换之前的 keyPathsForValuesAffectingValue 方法。
__old_unswizzled_keyPathsForValuesAffectingValue 的上一层栈帧 __KVOKeyPathBridgeMachinery keyPathsForValuesAffectingValue:
@objc override class func keyPathsForValuesAffectingValue(forKey key: String?) -> Set<String> {
//This is swizzled so that it's -[NSObject keyPathsForValuesAffectingValueForKey:]
if let customizingSelf = self as? NSKeyValueObservingCustomization.Type, let path = _KVOKeyPathBridgeMachinery._bridgeKeyPath(key!) {
let resultSet = customizingSelf.keyPathsAffectingValue(for: path)
return Set(resultSet.lazy.map {
guard let str = $0._kvcKeyPathString else { fatalError("Could not extract a String from KeyPath ($0)") }
return str
})
} else {
return self._old_unswizzled_keyPathsForValuesAffectingValue(forKey: key) //swizzled to be NSObject's original implementation
}
}
根据这里的注释,__KVOKeyPathBridgeMachinery 和 NSObject 的keyPathsForValuesAffectingValueForKey 进行了替换。根据上下文也不难看出 __KVOKeyPathBridgeMachinery 是一个中间层,用于处理特定类型的 KVO 对象,当满足筛选条件,走 __KVOKeyPathBridgeMachinery 的处理逻辑,否则走 NSObject 的原始方法。
在 NSObject.swift 文件内查找 keyPathsForValuesAffectingValueForKey 的替换逻辑:
@nonobjc static var keyPathTable: [String : AnyKeyPath] = {
/*
Move all our methods into place. We want the following:
_KVOKeyPathBridgeMachinery's automaticallyNotifiesObserversForKey:, and keyPathsForValuesAffectingValueForKey: methods replaces NSObject's versions of them
NSObject's automaticallyNotifiesObserversForKey:, and keyPathsForValuesAffectingValueForKey: methods replace NSObject's _old_unswizzled_* methods
NSObject's _old_unswizzled_* methods replace _KVOKeyPathBridgeMachinery's methods, and are never invoked
*/
let rootClass: AnyClass = NSObject.self
let bridgeClass: AnyClass = _KVOKeyPathBridgeMachinery.self
let dependentSel = #selector(NSObject.keyPathsForValuesAffectingValue(forKey:))
let rootDependentImpl = class_getClassMethod(rootClass, dependentSel)!
let bridgeDependentImpl = class_getClassMethod(bridgeClass, dependentSel)!
method_exchangeImplementations(rootDependentImpl, bridgeDependentImpl) // NSObject <-> Us
let originalDependentImpl = class_getClassMethod(bridgeClass, dependentSel)! //we swizzled it onto this class, so this is actually NSObject's old implementation
let originalDependentSel = #selector(NSObject._old_unswizzled_keyPathsForValuesAffectingValue(forKey:))
let dummyDependentImpl = class_getClassMethod(rootClass, originalDependentSel)!
method_exchangeImplementations(originalDependentImpl, dummyDependentImpl) // NSObject's original version <-> NSObject's _old_unswizzled_ version
// 省略了 automaticallyNotifiesObservers 的替换
return [:]
}()
第一次 swizzle:
let dependentSel = #selector(NSObject.keyPathsForValuesAffectingValue(forKey:))
let rootDependentImpl = class_getClassMethod(rootClass, dependentSel)!
let bridgeDependentImpl = class_getClassMethod(bridgeClass, dependentSel)!
method_exchangeImplementations(rootDependentImpl, bridgeDependentImpl) // NSObject <-> Us
NSObject & _KVOKeyPathBridgeMachinery 交换方法 keyPathsForValuesAffectingValue 的 imp,
NSObject keyPathsForValuesAffectingValue sel 指向 _KVOKeyPathBridgeMachinery 的 keyPathsForValuesAffectingValue imp。
_KVOKeyPathBridgeMachinery keyPathsForValuesAffectingValue sel 指向 NSObject 的 keyPathsForValuesAffectingValue imp。
第二次 swizzle:
let originalDependentImpl = class_getClassMethod(bridgeClass, dependentSel)! //we swizzled it onto this class, so this is actually NSObject's old implementation
let originalDependentSel = #selector(NSObject._old_unswizzled_keyPathsForValuesAffectingValue(forKey:))
let dummyDependentImpl = class_getClassMethod(rootClass, originalDependentSel)!
method_exchangeImplementations(originalDependentImpl, dummyDependentImpl) // NSObject's original version <-> NSObject's _old_unswizzled_ version
_KVOKeyPathBridgeMachinery.keyPathsForValuesAffectingValue sel 指向 NSObject _old_unswizzled_keyPathsForValuesAffectingValue 的 imp
NSObject 的 _old_unswizzled_keyPathsForValuesAffectingValue sel 指向 _KVOKeyPathBridgeMachinery keyPathsForValuesAffectingValue imp,经过第一次交换也就是
NSObject 的 keyPathsForValuesAffectingValue imp。_old_unswizzled_keyPathsForValuesAffectingValue 命名和语义能够对上,但是崩溃的时候 _old_unswizzled_keyPathsForValuesAffectingValue sel 指向的还是 NSObject 的 _old_unswizzled_keyPathsForValuesAffectingValue 的 imp,触发了 assert
fatalError("Should never be reached")
另外崩溃的时候已经执行到了 __KVOKeyPathBridgeMachinery keyPathsForValuesAffectingValue 说明第一次 swizzle 已经成功了。在 __KVOKeyPathBridgeMachinery keyPathsForValuesAffectingValue 内调用 _old_unswizzled_keyPathsForValuesAffectingValue 执行的是 _old_unswizzled_keyPathsForValuesAffectingValue imp 也就是第二次 swizzle 还没有成功。
未成功的原因猜测可能有几种:
- 代码尚未执行
- 执行了,swizzle 失败了
swizzle 失败的概率很小,且造成失败的原因都是未知的,因此优先从代码尚未执行入手分析。
第一次 keyPathsForValuesAffectingValue swizzle 执行完,此时第二次 _old_unswizzled_keyPathsForValuesAffectingValue swizzle 尚未执行,当其他线程调用 keyPathsForValuesAffectingValue 方法时,继续执行 _old_unswizzled_keyPathsForValuesAffectingValue 便会走到 origin 方法里面,也就是 fatalError 的实现。接下来按照这个思路继续排查。
什么时候会触发这个 swizzle 逻辑呢?
demo 工程未添加 KVO 直接调用 _old_unswizzled_keyPathsForValuesAffectingValue 方法,触发 fatalError。说明 method swizzle 没有执行。
[NSObject performSelector:@selector(__old_unswizzled_automaticallyNotifiesObserversForKey:) withObject:nil];
这个崩溃显然和 Swift 相关,测试对 OC 对象添加监听后 ,预料之中,仍然触发 fatalError。
[self addObserver:self forKeyPath:@"name"
options:NSKeyValueObservingOptionNew
context:nil];
[NSObject performSelector:@selector(__old_unswizzled_automaticallyNotifiesObserversForKey:) withObject:nil];
尝试对 Swift 对象添加 observer,结果有些出乎意料,__old_unswizzled_automaticallyNotifiesObserversForKey 依旧会触发 fatalError,method swizzle 还是没有执行。
@objc class SwiftObj: NSObject {
@objc public var name: String?
@objc public init(name: String? = nil) {
self.name = name
super.init()
addObserver(self,
forKeyPath: "name",
options: .new
, context: nil);
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {}
}
难不成 Swift 和 OC 添加 observer 的方式不一样?上网查询后,发现 Swift 还有另外一种添加监听的方式。
OC 的 KVO 通过方法回调:
Swift 通过闭包回调:
developer.apple.com/documentati…
demo 中使用 Swift KVO 添加监听:
@objc class SwiftObj: NSObject {
@objc dynamic var name: String?
var observation: NSKeyValueObservation?
@objc public init(name: String? = nil) {
self.name = name
super.init()
observation = observe(.name!
, options: .new,
changeHandler: { _, _ in
})
}
}
此时调用 __old_unswizzled_automaticallyNotifiesObserversForKey 不再触发崩溃,说明执行了 method swizzle。
在代码内搜索 NSKeyValueObservation 只发现有一处调用,这极大的缩小了我们的排查范围。
只有使用了 Swift KVO 才会执行 _KVOKeyPathBridgeMachinery 相关的替换逻辑,删除这一处调用,这个问题不就解了?
崩溃现场分析
回到崩溃现场,在查看多个线程堆栈后,每个堆栈的全部线程堆栈中都有一个线程正在执行创建唯一的那个 NSKeyValueObservation。
NSKeyValueObservation 栈顶执行:
_$s10Foundation27_KeyValueCodingAndObservingPAAE7observe_7options13changeHandlerAA05NSKeyC11ObservationCs0B4PathCyxqd__G_So0kcF7OptionsVyx_AA0kC14ObservedChangeVyqd__GtctlF()
demangle 后的符号:
(extension in Foundation):Foundation._KeyValueCodingAndObserving.observe<A>(_: Swift.KeyPath<A, A1>, options: __C.NSKeyValueObservingOptions, changeHandler: (A, Foundation.NSKeyValueObservedChange<A1>) -> ()) -> Foundation.NSKeyValueObservation()
_KeyValueCodingAndObserving observe 的实现如下,observe 方法会创建一个 NSKeyValueObservation 对象,然后执行该对象的 start 方法:
///when the returned NSKeyValueObservation is deinited or invalidated, it will stop observing
public func observe<Value>(
_ keyPath: KeyPath<Self, Value>,
options: NSKeyValueObservingOptions = [],
changeHandler: @escaping (Self, NSKeyValueObservedChange<Value>) -> Void)
-> NSKeyValueObservation {
let result = NSKeyValueObservation(object: self as! NSObject, keyPath: keyPath) { (obj, change) in
let notification = NSKeyValueObservedChange(kind: change.kind,
newValue: change.newValue as? Value,
oldValue: change.oldValue as? Value,
indexes: change.indexes,
isPrior: change.isPrior)
changeHandler(obj as! Self, notification)
}
result.start(options)
return result
}
NSKeyValueObservation 构造方法调用 _bridgeKeyPathToString:
_bridgeKeyPathToString 调用 _bridgeKeyPath 执行 method swizzle。
func _bridgeKeyPathToString(_ keyPath:AnyKeyPath) -> String {
return _KVOKeyPathBridgeMachinery._bridgeKeyPath(keyPath)
}
当然看崩溃堆栈,Swift KVO 的执行也并非是第一次 method swizzle 执行完了,第二次 swizzle 尚未开始,这是因为 mach 崩溃触发后,崩溃线程会被 suspend,而其它线程还在继续执行,但是根据我们的分析以及崩溃现场的状态,也足以能说明崩溃的原因和我们的推测吻合。
结论
修复方案
代码内仅有一处调用,最快速的修复方案就是把这里的 Swift KVO 删除掉。App 兼容 iOS 12 的情况下,后续避免调用该 Api。
看这些接口的实现 Swift KVO 和一些第三方库的目的类似,因此也没有非用不可的理由。
激增原因
和相关同学沟通后,这里的代码在近期的一个优化里面放到了子线程执行,KVO 多在主线程执行,导致多线程冲突的概率增大。最近这个功能推全,崩溃的量级上涨。
免责声明
以上分析,只针对崩溃这个场景下的 Swift KVO 调用,对 Swift KVO 整体实现,本人并没有深入的研究,了解相关的实现建议看下源码。
文章中如有任何问题,欢迎评论区交流~~