Log 输出是程序开发中重要的组成部分,它虽然不是直接的业务代码,却可以忠实的反映我们的程序时如何工作的,以及记录程序运行的过程中发生了什么。

在Swift 中,最简单的输出方式是使用print,在我们关心的地方输出字符串和值。但是这并不够,试想一下当程序变得非常复杂的时候,我们可能会输出很多内容,而想在其中寻找到我们希望的输出其实并不容易。我们往往需要更好更精确的输出,这包括输出这个log 的文件调用的行号所处的方法名字等。

我们当然可以在print 的时候将当前的文件名和那些必要的信息作为参数同我们的消息一起进行打印:

// Test.swift
func method() {
    //...
    print("文件名:Test.swift,方法名:method,这是一条输出")
    //...
}

但是这显然非常麻烦,每次输入文件名和方法名不说,随着代码的改变,这些Log 的位置也可能发生改变,这时我们可能还需要不断的去维护这些输出,代价实在太大。

在Swift 中,编译器为我们准备了几个很有用的编译符号,用来处理类似这样的需求,它们分别是:

符号

类型

描述

#file

String

包含这个符号的文件的路径

#line

Int

符号出现处的行号

#column

Int

符号出现处的列

#function

String

包含这个符号的方法名字

因此我们可以通过这些符号来写一个好一些的Log 输出方法:

func printLog<T>(_ message: T, file: String = #file, method: String = #function, line: Int = #line) {
    print("\((file as NSString).lastPathComponent)[\(line)], \(method): \(message)")
}

这样,在打印输出的时候我们只需要使用这个方法就能完成文件名、行号及方法名的输出了。最棒的是,我们不再需要对这样的输出进行维护,无论在哪里它都能正确的输出各个参数:

// ViewController.swift
class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        printLog("这是一条输出");
    }
}

// 输出
// ViewController.swift[30], viewDidLoad(): 这是一条输出

另外,对于log 输出更多的其实是用在程序开发和调试的过程中的,过多的输出有可能对运行的性能造成影响。在Release 版本中关闭向控制台的输出也是软件开发中一种常见做法。如果我们在开发中就注意使用了统一的log 输出的话,这就变得非常简单了。使用条件编译的方法,我们可以添加条件,并设置合适的编译配置,使printLog 的内容在Release 时被去掉,从而成为一个空方法:

func printLog<T>(_ message: T, file: String = #file, method: String = #function, line: Int = #line) {
    #if DEBUG
    print("\((file as NSString).lastPathComponent)[\(line)], \(method): \(message)")
    #endif
}

新版本的LLVM 编译器在遇到这个空方法时,甚至会直接将这个方法整个去掉,完全不去调用它,从而实现零成本