iOS 12 Swift KVO 崩溃排查

617 阅读7分钟

背景

处理 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。

github.com/swiftlang/s…

__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 还没有成功。

未成功的原因猜测可能有几种:

  1. 代码尚未执行
  2. 执行了,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 整体实现,本人并没有深入的研究,了解相关的实现建议看下源码。

文章中如有任何问题,欢迎评论区交流~~