Swift中的禁止@inline属性

最初发布于 swiftrocks.com

@inline属性是Swift中那些晦涩的事物之一-在苹果的文档中找不到它,它不能帮助您编写更@inline代码,也无济于事,只能帮助编译器做出优化决策,但这与一个非常重要的问题有关。应用性能的一个方面。

在编程中, 函数内联是一种编译器优化技术,它通过直接用方法的内容替换对某些方法的调用来消除调用某些方法的开销,基本上是假装该方法根本就不存在。 这极大地提高了性能。

例如,考虑以下代码:

 func calculateAndPrintSomething() { 
var num = 1
num *= 10
num /= 5
print("My number: \(num)")
}
print("I'm going to do print some number") calculateAndPrintSomething()
print("Done!")

假定在其他任何地方都没有使用calculateAndPrintSomething() ,那么很明显,该方法不需要存在于已编译的二进制文件中,其目的只是为了使代码更易于阅读。

通过函数内联,Swift编译器可以通过将其替换为内容来消除调用无用方法的开销:

 //The compiled binary version of the above example 
print("I'm going to do print some number")
var num = 1
num *= 10
num /= 5
print("My number: \(num)")
print("Done!")

根据您的优化级别,这是由Swift编译器自动完成的-支持对速度进行优化( -O )进行内联;如果对二进制大小进行优化( -Osize )则对不进行内联,因为对一个长-Osize方法进行了内联处理的地方将导致重复的代码和更大的二进制文件。

即使编译器可以做出自己的内联决策, @inline属性也可以在Swift中用于强制其决策。 它可以以两种方式使用:

@inline(__always) :如果可能的话, @inline(__always)编译器始终内联该方法。

@inline(never)@inline(never)编译器从不内联该方法。

现在,您可能会问: 什么时候做这个好主意?

根据苹果工程师的说法,答案基本上是永远不会。 即使该属性可供公众使用并已在Swift源代码中广泛使用,但尚未正式支持该属性供公众使用。 根本就没打算公开它,而是引用Jordan Rose: 下划线是有原因的。 如果使用它,可能会出现许多已知和未知问题。

但是由于该属性可以公开使用,所以我决定为了学习新知识而尝试使用它-实际上,我发现该属性在iOS项目中有用的情况。

编译器将根据项目的优化设置做出内联决策,但是在某些情况下,您可能希望方法违背优化设置。 在这些情况下, @inline可用于实现用户的目标。

例如,在优化速度时,似乎编译器倾向于内联甚至不短的方法,从而导致二进制大小增加。 在这种情况下, @inline(never)可以用于防止内联特定的广泛使用的long方法,同时仍然专注于快速二进制文件。

另一个更实际的示例是,您可能希望将一种方法隐藏在可能包含某些敏感信息的黑客面前,无论它会使您的代码变慢还是变大。 您可以尝试使代码更难以理解,也可以使用诸如SwiftShield之类的混淆工具,但是@inline(__always)可以在不损害代码的情况下实现这一目标。 我在下面详细说明了这个例子。

使用@inline(__always)混淆高级内容

假设我们的应用程序中有音乐播放器,并且某些操作仅适用于高级用户。 isUserSubscribed(_:)方法验证用户订阅并返回布尔值,说明是否已订阅用户:

 func isUserSubscribed() -> Bool { 
//Some very complex validation
return true
}
 func play(song: Song) { 
if isUserSubscribed() {
//Play the song
} else {
//Ask user to subscribe
}
}

这对我们的代码非常有用,但是看看如果我反汇编此应用程序并搜索play(_:)方法的程序集会发生什么:

如果我是一个试图破解该应用程序订阅的黑客,那么我必须了解一下play(_:)方法,才能意识到名为isUserSubscribed(_:)的布尔值正在控制该应用程序的订阅。

现在,仅查找isUserSubscribed(_:)并强制其返回true即可解锁应用程序的全部高级内容:

在这种情况下,可能是因为该方法在应用程序中广泛使用,因此编译器决定不对其进行内联。 这个天真的决定造成了一个安全漏洞,该漏洞使该应用可以快速进行反向工程。

现在看看将@inline(__always)应用于isUserSubscribed(_:)时会发生什么:

 @inline(__always) func isUserSubscribed() -> Bool { 
//Some very complex validation
return true
}
 func play(song: Song) { 
if isUserSubscribed() {
//Play the song
} else {
//Ask user to subscribe
}
}

现在,同一play(_:)方法的程序集没有明显的订阅引用! 该方法调用已完全替换为其中发生的“复杂验证”,这使程序集看起来更加隐秘,并且订阅也很难破解。

另外,由于每次对isUserSubscribed(_:)调用都被验证替换,因此没有一种方法可以解锁应用程序的整个订阅-黑客现在必须破解执行验证的每个方法。 当然,这也意味着我们的二进制文件越来越大,因为现在到处都有重复的代码。

请注意,使用@inline(__always)不能保证编译器实际上可以内联您的方法。 它的规则是未知的,并且在某些情况下无法进行内联,例如在无法避免动态调度的情况下。

还有什么?

由于@inline不受官方支持,因此您绝对不应在实际项目中使用它,而仅出于学习新知识的目的而使用本文。

但是,我发现它非常有用,希望苹果决定有一天正式支持它。 如果您对更晦涩的Swift内容感兴趣,请查看Swift的源代码。

参考文献和优秀读物

内联函数
[swift-users] @inline线程