swift底层探索 02 - 属性swift底层探索 02 - 属性

2021-08-09 11:28:26 浏览数 (1)

在本文会使用swift底层探索 01 - Swift类初始化&类结构提到的sil的方式来进行探索

获取sil文件

  • swift文件到可执行文件.o的整个编译过程。
  • swift编译过程参考

在当前文件路径下使用该命令:

代码语言:javascript复制
// 单纯转换sil
swiftc -emit-sil main.swift > ./main.sil
// 反解sil中混淆的字符串
xcrun swift-demangle s4main1tAA10TeachModelCvp
// 完整版
swiftc -emit-sil `文件名`.swift | xcrun swift-demangle > `文件名`.sil 
  • sil文件相当于OC探索中的cpp文件,silcpp都是编译之后的产物
  • sil语法官方文档,阅读sil可以更加深刻的理解swift的一些内部机制。对于学习swift很有帮助。
获取ast抽象语法树
代码语言:javascript复制
swiftc  -dump-ast main.swift ast抽象语法树
  • 这是在sil的上一步生成的文件,主要是做一些语法、词法的分析。

Swift的属性分为:

  • 存储属性
  • 计算属性
  • 属性观察者(didSet、willSet)
  • 延迟存储属性
  • 类型属性
1. 存储属性:

可以保存各类信息的属性,需要占用内存空间

  • 根据对象内存分布可以证明
存储属性分为
  • 常量存储属性,及let
  • 变量存储属性,及var
代码语言:javascript复制
class TeachModel{
    let age:Int = 18
    var name:String = "Henry"
}
sil文件,这部分需要对照观察
代码语言:javascript复制
class TeachModel {
  @_hasStorage @_hasInitialValue final let age: Int { get }
  @_hasStorage @_hasInitialValue var name: String { get set }
  @objc deinit
  init()
}
  • let修饰的变量在编译之后会增加一个final修饰符,表明常量存储属性是不可继承的.
  • var修饰的变量有get,set方法。而let修饰的变量只有get方法,没有set方法直接印证了let是不可修改的.
2. 计算属性:

计算属性的本质就是get、set方法,并不占用内存

  • 并没有在内存中找到具体的String值

String在swift中是一个字面量,及将String值存在内存中String是一个结构体,而结构体是值类型

代码语言:javascript复制
class TeachModel{
    var name:String{
        get{
            return "Henry"
        }
        set{
            print(newValue)
        }
    }
}
sil文件
代码语言:javascript复制
class TeachModel {
  var name: String { get set }
  @objc deinit
  init()
}
  • 相比于存储属性少了2个关键字:@_hasStorage ,@_hasInitialValue.
  • 声明了get,set方法。
  • get方法的sil实现
3. 属性观察者(willSet、didSet)

作用可以简单的理解为oc中的KVO,区别是使用更加简单,但也有自己的一些规则.

  • willSet:新值存储之前调用. 内建变量newValue
  • didSet:新值存储之后调用. 内建变量OldValue
  • 在你使用属性观察者(willSet、didSet)之后,在编译阶段会在set方法中增加调用这两个方法的代码。当然这些都是编译器完成的,不需要我们再去进行额外的操作。
在使用过程中有几个问题:
1. 在init中会不会触发属性观察者

答案是不一定

代码语言:javascript复制
class CJLTeacher{
    var name: String = "测试"{
        //新值存储之前调用
        willSet{
            print("willSet newValue (newValue)")
        }
        //新值存储之后调用
        didSet{
            print("didSet oldValue (oldValue)")
        }
    }
    
    init() {
        self.name = "CJL"
    }
}
  • 事实证明在init方法中不会触发属性观察者
  • 因为在初始化过程中内存中的对应地址可能是脏的,获取oldvalue可能会造成问题
  • 【反例】但是在子类的init中调用会触发属性观察者,因为在子类中已经完成了父类的内存布局已经age的内存布局,所以可以触发属性观察者
2. 子类和父类同时存在didset、willset时,其调用顺序
  • 调用顺序:子类的willSet->父类的wilSet->父类的didSet->子类的didset
4. 延迟存储属性-lazy

可以对比oc中的懒加载思想来理解。使用时才进行加载,可以优化类的创建过程。

代码语言:javascript复制
class TeachModel{
    lazy var age : Int = 18
}
    1. 用关键字lazy来进行表示
    1. 第一次使用时才进行初始化
sil文件
代码语言:javascript复制
class TeachModel {
  lazy var age: Int { get set } //计算属性
  @_hasStorage @_hasInitialValue final var $__lazy_storage_$_age: Int? { get set }  //存储属性
  @objc deinit
  init()
}
  • 加了lazy在编译之后,编译器会添加对应的计算属性,已经可选类型的存储属性。这样会导致对象的内存大小发生变化.

可选类型是一个enum 关联值(当前类型). 结果:内存占用需要在Int(8字节) enum(1字节) -> 字节对齐 (16字节)

sil文件中get方法的实现

  • get方法简单理解: 第一次使用时,变量内存为空,调用get方法时,进行初始化。后续使用则直接返回内存中的值.
  • set方法简单理解: 将新值包装为可选类型。保证变量数据类型的一致。
无法保证线程安全

在查看sil过程中并没有发现线程锁之类的代码。所以在get方法的switch判断那存在多线程问题,一定概率会出现多次初始化的情况.

5. 类型属性static
代码语言:javascript复制
class TeachModel{
    //声明
    static var age : Int = 18
}
//使用
TeachModel.age = 20

类型属性,主要有以下几点说明:

使用关键字static修饰,且是一个全局变量

查看sil文件

  • 定义为全局变量
  • 全局初始化的时候就完成了唯一一次初始化,并不需要依赖类对象的初始化.
  • 因为需要定义到全局,所以一定要提供初始化值.
线程安全
  • 发现会调用build once。可这个build once是什么呢?
  • 通过xcode汇编调试,会发现调用了swift_once
  • 打开源码搜索swift_once,在Once.cpp文件中发现了具体实现。发现调用了熟悉的dispathch_once_f
单例
  • 线程安全 只进行一次初始化;这不就是单例吗~~
代码语言:javascript复制
class Teacher{
    //1、使用 static   let 创建声明一个实例对象
    static let shareInstance = Teacher.init()
    //2、给当前init添加private访问权限
    private init(){ }
}

//使用(只能通过单例,不能通过init)
var t = CJLTeacher.shareInstance
  • swift的单例相比于OC的单例要简单很多

0 人点赞