本文介绍了swift中最基本的属性。
- 存储属性和计算属性。
- 类型属性及其联想到的单例设计。
- 通过Mach-O文件查找验证属性在类中的结构体
存储属性
存储属性非常普遍,分别用var和let修饰的常量或变量。常量赋值后不可修改。
- 存储属性必须设置初始值,或者在初始化器里赋值。否则编译器会报错。
- 类和结构体都能设置存储属性,枚举不行。
let和var定义的存储属性有啥区别?
let age = 18
var name = "name"
汇编分析: 没啥区别都是存到寄存器中。
LLDB分析:全局变量所在区。变量和常量也只是个内存地址。
SIL分析: let不生成set方法,也不能手动生成。
class YFTeacher {
@_hasStorage @_hasInitialValue final let age: Int { get }
@_hasStorage @_hasInitialValue var name: String { get set }
@objc deinit
init()
}
计算属性
类、结构体和枚举也能够定义计算属性,计算属性并不存储值,他们提供 getter 和 setter 来修改和获取值。对于存储属性来说可以是常量或变量,但计算属性必须定义为变量。于此同时我们书写计算属性时候必须包含类型,因为编译器需要知道期望返回值是什么。
通过观察SIL代码发现,set方法有默认入参newValue
当然set入参也可以自己指定。在拥有返回值的方法里,如果方法内部只有 return,那么可以直接省略 return。比如计算属性的get方法。
class Square {
var width: Double = 10.0
var area: Double {
get {
// 表达式只有 return 这行 可以省略关键字。
width * width
}
// 默认入参newValue,也可以指定
set(newArea) {
self.width = newArea
}
}
}
- 只读计算属性
只提供或者只暴露 get 方法。如果area属性去掉set方法,赋值报错:
Cannot assign to property: 'area' is a get-only property
如果area声明前加上private (set) 私有化get,赋值报错变成:
Cannot assign to property: 'area' setter is inaccessible
-
结构体属性的get/set是静态调用,不存储在V-Table。 因为在上文代码的SIL中,class有sil_vtable,改成struct则没有。
-
计算属性本质是set、get方法。
在SIL中,属性width的声明@_hasStorage关键字,而计算属性area则没有。这代表计算属性不占内存。观察两者的get方法也能发现区别,width.getter返回的是%4寄存器的内容,而%4从%3的Double类型指针从内存读取而来。area.getter则没有load操作。
class Square {
@_hasStorage @_hasInitialValue var width: Double { get set }
var area: Double { get set }
@objc deinit
init()
}
// Square.width.getter
sil hidden [transparent] @$s4main6SquareC5widthSdvg : $@convention(method) (@guaranteed Square) -> Double {
// %0 "self" // users: %2, %1
bb0(%0 : $Square):
debug_value %0 : $Square, let, name "self", argno 1 // id: %1
%2 = ref_element_addr %0 : $Square, #Square.width // user: %3
%3 = begin_access [read] [dynamic] %2 : $*Double // users: %4, %5
%4 = load %3 : $*Double // user: %6
end_access %3 : $*Double // id: %5
return %4 : $Double // id: %6
} // end sil function '$s4main6SquareC5widthSdvg'
// Square.area.getter
sil hidden @$s4main6SquareC4areaSdvg : $@convention(method) (@guaranteed Square) -> Double {
// %0 "self" // users: %5, %4, %3, %2, %1
bb0(%0 : $Square):
debug_value %0 : $Square, let, name "self", argno 1 // id: %1
%2 = class_method %0 : $Square, #Square.width!getter : (Square) -> () -> Double, $@convention(method) (@guaranteed Square) -> Double // user: %3
%3 = apply %2(%0) : $@convention(method) (@guaranteed Square) -> Double // user: %6
%4 = class_method %0 : $Square, #Square.width!getter : (Square) -> () -> Double, $@convention(method) (@guaranteed Square) -> Double // user: %5
%5 = apply %4(%0) : $@convention(method) (@guaranteed Square) -> Double // user: %7
%6 = struct_extract %3 : $Double, #Double._value // user: %8
%7 = struct_extract %5 : $Double, #Double._value // user: %8
%8 = builtin "fmul_FPIEEE64"(%6 : $Builtin.FPIEEE64, %7 : $Builtin.FPIEEE64) : $Builtin.FPIEEE64 // user: %9
%9 = struct $Double (%8 : $Builtin.FPIEEE64) // user: %10
return %9 : $Double // id: %10
} // end sil function '$s4main6SquareC4areaSdvg'
因为不占内存,只起到中转功能,个人理解为方法的中间商。
属性观察者
属性观察者会观察用来观察属性值的变化,一个willSet当属性将被改变调用,即使这个值与原有的值相同,而didSet在属性已经改变之后调用。
- 初始化期间设置属性时不会调用观察者
观察以下代码截图发现,初始化后的实例赋新值时才会调用观察者。
通过sil再来证明这一点:init的时候没出现willset和didset方法调用。store %0 to %6代表直接属性地址里存值。
// Subject.init(_:)
sil hidden @$s4main7SubjectCyACSScfc : $@convention(method) (@owned String, @owned Subject) -> @owned Subject {
// %0 "name" // users: %9, %7, %4, %2
// %1 "self" // users: %5, %10, %3
bb0(%0 : $String, %1 : $Subject):
debug_value %0 : $String, let, name "name", argno 1 // id: %2
debug_value %1 : $Subject, let, name "self", argno 2 // id: %3
retain_value %0 : $String // id: %4
%5 = ref_element_addr %1 : $Subject, #Subject.subjectName // user: %6
%6 = begin_access [modify] [dynamic] %5 : $*String // users: %7, %8
store %0 to %6 : $*String // id: %7
end_access %6 : $*String // id: %8
release_value %0 : $String // id: %9
return %1 : $Subject // id: %10
} // end sil function '$s4main7SubjectCyACSScfc'
可能因为初始化的时候,不是所有属性和方法都已经生成完整了,避免出错。
- 继承属性观察期的调用顺序:子类willSet -> 父类willSet-> 父类didSet -> 子类didSet 继承后代码调用如图:
延迟存储属性
- 用
lazy来表示延迟存储属性 - lazy 属性必须是 var,因为 let 必须在实例的初始化方法完成之前就赋值。
- 如果多条线程同时第一次访问 lazy 属性,无法保证属性只被初始化 1 次。
- 延迟存储属性的初始值在其第一次使用时才进行计算 在第一次赋值前看一下实例内存情况。class的metadata和refCount各占8字节,可以看到图中第三个8字节数值是0,代表18还没赋值进去。
但是内存大小不变,已经预占用了。SIL角度下就是可选属性,并且有final关键字:
class Subject {
lazy var age: Int { get set }
@_hasStorage @_hasInitialValue final var $__lazy_storage_$_age: Int? { get set }
@objc deinit
init()
}
那么,可选属性get方法是怎么实现的呢?
如图所示,Optional.some和Optional.none分别代表有没有初始化的情况,没有的时候走bb2分支,此时才有%10 = integer_literal $Builtin.Int64, 18初始化,以及后续store的存储操作。最后都有br bb3,代表通过bb3来返回值。
类型属性
类和实例都可以拥有存储属性和计算属性。默认是实例的,通过static关键字声明为类型属性,适用于结构体和类。在类里面还可以使用class关键字。
- 类型属性,只能通过类去访问。例如 Person.age
- 类型属性只会被初始化一次,本质是全局变量
举例并查看sil代码:
class Subject {
// 最大课时
static var maxTimes: Int = 100
var name: String = "课程名称"
}
let maxTimes = Subject.maxTimes
class Subject {
@_hasStorage @_hasInitialValue static var maxTimes: Int { get set }
@_hasStorage @_hasInitialValue var name: String { get set }
@objc deinit
init()
}
@_hasStorage @_hasInitialValue let maxTimes: Int { get }
// one-time initialization token for maxTimes
sil_global private @$s4main7SubjectC8maxTimes_Wz : $Builtin.Word
// static Subject.maxTimes
sil_global hidden @$s4main7SubjectC8maxTimesSivpZ : $Int
// maxTimes
sil_global hidden [let] @$s4main8maxTimesSivp : $Int
可以看到one-time initialization token for maxTimes意味着只初始化一次,sil_global代表全局。还原一下这个混淆的方法名:
MacBook-Pro:~ mbp$ xcrun swift-demangle s4main7SubjectC8maxTimesSivpZ
$s4main7SubjectC8maxTimesSivpZ ---> static main.Subject.maxTimes : Swift.Int
继续观察sil里main函数,看看如何调用maxTimes的。
图中圈起来的Subject.maxTimes.unsafeMutableAddressor方法里可以发现可疑的builtin "once",字面意思编译一次,在sil没搜到相关解释。为了进一步证明,将SIL代码降级为IR代码。
swiftc -emit-ir ${SRCROOT}/SwiftDemo/main.swift > ./main.ll && open main.ll
在main.ll文件搜索上文的混淆方法名 s4main7SubjectC8maxTimesSivpZ 可以看到@swift_once这个关键字,可能是swift初始化一次的意思。
once_done: ; preds = %once_not_done, %entry
%3 = load i64, i64* @"$s4main7SubjectC8maxTimes_Wz", align 8
%4 = icmp eq i64 %3, -1
call void @llvm.assume(i1 %4)
ret i8* bitcast (%TSi* @"$s4main7SubjectC8maxTimesSivpZ" to i8*)
once_not_done: ; preds = %entry
call void @swift_once(i64* @"$s4main7SubjectC8maxTimes_Wz", i8* bitcast (void ()* @"$s4main7SubjectC8maxTimes_WZ" to i8*), i8* undef)
br label %once_done
}
那就继续在swift源码中找呗!
原来是通过swift_once方法调GCD。这不就是熟悉的OC单例具体实现嘛,那么swift单例怎么写?
Swift单例
之前参照OC的思路实现swift单例,发现不行:
从目前掌握的情况看,static代表全局,let代表不可变。能不能组合一下?再考虑到类本身可以通过继承实现指定初始化器,那么将指定初始化器私有化,杜绝外界访问。
class Subject {
static let sharedInstance = Subject()
// 指定初始化器私有化,杜绝外界访问。
private init(){}
}
Subject.sharedInstance
类型方法
类型方法属于静态派发。尝试打开debug模式下的swift函数内联优化了,发现没有符号栈调用,断点竟然没生效就结束。
- class和static修饰方法时的区别: class不能修饰struct里的方法。 class修饰的方法会注册在vtable里,同时也是静态派发。
sil_vtable Subject {
#Subject.testClass: (Subject.Type) -> () -> () : @$s4main7SubjectC9testClassyyFZ // static Subject.testClass()
#Subject.init!allocator: (Subject.Type) -> () -> Subject : @$s4main7SubjectCACycfC // Subject.__allocating_init()
#Subject.deinit!deallocator: @$s4main7SubjectCfD // Subject.__deallocating_deinit
}
属性在Maco-O里的位置
属性描述的结构体查找
上篇文章 已经找到类的描述信息TargetClassDescriptor,其父类TargetTypeContextDescriptor有个属性Fields是FieldDescriptor类型,通过源码注释可知这是属性描述的指针。
template <typename Runtime>
class TargetTypeContextDescriptor
: public TargetContextDescriptor<Runtime> {
public:
/// 源码节选
/// A pointer to the field descriptor for the type, if any.
TargetRelativeDirectPointer<Runtime, const reflection::FieldDescriptor,
/*nullable*/ true> Fields;
不难搜到,而且类、结构体和枚举都能声明。
整理结构如下:
struct FieldDescriptor {
MangledTypeName int32
Superclass int32
Kind uint16
FieldRecordSize uint16
NumFields uint32
FieldRecords [FieldRecord]
}
struct FieldRecord{
Flags uint32
MangledTypeName int32
FieldName int32
}
Flags是用来标记的,可以看到能返回是不是enum和var等。
Maco-O文件查找过程
通过一个简单的示例代码查找
class Person{
var height: Double = 1.80
var name: String = "swifter"
}
class ViewController: UIViewController{
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
let p = Person()
print(p.name)
}
}
计算TargetClassDescriptor在内存的起始地址得到 A0D8,地址起点对应下图绿框
// 要减去虚拟内存基地址
BC7C + FFFFE45C - 100000000 = A0D8
回顾一下,FieldDescriptor是TargetClassDescriptor的属性,偏移16字节,地址起点对应上图红框。加上得到BC0C
A0E0 + 4 + 4 + 1B24 = BC0C
上文FieldDescriptor结构体中,FieldRecords属性需要偏移5个属性共16字节,而FieldRecords是数组,每个元素FieldRecord结构体包含3个字段(Flags、MangledTypeName、FieldName),所以每12字节代表一个FieldRecord。
要通过FieldName找到属性名称,计算第一个身高属性height的名称地址:BBD3。找到对应地址能看到值68 65 69 67,注意看Value这栏显示
BC1C + 4 + 4 + FFFFFFAF = 0x10000BBD3
10000BBC3 - 100000000 = BBD3
以上就是查找流程