Swift 的坑:static var 的初始化时机并不确定

·  阅读 3170

背景

昨天大年初三,晚上 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 在北京的朋友也欢迎过来办公室玩!只要提前跟我约好就好啦~

分类:
iOS
标签:
分类:
iOS
标签: