[Swift]属性真没啥好卷的

614 阅读5分钟

本文介绍了swift中最基本的属性。

  1. 存储属性和计算属性。
  2. 类型属性及其联想到的单例设计。
  3. 通过Mach-O文件查找验证属性在类中的结构体

存储属性

存储属性非常普遍,分别用var和let修饰的常量或变量。常量赋值后不可修改。

  • 存储属性必须设置初始值,或者在初始化器里赋值。否则编译器会报错。
  • 类和结构体都能设置存储属性,枚举不行。

let和var定义的存储属性有啥区别?

let age = 18
var name = "name"

汇编分析: 没啥区别都是存到寄存器中。 存储属性汇编分析

LLDB分析:全局变量所在区。变量和常量也只是个内存地址。

SWIFT_存储属性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

image-20220111235057150

当然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在属性已经改变之后调用。

  • 初始化期间设置属性时不会调用观察者

观察以下代码截图发现,初始化后的实例赋新值时才会调用观察者。

SWIFT_属性观察初始化

通过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 继承后代码调用如图:

SWIFT_属性观察继承

延迟存储属性

  • lazy来表示延迟存储属性
  • lazy 属性必须是 var,因为 let 必须在实例的初始化方法完成之前就赋值。
  • 如果多条线程同时第一次访问 lazy 属性,无法保证属性只被初始化 1 次。
  • 延迟存储属性的初始值在其第一次使用时才进行计算 在第一次赋值前看一下实例内存情况。class的metadata和refCount各占8字节,可以看到图中第三个8字节数值是0,代表18还没赋值进去。

image-20220112001618381

但是内存大小不变,已经预占用了。SIL角度下就是可选属性,并且有final关键字:

class Subject {
  lazy var age: Int { get set }
  @_hasStorage @_hasInitialValue final var $__lazy_storage_$_age: Int? { get set }
  @objc deinit
  init()
}

那么,可选属性get方法是怎么实现的呢?

image-20220117153842953

如图所示,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的。 image-20220117164323636

图中圈起来的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源码中找呗!

image-20220112010119177

原来是通过swift_once方法调GCD。这不就是熟悉的OC单例具体实现嘛,那么swift单例怎么写?

Swift单例

之前参照OC的思路实现swift单例,发现不行:

image-20220117170018554

从目前掌握的情况看,static代表全局,let代表不可变。能不能组合一下?再考虑到类本身可以通过继承实现指定初始化器,那么将指定初始化器私有化,杜绝外界访问。

class Subject {
    static let sharedInstance = Subject()
    // 指定初始化器私有化,杜绝外界访问。
    private init(){}
}

Subject.sharedInstance

类型方法

类型方法属于静态派发。尝试打开debug模式下的swift函数内联优化了,发现没有符号栈调用,断点竟然没生效就结束。

image-20220117171944447

image-20220117171812950

  • 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有个属性FieldsFieldDescriptor类型,通过源码注释可知这是属性描述的指针。

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;

不难搜到,而且类、结构体和枚举都能声明。 SWIFT_FieldDescriptor

SWIFT_FieldRecord

整理结构如下:

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等。

SWIFT_属性Flags

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)
    }
}

swift_field_macho_1

计算TargetClassDescriptor在内存的起始地址得到 A0D8,地址起点对应下图绿框

// 要减去虚拟内存基地址
BC7C + FFFFE45C - 100000000 = A0D8

swift_field_macho_2

swift_field_macho_3

回顾一下,FieldDescriptor是TargetClassDescriptor的属性,偏移16字节,地址起点对应上图红框。加上得到BC0C

A0E0 + 4 + 4 + 1B24 = BC0C

上文FieldDescriptor结构体中,FieldRecords属性需要偏移5个属性共16字节,而FieldRecords是数组,每个元素FieldRecord结构体包含3个字段(Flags、MangledTypeName、FieldName),所以每12字节代表一个FieldRecord。

swift_field_macho_4

要通过FieldName找到属性名称,计算第一个身高属性height的名称地址:BBD3。找到对应地址能看到值68 65 69 67,注意看Value这栏显示

BC1C + 4 + 4 + FFFFFFAF = 0x10000BBD3
10000BBC3 - 100000000 = BBD3

swift_field_macho_5

以上就是查找流程