[译] Swift 里的强制 @inline 注解

Swift 中的 @inline 注解是一个含糊不清的东西,你在 Apple 的文档中是找不到它的,它并不能帮助你编写更清晰的代码,也没有任何目的性,它的存在只是为了帮助编译器作出优化的决策,但它同时也与你的 App 的性能的有很大关系。html

在编程中,函数内联 是一种编译器优化技术,它经过使用方法的内容替换直接调用该方法,就至关于伪装该方法并不存在同样,这种作法在很大程度上优化了性能。前端

例如,请看一下代码:android

func calculateAndPrintSomething() {
    var num = 1
    num *= 10
    num /= 5
    print("个人数字:\(num)")
}

print("准备打印一些数字")
calculateAndPrintSomething()
print("完成")
复制代码

假设 calculateAndPrintSomething() 没有在其余任何地方使用过,很明显该方法不须要存在于编译后的二进制文件中,它存在的目的只是为了使你更加易于阅读。ios

经过使用函数内联,Swift 编译器能够经过将调用这个方法替换为调用它里面的具体内容,从而消除那些没必要要的开销:git

// 上面的示例转化为编译后二进制版本
print("准备打印一些数字")
var num = 1
num *= 10
num /= 5
print("个人数字:\(num)")
print("完成")
复制代码

基于你选择的优化级别,这个过程由 Swift 编译器自动完成的,经过支持内联来优化速度(-O),或者 进行内联来优化二进制包的大小(-Osize),由于内联一个常常调用且内容不少的方法会致使大量的重复代码和更大的二进制包。github

尽管编译器能够本身进行内联,不过你仍是能够 Swift 中使用 @inline 注解来 强制 内联,它有两种用法:编程

@inline(__always):若是能够的话,指示编译器始终内联方法。swift

@inline(never):指示编译器永不内联方法。后端

如今你可能会问:到底怎么选择呢?安全

根据苹果工程师的说法,答案基本上是 never。尽管该属性可用于公共或普遍使用的 Swift 源代码,但它尚未正式支持公共使用。它历来没有打算过要公开,Jordan Rose 也曾说到:设定它不被公开是有缘由的。 若是你要使用它,可能会出现许多已知和未知的问题。

但因为该属性能够公开使用,为了学习新的东西,我会去尝试一下它,并且我实际上发现了这个注解在 iOS 项目中一些很实用的地方。

编译器将根据项目的优化设置作出内联决策,但在某些状况下,你可能须要一种方法来手动决策。这时 @inline 就能够帮助到你。

例如,在优化速度时,彷佛编译器会对一些内容并非很短的方法进行内联,从而致使二进制大小增长。在这种状况下,@inline(never) 可用于防止这个,同时保证二进制文件的速度。

另外一个更实际的例子是,你可能想防止黑客接触到一个包含某种敏感信息的方法,它是否会使代码变慢或包变大都可有可无。你确定会尝试混淆你的代码来使代码更难理解,或者能够选择混淆工具,例如 SwiftShield,但 @inline(__always) 能够轻松实现这一点而同时不会损害你的代码,我将在下面详细介绍了这个例子:

使用 @inline(__always) 来混淆订阅的部分

假设咱们的 App 中有一个音乐播放器,其中部分操做只有开通了高级版才能使用。isUserSubscribed(_:) 方法能够返回一个布尔值以查看用户是否订阅了高级版:

func isUserSubscribed() -> Bool {
    // 一些很复杂的验证逻辑
    return true
}

func play(song: Song) {
	if isUserSubscribed() {
        // 播放歌曲
    } else {
        // 让用户订阅
    }
}
复制代码

这对咱们的代码很是重要,但若是咱们把这个 App 进行反编译并搜索 play(_:) 方法的程序集会发生什么:

若是我是一个黑客试图破解这个 App 的订阅,看看 play(_:) 方法我就知道 isUserSubscribed(_:) 返回的布尔值控制着 App 的订阅。

我如今能够经过仅查找 isUserSubscribed(_:) 并强制它返回 true 就能够解锁 App 的所有高级内容:

在这种状况下,可能由于该方法在 App 里普遍使用,因此编译器决定不内联它。这种决定就形成了一个安全漏洞,使得 App 可以很容易地被逆向工程破解。

如今看看给 isUserSubscribed(_ :) 添加了 @inline(__always) 后会发生什么:

@inline(__always) func isUserSubscribed() -> Bool {
    // 一些很复杂的验证逻辑
    return true
}

func play(song: Song) {
	if isUserSubscribed() {
        // 播放歌曲
    } else {
        // 让用户订阅
    }
}
复制代码

一样的 play(_:) 方法里如今不包括对订阅状态的判断。这个方法调用彻底被其内部的 “复杂的验证” 所取代,这样反编译后看起来变得更加复杂,订阅也更加难以破解。

好处是,因为每次调用 isUserSubscribed(_:) 都被复杂的验证取代,所以就没有一种方法能够解锁应用程序的整个订阅,黑客如今必须破解每个进行验证的方法。固然,多处的重复的代码也意味着咱们的二进制文件会变得更大。

请注意,使用 @inline(__always) 并不能保证编译器会真正内联你的方法。它的规则是未知的,例如在没法避免动态派发的状况下就没法进行内联。

还有什么?

因为 @inline 没有获得官方支持,你真的不该该在实际的项目中使用它,这篇文章使用它的目的只是为了学习新东西。

可是我确实发现它很是有用,但愿 Apple 决定在某一天正式支持它。若是你对 Swift 中一些模糊的概念感兴趣,请查看 Swift 的源代码

你能够在 Twitter 上关注我 @rockthebruno,若是你有任何建议也欢迎分享。

参考文献和一些好的读物

若是发现译文存在错误或其余须要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可得到相应奖励积分。文章开头的 本文永久连接 即为本文在 GitHub 上的 MarkDown 连接。


掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 AndroidiOS前端后端区块链产品设计人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏

相关文章
相关标签/搜索