从一次线上事故,彻底搞懂Swift中的as

814 阅读3分钟

上次上线之后,在接口监控中,就有个接口时不时的就会报验签失败。并且全部都是iOS的系统报错的,验签的规则是这样的,把请求的所有参数进行转成字符串之后,进行某些操作,生成一个签名,把这个签名放到参数里,而服务端拿到参数之后进行相同的操作,也生成一个签名,进行比较,如果不一致,则会报签名错误。

在这个错误发生之后,有个组内的同学说,检查一下参数里有没有double类型的数据,如果有的话,可能会导致签名错误,一检查参数,果然有double类型的参数。可double类型为什么会造成签名错误呢?我们进行签名的时候是把double类型转成字符串,而传给后端进行urlEncoding的时候,也是把double类型转成string放到httpbody中的。

下面就解开谜底,原因是这样的

在我们的项目中,把Double类型转成String的操作是"\(double)"这种方式,如果double的值是恰好是整数,比如是1,则会转成"1.0",而Alamofire中的URLEncoding则是通过下面的方法把map中的key、value转成字符串的。

  public func queryComponents(fromKey key: String, value: Any) -> [(String, String)] {
    var components: [(String, String)] = []
    switch value {
    case let dictionary as [String: Any]:
        for (nestedKey, value) in dictionary {
            components += queryComponents(fromKey: "\(key)[\(nestedKey)]", value: value)
        }
    case let array as [Any]:
        for value in array {
            components += queryComponents(fromKey: arrayEncoding.encode(key: key), value: value)
        }
    case let number as NSNumber:
        if number.isBool {
            components.append((escape(key), escape(boolEncoding.encode(value: number.boolValue))))
        } else {
            components.append((escape(key), escape("\(number)")))
        }
    case let bool as Bool:
        components.append((escape(key), escape(boolEncoding.encode(value: bool))))
    default:
        components.append((escape(key), escape("\(value)")))
    }
    return components
}

经过debug,当value类型为Double时,竟然进入到了NSNumber的case里,在这里也就是说,当value为Double时会把value转成NSNumber再转成字符串,如果value类型为Double, 而小数部分为0,比如1,则会转成“1”,到这里验签失败的原因是找到了。

但是有产生了一个疑问?下面的代码为什么会转换成功,而as操作符又干了什么?

let d = 1.1
let number = d as NSNumber

通过源码加上debug发现了_ObjectiveCBridgeable这个协议,这个协议是在Swift类型和OC类型通过as转换的时候调用的,下面通过一个例子来展示一下这个协议如何使用。注释说明了协议中的每个方法是如何使用的。

/// 自定义一个结构体来实现和NSNumber进行相互转换
struct Test: _ObjectiveCBridgeable {
    var value : Int
    /// 定义了和OC相互转换的类型
    typealias _ObjectiveCType = NSNumber
    
    /// 当使用NSNumber as! Test时会调用
    static func _forceBridgeFromObjectiveC(_ source: NSNumber, result: inout Test?) {
        if !_conditionallyBridgeFromObjectiveC(source, result: &result) {
            fatalError("Unable to bridge \(_ObjectiveCType.self) to \(self)")
        }
    }
    
    /// 当使用NSNumber as? Test时会调用
    static func _conditionallyBridgeFromObjectiveC(_ source: NSNumber, result: inout Test?) -> Bool {
        result = Test(value: source.intValue )
        return true
    }
    
    /// 当OC文件和swift相互调用时,自动转换会调用此方法
    static func _unconditionallyBridgeFromObjectiveC(_ source: NSNumber?) -> Test {
        return Test(value: source?.intValue ?? 0)
    }
    
    /// 当使用Test as NSNumber时会调用
    func _bridgeToObjectiveC() -> NSNumber {
        return NSNumber(value: value)
    }
}

let t1 = Test(value: 10) //t1:Test
let n = t1 as NSNumber //n: NSNumber
let t2 = n as! Test // t2:Test

Swift源码中Double实现这个协议的源码如下

extension Double : _ObjectiveCBridgeable {
    @available(swift, deprecated: 4, renamed: "init(truncating:)")
    public init(_ number: __shared NSNumber) {
        self = number.doubleValue
    }

    public init(truncating number: __shared NSNumber) {
        self = number.doubleValue
    }

    public init?(exactly number: __shared NSNumber) {
        let type = number.objCType.pointee
        if type == 0x51 {
            guard let result = Double(exactly: number.uint64Value) else { return nil }
            self = result
        } else if type == 0x71 {
            guard let result = Double(exactly: number.int64Value) else  { return nil }
            self = result
        } else {
            // All other integer types and single-precision floating points will
            // fit in a `Double` without truncation.
            guard let result = Double(exactly: number.doubleValue) else { return nil }
            self = result
        }
    }

    @_semantics("convertToObjectiveC")
    public func _bridgeToObjectiveC() -> NSNumber {
        return NSNumber(value: self)
    }
    
    public static func _forceBridgeFromObjectiveC(_ x: NSNumber, result: inout Double?) {
        if !_conditionallyBridgeFromObjectiveC(x, result: &result) {
            fatalError("Unable to bridge \(_ObjectiveCType.self) to \(self)")
        }
    }
    
    public static func _conditionallyBridgeFromObjectiveC(_ x: NSNumber, result: inout Double?) -> Bool {
        if x.doubleValue.isNaN {
            result = x.doubleValue
            return true
        }
        result = Double(exactly: x)
        return result != nil
    }
    
    @_effects(readonly)
    public static func _unconditionallyBridgeFromObjectiveC(_ source: NSNumber?) -> Double {
        var result: Double?
        guard let src = source else { return Double(0) }
        guard _conditionallyBridgeFromObjectiveC(src, result: &result) else { return Double(0) }
        return result!
    }
}

总结

当使用as操作符转换类型时,如果as操作符两端分属于OC和Swift类型,就会调用_ObjectiveCBridgeable协议中对应的方法。

Swift有很多类型都实现了这个协议。比如我们很常见的Array,Dictionary。如果本篇文章对你有帮助的话,帮忙点个赞呗