首页 > 解决方案 > 为什么包装 os_log() 会导致无法正确记录双打?

问题描述

考虑以下示例:

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

我已经检查了我能想到的所有内容,但我无法确定这里出了什么问题。浏览文档并没有产生任何帮助。

标签: iosswiftlogging

解决方案


编译器通过将每个参数转换为声明的可变参数类型,将它们打包成Array该类型的一个,并将该数组传递给可变参数函数来实现可变参数参数。在 的情况下testWrapper,声明的可变参数类型是CVarArg,所以当testWrapper调用时logDefault,这就是幕后发生的事情:testWrapper强制转换1.2345为 a CVarArg,创建一个Array<CVarArg>,并将其传递给logDefaultas args

然后logDefault调用os_log,将其Array<CVarArg>作为参数传递。这是您的代码中的错误。该错误非常微妙。问题是os_log不需要Array<CVarArg>争论。os_log本身就是可变参数的CVarArg。所以 Swift 将args(an 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格式化它的第一个元素,它是(有点) an Array<CVarArg>, using %f,这是无意义的,你碰巧得到0.000000了输出。(我说“有点”是因为这里有一些我稍后会解释的微妙之处。)

因此,logDefault将其传入Array<CVarArg>作为可能的许多可变参数之一传递给os_log,但您真正想要logDefault做的是将该传入Array<CVarArg>作为整个可变参数集传递给os_log,而不重新包装它。这有时在其他语言中被称为“争论飞溅”。

可悲的是,Swift 还没有任何用于参数飞溅的语法。在 Swift-Evolution 中已经不止一次讨论过这个问题(例如,在这个线程中),但目前还没有一个解决方案。

这个问题的通常解决方案是寻找一个伴随函数,它将已经捆绑的可变参数作为单个参数。通常,伴随v程序在函数名称中添加了一个。例子:

  • printf(variadic) and vprintf(take a va_list, C's 等价于Array<CVarArg>)
  • NSLog(可变参数)和NSLogv(取 a va_list
  • -[NSString initWithFormat:](可变参数)和-[NSString WithFormat:arguments:](取 a va_list

所以你可能会去寻找一个os_logv. 可悲的是,你不会找到一个。没有记录在案的伴侣os_log需要预先捆绑的参数。

此时您有两个选择:

  • 放弃os_log使用您自己的可变参数包装器进行包装,因为根本没有好方法可以做到这一点,或者

  • 接受卡姆兰的建议(在他对您问题的评论中)并使用%@而不是%f. 但请注意,您的消息字符串中只能有一个%@(并且没有其他格式说明符),因为您只将一个参数传递给os_log. 输出如下所示:

    2018-06-20 02:22:56.132704-0500 test[39313:6086331] WTF: (
        "1.2345"
    )
    

您还可以在https://bugreport.apple.com上提交增强请求雷达,要求提供os_logv功能,但您不应该期望它会很快实现。

就是这样了。做这两件事中的一件,也许归档一个雷达,然后继续你的生活。严重地。在这里停止阅读。这条线之后就没什么好说的了。


好吧,你一直在读。让我们窥视一下os_log。事实证明 Swiftos_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)
      }
    }
  }
}

所以事实证明,有一个版本os_log,称为_swift_os_log,它采用预先捆绑的参数。Swift 包装器使用withVaList(已记录)将 a 转换Array<CVarArg>为 ava_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? 为什么卡姆兰的建议(使用%@)有效?

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

这种静默转换可能通常是一个错误(就像您的情况一样),因此编译器发出警告是合理的,并允许您使用显式强制转换(例如args as CVarArg)使警告静音。如果需要,您可以在https://bugs.swift.org提交错误报告。


推荐阅读