Swift类型属性底层研究

我们研究过成员属性的一些具体实现细节,本文我们来研究下类型属性的底层逻辑。

基本语法

  • 类型属性的语法和成员属性类似的地方包括:可以定义存储属性和计算属性,也可以添加存储属性监听器
struct Sequence {
    static var first: Int = 1 // 存储属性
    static var second: Int {  // 计算属性
        get {
            return first
        }
        set {
            first = newValue
        }
    }
    static var third: Int = 3 { // 存储添加属性监听器
        didSet {
            print("third didSet")
        }
        willSet {
            print("third willSet")
        }
    }
}

区别是类型属性要用static进行修饰

  • 类型属性不能用lazy修饰,因为类型属性默认就是already-lazy global
不能用lazy修饰

swift_once

实现分析

类型属性默认是懒加载,我们来看看底层的实现逻辑。

<!-- 测试代码 -->
struct Sequence {
    static var first: Int = 1
}

func test() {
    Sequence.first = 2
}

test()
  • 获取Sequence.first的内存地址:Sequence.first.unsafeMutableAddressor
Sequence.first.unsafeMutableAddressor
  • 如果地址不存在,利用swift_once进行变量的初始化
swift_once
  • swift_once底层调用的是dispatch_once_f
dispatch_once_f

我们得知:编译器会封装一个初始化函数,作为dispatch_once_ffn参数进行初始化调用

  • fn函数封装
// one-time initialization function for first
sil private [global_init_once_fn] @$s4main8SequenceV5first_WZ : $@convention(c) () -> () {
bb0:
  alloc_global @$s4main8SequenceV5firstSivpZ      // id: %0
  %1 = global_addr @$s4main8SequenceV5firstSivpZ : $*Int // user: %4
  %2 = integer_literal $Builtin.Int64, 1          // user: %3
  %3 = struct $Int (%2 : $Builtin.Int64)          // user: %4
  store %3 to %1 : $*Int                          // id: %4
  %5 = tuple ()                                   // user: %6
  return %5 : $()                                 // id: %6
} 

通过SIL分析,我们得知:编译器会封装一个初始化函数,大体的实现逻辑是:

  • 得到变量的内存地址, 类似于:var ptr = withUnsafePointer(to: &Sequence.first) { $0 }
  • 将1赋值给这个内存地址上,类似于:ptr.pointee = 2
总结
  • 编译器会将var first: Int = 1封装成一个函数,函数体是先获取变量指针,然后给指针所指的内存地址赋值为初始值
  • 类型属性底层是通过dispatch_once_f进行初始化,确保只会初始化一次,并且是线程安全的

全局变量

从图一的编译器错误提示我们可以得知,类型属性本质就是全局变量,只是有访问权限限定。

let zero: Int = 0

struct Sequence {
    static var first: Int = 1
}

我们利用实例代码进行分析。

SIL分析
@_hasStorage @_hasInitialValue var zero: Int { get set }

struct Sequence {
  @_hasStorage @_hasInitialValue static var first: Int { get set }
  init()
}

// zero
sil_global hidden @$s4main4zeroSivp : $Int

// static Sequence.first
sil_global hidden @$s4main8SequenceV5firstSivpZ : $Int

我们看到SIL语法中,全局变量和类型属性的定义是完全相同的。

内存验证
func test() {
    let ptr1 = withUnsafePointer(to: &zero) { UnsafeRawPointer($0) }
    let ptr2 = withUnsafePointer(to: &Sequence.first) { UnsafeRawPointer($0) }
    print("\(ptr1) \(ptr2)")
}
// 0x100008000 0x100008008

通过查看内存地址,我们得到的结果是zeroSequence.first的内存地址是连续挨在一起的。zero肯定是全局变量,所以Sequence.first本质上也是一个全局变量。

全局变量的更多用法

既然类型属性是全局变量,那全局变量应该也可以是计算属性等。其实确实也是可以这样写的:

var zero: Int = 0
var one: Int {
    get {
        zero
    }
    set {
        zero = newValue
    }
}
var two: Int = 2 {
    willSet {
        
    }
    didSet {
        
    }
}

全局变量的语法和类型属性的语法也是一致的。

变量内存安全(参考地址

前面我们看到了类型属性本质是通过swift_once得到了变量内存地址指针。Swift编译器可以(也仅仅只有编译器可以)获取到全局变量的内存地址指针。

为什么需要获取变量的内存地址指针呢?这涉及到内存安全的部分

Swift会保证同时访问同一块内存时不会冲突,通过约束代码里对于存储地址的写操作,去获取那一块内存的访问独占权。避免了读写冲突。

变量内存安全是通过swift_beginAccessswift_endAccess等方法类控制的。

变量内存安全
swift_beginAccess
swift_beginAccess
AccessSet::insert

逻辑总结:

  1. 先将内存指针封装成Access对象
  2. Access对象的封装的内存指针如果在SwiftTLSContext::get().accessSet数组中不存在,说明目前没有其他方法占用该内存地址,可以访问,并且将Access对象保存起来;
  3. Access对象的封装的内存指针如果在SwiftTLSContext::get().accessSet数组中存在,说明该内存地址已经有访问存在了,如果所有的访问都是读访问,则不认为是冲突,可以继续访问,否则就会报访问冲突错误。
swift_endAccess
swift_endAccess

逻辑总结:
将当前的访问从SwiftTLSContext::get().accessSet数组中移除,也就是将本次内存访问移除。

总结

  • 类型属性本质上是全局变量,只是访问权限有所限制
  • 类型属性和全局变量可以是存储属性,计算属性,也可以添加属性监听器,但是不能添加懒加载的lazy关键字
  • 类型属性是懒加载的,通过dispatch_once_f进行, 确保只会初始化一次,并且是线程安全的
  • 编译器对类型属性和全局变量添加了内存安全的控制,避免了访问的读写冲突,使代码更加安全
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,948评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,371评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,490评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,521评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,627评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,842评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,997评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,741评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,203评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,534评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,673评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,339评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,955评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,770评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,000评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,394评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,562评论 2 349

推荐阅读更多精彩内容