Swift 的坑:static var 的初始化時機並不確定

戴倉薯發表於2019-02-08

背景

昨天大年初三,晚上 11 點半,突然被我的同事金司機敲了一下:“幫我看看這個實驗吧,我們們還一起 review 的……”

難道這個實驗能出問題?公司的 code review 制度做得很完善,每一行程式碼都要過 review ;而我同事這段還專門放到 review 會上,七八個腦袋湊在一起盯著看過的。這還能有問題嗎?

但資料不會說謊:明明開給使用中文的使用者的實驗,從資料上看卻湧入了大批的美國使用者。

倉鼠:“從程式碼上看真的完全沒問題。”

同事:“可能在美國確實有很多使用中文的人。出乎意料啊!”

倉鼠:“這個肯定有問題的,美國是中國的4倍……真是這樣,我們們可以解放美帝了……”

一起看了兩個多小時,終於發現了問題所在,原來是被 Swift 坑了。讓我們來看看程式碼是怎麼寫的吧!

問題

出現問題的相關程式碼可以大概簡化如下:

class StaticVarTestClass {
  static var myStaticVar: Bool = {
    print("initializing static var!")
    return false
  }()
}

class SomeOtherClass {
    static func foo(myEnum: SomeEnum) {
        print("before!")
        switch myEnum {
            case a: 
                print("into case a!")
                print(StaticVarTestClass.myStaticVar)
            case b: break
        }
        print("after!")
    }
}

// 呼叫的時候
print("call foo!")
SomeOtherClass.foo(.b)

複製程式碼

上面這段程式碼,應該輸出什麼結果呢?

研究

要解答這個問題,需要先弄明白 static var 的初始化時機。Swift 官方文件是這麼說的:

Stored type properties are lazily initialized on their first access. They are guaranteed to be initialized only once, even when accessed by multiple threads simultaneously, and they do not need to be marked with the lazy modifier.

簡單翻譯下:人家文件裡說了,static var 是在第一次訪問的時候懶載入的,不需要寫 lazy 關鍵字也能保證是懶載入。而且能保證即使是多執行緒訪問,都會只初始化一次(所以可以用來做單例)。順帶提醒一個小知識,非 static 的 lazy var 是不能保證多執行緒訪問下只初始化一次的。

這不是說得很明白了嗎?那麼從我們的程式碼看,呼叫 foo 的時候,傳死了一個 .b,因此根本走不到 .a 的分支裡,也就不會訪問到 myStaticVar,也就不會觸發它的懶載入。因此,console 輸出應該是:

call foo!
before!
after!
複製程式碼

實際上,debug 編譯的輸出結果的確是這樣的。但是,release 編譯的輸出結果出人意料:

call foo!
initializing static var!
before!
after!
複製程式碼

release 下,如果把斷點打在 print("into case a!") 上,斷點不會停。如果打在 print("initializing static var!") 上,斷點是會停的,並且順著呼叫棧往上找,能找到停在 print(StaticVarTestClass.myStaticVar) 這一句,儘管程式碼的實際執行根本就沒走進 case a 這個分支。

這是什麼情況,說好的懶載入呢?但執行結果擺在眼前,況且線上收集到的大量資料也證明了這個殘酷的現實:static var 的懶載入並不是那麼靠譜,實際上,它的初始化時機並不確定。文件裡所說的“懶載入”只能大概理解成,它不會在 app 一啟動就全面載入,不用擔心效能。但是在我們遇到的這個 case 下,實際路徑並沒有訪問到這個變數;只是調到了 foo 這個函式,而這個函式裡涉及到了它。可能是一個編譯的優化,它先把可能用到的 static var 給預備好了,這就觸發了 static var 的初始化。

如果用我上面提供的程式碼寫個 demo,很可能會發現並不能重現這樣的結果,我和金司機分別寫的 demo 都沒有重現;但是在我們的工程裡是穩定重現的,而實際工程非常複雜,不知道是哪一點觸發了這個問題。沒處說理,只能記住這個血的教訓:可以不用擔心 static var 會影響 app 的冷啟動效能,但是業務邏輯上不能依賴它的懶載入特性,它是有可能在沒訪問到的情況下預先載入的。

題外話

倉鼠本來已經對公司的推薦獎死心了,但是今年意外成功了一次,因此賺外快的心又蠢蠢欲動;容我在我的文章結尾打個小廣告:Airbnb 北京辦公室今年大量招聘 iOS、Android 工程師~ 幾個常見 Q&A:

Q: 崗位有什麼要求嗎?

A: 硬性要求的話,就是大體 3 年以上的 iOS/Android 開發經驗就可以~ 然後我覺得有大中廠的經歷或者畢業學校比較好,以上兩者有其一簡歷會比較容易過~

Q: 對英語有要求嗎?

A: 零要求!只要寫程式碼不寫拼音就好……

Q: 面試都面啥?

A: 實際工作 90% swift ,但面試不需要會。我特別喜歡這公司 iOS 面試的一點,就是完全不考知識性問題,像“訊息機制分為哪幾步”……從電話面試到現場面試,所有環節全都是上機寫程式碼,而且題目全都是開發工作中常見的功能、解決工程中實際出現的 bug,沒有任何偏難怪的東西。我們主要會看開發的熟練度、程式碼結構的設計、解決問題的能力。

Q: 薪資怎麼樣?福利呢?

A: 我猜測薪水能至少跟 BAT 差不多,另外還給不少股票。公司年年嚷著明年上市,但確實還是比較有希望要上市的。福利方面,幾個比較給力的:15 天年假 + 一週左右聖誕假;高階醫療保險,三甲醫院特需、國際部隨便看,生孩子好像和睦家全包,公司直付;入職總部培訓三週,之後還有不少出差機會,可以順便去舊金山玩。其他就跟正常大公司差不多了。

Q: 有沒有機會 transfer 到總部?

A: 有機會,但不保證。

Q: 我想報名 / 可以推薦朋友嗎?

A: 私信我吧!我會告訴你郵箱,然後發一份簡歷(中文即可)給我就好~ 推薦朋友如果成功的話,我也不許諾啥電子產品了,給您轉一萬人民幣現金好了您想買什麼自己買 :)

還有任何問題,倉鼠都很願意幫忙解答~ 可以加我的 QQ 群倉鼠洞:546948320 在北京的朋友也歡迎過來辦公室玩!只要提前跟我約好就好啦~

相關文章