[譯] Swift 裡的強制 @inline 註解

iWeslie發表於2019-05-11

Swift 中的 @inline 註解是一個含糊不清的東西,你在 Apple 的文件中是找不到它的,它並不能幫助你編寫更清晰的程式碼,也沒有任何目的性,它的存在只是為了幫助編譯器做出優化的決策,但它同時也與你的 App 的效能的有很大關係。

在程式設計中,函式內聯 是一種編譯器優化技術,它通過使用方法的內容替換直接呼叫該方法,就相當於假裝該方法並不存在一樣,這種做法在很大程度上優化了效能。

例如,請看一下程式碼:

func calculateAndPrintSomething() {
    var num = 1
    num *= 10
    num /= 5
    print("我的數字:\(num)")
}

print("準備列印一些數字")
calculateAndPrintSomething()
print("完成")
複製程式碼

假設 calculateAndPrintSomething() 沒有在其他任何地方使用過,很明顯該方法不需要存在於編譯後的二進位制檔案中,它存在的目的只是為了使你更加易於閱讀。

通過使用函式內聯,Swift 編譯器可以通過將呼叫這個方法替換為呼叫它裡面的具體內容,從而消除那些不必要的開銷:

// 上面的示例轉化為編譯後二進位制版本
print("準備列印一些數字")
var num = 1
num *= 10
num /= 5
print("我的數字:\(num)")
print("完成")
複製程式碼

基於你選擇的優化級別,這個過程由 Swift 編譯器自動完成的,通過支援內聯來優化速度(-O),或者 進行內聯來優化二進位制包的大小(-Osize),因為內聯一個經常呼叫且內容很多的方法會導致大量的重複程式碼和更大的二進位制包。

儘管編譯器可以自己進行內聯,不過你還是可以 Swift 中使用 @inline 註解來 強制 內聯,它有兩種用法:

@inline(__always):如果可以的話,指示編譯器始終內聯方法。

@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(_:) 方法的程式集會發生什麼:

[譯] Swift 裡的強制 @inline 註解

如果我是一個黑客試圖破解這個 App 的訂閱,看看 play(_:) 方法我就知道 isUserSubscribed(_:) 返回的布林值控制著 App 的訂閱。

我現在可以通過僅查詢 isUserSubscribed(_:) 並強制它返回 true 就可以解鎖 App 的全部高階內容:

[譯] Swift 裡的強制 @inline 註解

在這種情況下,可能因為該方法在 App 裡廣泛使用,所以編譯器決定不內聯它。這種決定就造成了一個安全漏洞,使得 App 能夠很容易地被逆向工程破解。

現在看看給 isUserSubscribed(_ :) 新增了 @inline(__always) 後會發生什麼:

@inline(__always) func isUserSubscribed() -> Bool {
    // 一些很複雜的驗證邏輯
    return true
}

func play(song: Song) {
	if isUserSubscribed() {
        // 播放歌曲
    } else {
        // 讓使用者訂閱
    }
}
複製程式碼

[譯] Swift 裡的強制 @inline 註解

同樣的 play(_:) 方法裡現在不包括對訂閱狀態的判斷。這個方法呼叫完全被其內部的 “複雜的驗證” 所取代,這樣反編譯後看起來變得更加複雜,訂閱也更加難以破解。

好處是,由於每次呼叫 isUserSubscribed(_:) 都被複雜的驗證取代,因此就沒有一種方法可以解鎖應用程式的整個訂閱,黑客現在必須破解每一個進行驗證的方法。當然,多處的重複的程式碼也意味著我們的二進位制檔案會變得更大。

請注意,使用 @inline(__always) 並不能保證編譯器會真正內聯你的方法。它的規則是未知的,例如在無法避免動態派發的情況下就無法進行內聯。

還有什麼?

由於 @inline 沒有得到官方支援,你真的不應該在實際的專案中使用它,這篇文章使用它的目的只是為了學習新東西。

但是我確實發現它非常有用,希望 Apple 決定在某一天正式支援它。如果你對 Swift 中一些模糊的概念感興趣,請檢視 Swift 的原始碼

你可以在 Twitter 上關注我 @rockthebruno,如果你有任何建議也歡迎分享。

參考文獻和一些好的讀物

如果發現譯文存在錯誤或其他需要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可獲得相應獎勵積分。文章開頭的 本文永久連結 即為本文在 GitHub 上的 MarkDown 連結。


掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章