[譯] 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前端後端區塊鏈產品設計人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索