为什么使用os_log()包装会导致双精度数未能正确记录日志?

52

考虑以下例子:

import Foundation
import os.log

class OSLogWrapper {

    func logDefault(_ message: StaticString, _ args: CVarArg...) {
        os_log(message, type: .default, args)
    }

    func testWrapper() {
        logDefault("WTF: %f", 1.2345)
    }
}

如果我创建一个 OSLogWrapper 的新实例并调用 testWrapper()

let logger = OSLogWrapper()
logger.testWrapper()

我在 Xcode 控制台中得到以下输出:

2018-06-19 18:21:08.327979-0400 WrapperWTF[50240:548958] WTF: 0.000000

我已经检查了所有我能想到的东西,但是我无法理清楚这里出了什么问题。查阅文档没有获得任何有用的东西。


请尝试使用%@代替%f - Kamran
3个回答

121
编译器通过将每个参数转换为声明的可变参数类型,将它们打包成该类型的Array,并将该数组传递给可变参数函数来实现可变参数。对于testWrapper,声明的可变参数类型是CVarArg,因此当testWrapper调用logDefault时,在内部发生了以下情况:testWrapper1.2345转换为CVarArg,创建一个Array<CVarArg>,并将其作为args传递给logDefault
然后,logDefault调用os_log,将Array<CVarArg>作为参数传递给它。你代码中的错误就在这里。这个错误非常微妙。问题在于,os_log不接受Array<CVarArg>参数;os_log本身在CVarArg上也是可变的。因此,Swift将args(一个Array<CVarArg>)转换为CVarArg,并将该转换后的CVarArg插入到另一个Array<CVarArg>中。数据结构看起来像这样:
Array<CVarArg> created in `logDefault`
  |
  +--> CVarArg (element at index 0)
         |
         +--> Array<CVarArg> (created in `testWrapper`)
                |
                +--> CVarArg (element at index 0)
                       |
                       +--> 1.2345 (a Double)

然后logDefault将这个新的Array<CVarArg>传递给os_log。因此,您要求os_log使用%f格式化其第一个元素,该元素(在某种程度上)是Array<CVarArg>,这是无意义的,并且您碰巧得到0.000000作为输出。(我说“在某种程度上”是因为这里有一些微妙之处,我稍后会解释。)
因此,logDefault将其传入的Array<CVarArg>作为可能的多个可变参数之一传递给os_log,但实际上您想要logDefault做的是将该传入的Array<CVarArg>作为整个可变参数集合传递给os_log,而不重新包装它。在其他语言中,这有时称为“参数展开”。
不幸的是,Swift目前还没有任何语法用于参数展开。它已经在Swift-Evolution中讨论过不止一次(例如,在此线程中),但目前还没有解决方案。
解决此问题的常见方法是寻找一个伴随函数,将已经捆绑好的可变参数作为单个参数传递。通常,伴随函数在函数名中添加了v。例如:
  • printf(可变参数)和vprintf(接受va_list,C语言中相当于Array<CVarArg>
  • NSLog(可变参数)和NSLogv(接受va_list
  • -[NSString initWithFormat:](可变参数)和-[NSString WithFormat:arguments:](接受va_list

所以你可能会寻找一个os_logv。不幸的是,你找不到。没有被记录的与os_log相关联的伴侣函数可以接受预打包的参数。

此时您有两个选择:

  • Give up on wrapping os_log in your own variadic wrapper, because there is simply no good way to do it, or

  • Take Kamran's advice (in his comment on your question) and use %@ instead of %f. But note that you can only have a single %@ (and no other format specifiers) in your message string, because you're only passing a single argument to os_log. The output looks like this:

    2018-06-20 02:22:56.132704-0500 test[39313:6086331] WTF: (
        "1.2345"
    )
    
你也可以在https://bugreport.apple.com提交一个增强请求雷达,要求添加os_logv函数,但不应期望它很快被实现。所以就是这样了。做其中的任何一件事情,或者提交一个雷达,然后继续你的生活。说真的,停在这里阅读没有任何好处。

好的,你一直在阅读。让我们来看看os_log的内部情况。事实证明,Swift os_log函数的实现是公共的Swift源代码的一部分:

@_exported import os
@_exported import os.log
import _SwiftOSOverlayShims

@available(macOS 10.14, iOS 12.0, watchOS 5.0, tvOS 12.0, *)
public func os_log(
  _ type: OSLogType,
  dso: UnsafeRawPointer = #dsohandle,
  log: OSLog = .default,
  _ message: StaticString,
  _ args: CVarArg...)
{
  guard log.isEnabled(type: type) else { return }
  let ra = _swift_os_log_return_address()

  message.withUTF8Buffer { (buf: UnsafeBufferPointer<UInt8>) in
    // Since dladdr is in libc, it is safe to unsafeBitCast
    // the cstring argument type.
    buf.baseAddress!.withMemoryRebound(
      to: CChar.self, capacity: buf.count
    ) { str in
      withVaList(args) { valist in
        _swift_os_log(dso, ra, log, type, str, valist)
      }
    }
  }
}
所以,原来有一个名为_swift_os_logos_log版本,它使用预打包的参数。Swift包装器使用withVaList(已记录)将Array<CVarArg>转换为va_list并将其传递给_swift_os_log,它本身也是公共Swift源代码的一部分。我不会在这里引用其代码,因为它很长,我们实际上不需要查看它。

无论如何,尽管它没有记录,我们实际上可以调用_swift_os_log。 我们只需将os_log的源代码复制并将其转换为您的logDefault函数:

func logDefaultHack(_ message: StaticString, dso: UnsafeRawPointer = #dsohandle, _ args: CVarArg...) {
    let ra = _swift_os_log_return_address()
    message.withUTF8Buffer { (buf: UnsafeBufferPointer<UInt8>) in
        buf.baseAddress!.withMemoryRebound(to: CChar.self, capacity: buf.count) { str in
            withVaList(args) { valist in
                _swift_os_log(dso, ra, .default, .default, str, valist)
            }
        }
    }
}

它可以工作。测试代码:

func testWrapper() {
    logDefault("WTF: %f", 1.2345)
    logDefault("WTF: %@", 1.2345)
    logDefaultHack("Hack: %f", 1.2345)
}

输出:

2018-06-20 02:22:56.131875-0500 test[39313:6086331] WTF: 0.000000
2018-06-20 02:22:56.132704-0500 test[39313:6086331] WTF: (
    "1.2345"
)
2018-06-20 02:22:56.132807-0500 test[39313:6086331] Hack: 1.234500

我会推荐这个解决方案吗?不会。绝对不会。 os_log 的内部实现是一个实现细节,可能会在未来的 Swift 版本中发生变化。所以不要依赖于它们。但是看看内部实现还是很有趣的。


最后一件事。为什么编译器不会抱怨将Array<CVarArg>转换为CVarArg?以及为什么Kamran的建议(使用%@)有效?

事实证明,这些问题有相同的答案:因为Array可以“桥接”到Objective-C对象。具体来说:

这种静默转换可能经常出错(就像你的情况一样),因此编译器警告并允许您使用显式强制转换来消除警告是合理的(例如args as CVarArg)。如果您愿意,可以在https://bugs.swift.org上报告错误。

16
哥们儿...哇....这是我在StackOverflow上得到的最好的答案,而我已经在这里待了一段时间了。我有点感觉我遇到了Swift处理可变参数的微妙之处,所以谢谢你。 - Mike Akers
对于任何试图使用 _swift_os_log* 函数的人来说,现在你将遇到“未解决的标识符”错误。 - zgosalvez
5
查看最新的Swift源码中的os_log,你会发现你现在需要导入_SwiftOSOverlayShims - kdubb
2
Rob,你在这里打开了地狱之门。干杯! - Vadim
关于Swift 5.2的os_log实现,请参见:https://github.com/apple/swift/blob/swift-5.2-branch/stdlib/public/Darwin/os/os_log.swift - spt025
显示剩余2条评论

3

如我在对Rob Mayoff的回答中所述,任何遇到使用os_signpost()出现问题的人,这里提供了一个我写的包装类:

import Foundation
import os
import _SwiftOSOverlayShims

public final class Signpost {

    private final let log: OSLog

    public init(log: OSLog) {
        self.log = log
    }

    public final func begin(name: StaticString, dso: UnsafeRawPointer = #dsohandle, idObject: AnyObject? = nil) {
        if #available(iOS 12.0, *) {
            signpost(.begin, dso: dso, name: name, idObject: idObject)
        }
    }

    public final func begin(name: StaticString, dso: UnsafeRawPointer = #dsohandle, idObject: AnyObject? = nil, _ format: StaticString, _ arguments: CVarArg...) {
        if #available(iOS 12.0, *) {
            signpost(.begin, dso: dso, name: name, idObject: idObject, format, arguments)
        }
    }

    public final func event(name: StaticString, dso: UnsafeRawPointer = #dsohandle, idObject: AnyObject? = nil) {
        if #available(iOS 12.0, *) {
            signpost(.event, dso: dso, name: name, idObject: idObject)
        }
    }

    public final func event(name: StaticString, dso: UnsafeRawPointer = #dsohandle, idObject: AnyObject? = nil, _ format: StaticString, _ arguments: CVarArg...) {
        if #available(iOS 12.0, *) {
            signpost(.event, dso: dso, name: name, idObject: idObject, format, arguments)
        }
    }

    public final func end(name: StaticString, dso: UnsafeRawPointer = #dsohandle, idObject: AnyObject? = nil) {
        if #available(iOS 12.0, *) {
            signpost(.end, dso: dso, name: name, idObject: idObject)
        }
    }

    public final func end(name: StaticString, dso: UnsafeRawPointer = #dsohandle, idObject: AnyObject? = nil, _ format: StaticString, _ arguments: CVarArg...) {
        if #available(iOS 12.0, *) {
            signpost(.end, dso: dso, name: name, idObject: idObject, format, arguments)
        }
    }

    @available(iOS 12.0, *)
    private final func signpost(_ type: OSSignpostType, dso: UnsafeRawPointer = #dsohandle, name: StaticString, idObject: AnyObject? = nil) {
        guard log.signpostsEnabled else { return }
        let signpostID = getSignpostId(forObject: idObject)
        os_signpost(type, dso: dso, log: log, name: name, signpostID: signpostID)
    }

    @available(iOS 12.0, *)
    private final func signpost(
        _ type: OSSignpostType,
        dso: UnsafeRawPointer,
        name: StaticString,
        idObject: AnyObject? = nil,
        _ format: StaticString,
        _ arguments: [CVarArg])
    {
        // This crazy mess is because [CVarArg] gets treated as a single CVarArg and repassing a CVarArg... actually passes a [CVarArg]
        // This was copied from the publicly available Swift source code at https://github.com/apple/swift/blob/master/stdlib/public/Darwin/os/os_signpost.swift#L40
        // THIS IS A HACK
        guard log.signpostsEnabled else { return }
        let signpostID = getSignpostId(forObject: idObject)
        guard signpostID != .invalid && signpostID != .null else { return }
        let ra = _swift_os_log_return_address()
        name.withUTF8Buffer { (nameBuf: UnsafeBufferPointer<UInt8>) in
            // Since dladdr is in libc, it is safe to unsafeBitCast
            // the cstring argument type.
            nameBuf.baseAddress!.withMemoryRebound(to: CChar.self, capacity: nameBuf.count) { nameStr in
                format.withUTF8Buffer { (formatBuf: UnsafeBufferPointer<UInt8>) in
                    // Since dladdr is in libc, it is safe to unsafeBitCast
                    // the cstring argument type.
                    formatBuf.baseAddress!.withMemoryRebound(to: CChar.self, capacity: formatBuf.count) { formatStr in
                        withVaList(arguments) { valist in
                            _swift_os_signpost_with_format(dso, ra, log, type, nameStr, signpostID.rawValue, formatStr, valist)
                        }
                    }
                }
            }
        }
    }

    @available(iOS 12.0, *)
    private final func getSignpostId(forObject idObject: AnyObject?) -> OSSignpostID {
        if let idObject = idObject {
            return OSSignpostID(log: log, object: idObject)
        }
        return .exclusive
    }
}

1

我需要一个包装os_log的函数,如果类型为.error,则可以将错误记录到crashlytics中。我的项目是iOS 13,因此我需要使用旧的os_log语法。正如Rob Mayoff所提到的,存在从CVarArg ...Array<CVArg>的隐式转换问题,当传递给os_log方法时会导致问题。

对于我来说,根据参数数量创建一个简单的开关,并针对每个计数进行分别调用os_log,从数组中取出每个参数并单独传递给os_log,解决了这个问题。代码很冗长,但对我来说做到了工作。

func log(_ message: StaticString, log: OSLog = .default, type: OSLogType = .default, _ args: CVarArg...) {
        switch args.count {
        case 0:
            os_log(message, log: log, type: type)
        case 1:
            os_log(message, log: log, type: type, args[0])
        case 2:
            os_log(message, log: log, type: type, args[0], args[1])
        case 3:
            os_log(message, log: log, type: type, args[0], args[1], args[2])
        case 4:
            os_log(message, log: log, type: type, args[0], args[1], args[2], args[3])
        case 5:
            os_log(message, log: log, type: type, args[0], args[1], args[2], args[3], args[4])
        default:
            assertionFailure("Currently only up to five arguments are supported")
        }
        // log error to crashlytics
    }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接