在本文会使用swift底层探索 01 - Swift类初始化&类结构提到的
sil
的方式来进行探索
获取sil文件
- 从
swift
文件到可执行文件.o
的整个编译过程。 - swift编译过程参考
在当前文件路径下使用该命令:
// 单纯转换sil
swiftc -emit-sil main.swift > ./main.sil
// 反解sil中混淆的字符串
xcrun swift-demangle s4main1tAA10TeachModelCvp
// 完整版
swiftc -emit-sil `文件名`.swift | xcrun swift-demangle > `文件名`.sil
- sil文件相当于OC探索中的
cpp
文件,sil
、cpp
都是编译之后的产物 -
sil语法官方文档,阅读
sil
可以更加深刻的理解swift的一些内部机制。对于学习swift很有帮助。
获取ast抽象语法树
swiftc -dump-ast main.swift ast抽象语法树
- 这是在
sil
的上一步生成的文件,主要是做一些语法、词法的分析。
Swift的属性分为:
- 存储属性
- 计算属性
- 属性观察者(didSet、willSet)
- 延迟存储属性
- 类型属性
1. 存储属性:
可以保存各类信息的属性,需要占用内存空间
。
- 根据对象内存分布可以证明
存储属性分为
- 常量存储属性,及
let
- 变量存储属性,及
var
class TeachModel{
let age:Int = 18
var name:String = "Henry"
}
sil文件,这部分需要对照观察
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是一个结构体,而结构体是值类型
。
class TeachModel{
var name:String{
get{
return "Henry"
}
set{
print(newValue)
}
}
}
sil文件
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中会不会触发属性观察者
答案是不一定
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
中的懒加载
思想来理解。使用时才进行加载
,可以优化类的创建过程。
class TeachModel{
lazy var age : Int = 18
}
- 用关键字
lazy
来进行表示
- 用关键字
- 在
第一次使用时
才进行初始化
- 在
sil文件
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
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
。
单例
-
线程安全 + 只进行一次初始化
;这不就是单例
吗~~
class Teacher{
//1、使用 static + let 创建声明一个实例对象
static let shareInstance = Teacher.init()
//2、给当前init添加private访问权限
private init(){ }
}
//使用(只能通过单例,不能通过init)
var t = CJLTeacher.shareInstance
-
swift的单例
相比于OC的单例要简单很多
网友评论