Swift探索(三): 属性

一. 存储属性

1. 存储属性定义

存储属性是一个作为特定类和结构体实例一部分的常量或变量。存储属性要么是变量存储属性 (由 var 关键字引入)要么是常量存储属性(由 let 关键字引入)。

var age = 18
let name = "小明"
  • var 用来声明变量,变量的值可以在将来设置为不同的值。
  • let 用来声明常量,常量的值一旦设置好便不能再被更改

在创建类或结构体的实例时,必须为所有的存储属性设置一个初始值。可以在初始化器( init )里为存储属性设置一个初始值,可以分配一个默认的属性值作为定义的一部分。

struct PersonStruct {
    var age: Int = 10
    let name: String = "小明"
}

class PersonClass {
    var age: Int
    let name: String

    init(_ age: Int, _ name: String) {
        self.age = age
        self.name = name
    }
}

let person = PersonStruct()
let person1 = PersonClass(18, "小明")

2. 从SIL查看let和var的区别

var age = 18
let name = "小明"

生成sil文件后可以看到如下代码

@_hasStorage @_hasInitialValue var age: Int { get set }

@_hasStorage @_hasInitialValue let name: String { get }

通过 sil 我们可以发现

  • var 修饰的属性有 getset 方法
  • let 修饰的属性只有 get 方法,所有 let 修饰的属性不能修改。

二. 计算属性

1. 计算属性定义

计算属性并不存储值,他们提供 gettersetter 来修改和获取值。对于存储属性来说可以是常量或变量,但计算属性必须定义为变量。于此同时我们书写计算属性时候必须包含类型,因为编译器需要知道期望返回值是什么

struct square {
    var width: Double
    var area: Double {
        get {
            width * width
//            return width * width
        }
        set {
            self.width = newValue
        }
//        set (newArea) {
//            self.width = newArea
//        }
    }
}

重写一个实例的 settergetter 可以用 setget 来修饰,setter 传入新的默认值叫做 newValue,也可以自定义。Swift 中,在拥有返回值的方法里,如果方法内部是单表达式,那么可以直接省略 return

2. 只读计算属性

2.1 方式一
struct square {
    var width: Double = 30.0
    var area: Double {
        get {
            width * width
        }
    }
    let height: Double = 20.0
}
2.2 方式二
struct square {
    var width: Double = 30.0
    private(set) var area : Double
    let height: Double = 20.0
}
2.3 两种方式的区别

我们来看一看 square 结构体在 sil 中如何声明的:

// 方式一
struct square {
  @_hasStorage @_hasInitialValue var width: Double { get set }
  var area: Double { get }
  @_hasStorage @_hasInitialValue let height: Double { get }
  init()
  init(width: Double = 30.0)
}

// 方式二
struct square {
  @_hasStorage @_hasInitialValue var width: Double { get set }
  @_hasStorage private(set) var area: Double { get set }
  @_hasStorage @_hasInitialValue let height: Double { get }
  init(width: Double = 30.0, area: Double)
}

我们可以看到方式二中的 area 实际上是存储属性,只是 set 方法被私有化了。而方式一中的 area没有任何修饰,只有get方法。

3. 计算属性本质

我们来查看一下完整代码的sil文件

struct square {
    var width: Double
    var area: Double {
        get {
            width
        }
        set {
            self.width = newValue
        }
    }
}

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        var s = square(width: 10.0)
        s.width = 20.0
        s.area = 30.0
        let w = s.width
        let a = s.area
    }
}

通过 swiftc ViewController.swift -emit-silViewController.swift 编译成 sil 文件

// sil 文件中 square 的声明
struct square {
  @_hasStorage var width: Double { get set }
  var area: Double { get set }
  init(width: Double)
}

我们可以看到存储属性 width 和计算属性 area 都跟有 { get set } ,那么就说明他们都是有 settergetter 方法的。我们接着往下看他们具体

3.1 width 属性的 getter 和 setter 在 sil 的内部实现

// square.width.getter
sil hidden [transparent] @$s4main6squareV5widthSdvg : $@convention(method) (square) -> Double {
// %0 "self"                                      // users: %2, %1
bb0(%0 : $square):
  debug_value %0 : $square, let, name "self", argno 1 // id: %1
  %2 = struct_extract %0 : $square, #square.width // user: %3
  return %2 : $Double                             // id: %3
} // end sil function '$s4main6squareV5widthSdvg'

// square.width.setter
sil hidden [transparent] @$s4main6squareV5widthSdvs : $@convention(method) (Double, @inout square) -> () {
// %0 "value"                                     // users: %6, %2
// %1 "self"                                      // users: %4, %3
bb0(%0 : $Double, %1 : $*square):
  debug_value %0 : $Double, let, name "value", argno 1 // id: %2
  debug_value_addr %1 : $*square, var, name "self", argno 2 // id: %3
  %4 = begin_access [modify] [static] %1 : $*square // users: %7, %5
  %5 = struct_element_addr %4 : $*square, #square.width // user: %6
  store %0 to %5 : $*Double                       // id: %6
  end_access %4 : $*square                        // id: %7
  %8 = tuple ()                                   // user: %9
  return %8 : $()                                 // id: %9
} // end sil function '$s4main6squareV5widthSdvs'

可以发现

  • 在调用 widthgetter 方法的时候,可以发现是直接获取 squarewidth 的值进行返回。
  • 在调用 widthsetter 方法的时候,是直接获取到 squarewidth 的地址,对地址所指向的内容进行修改。

3.2 area 属性的 getter 和 setter 在 sil 的内部实现

// square.area.getter
sil hidden @$s4main6squareV4areaSdvg : $@convention(method) (square) -> Double {
// %0 "self"                                      // users: %2, %1
bb0(%0 : $square):
  debug_value %0 : $square, let, name "self", argno 1 // id: %1
  %2 = struct_extract %0 : $square, #square.width // user: %3
  return %2 : $Double                             // id: %3
} // end sil function '$s4main6squareV4areaSdvg'

// square.area.setter
sil hidden @$s4main6squareV4areaSdvs : $@convention(method) (Double, @inout square) -> () {
// %0 "newValue"                                  // users: %6, %2
// %1 "self"                                      // users: %4, %3
bb0(%0 : $Double, %1 : $*square):
  debug_value %0 : $Double, let, name "newValue", argno 1 // id: %2
  debug_value_addr %1 : $*square, var, name "self", argno 2 // id: %3
  %4 = begin_access [modify] [static] %1 : $*square // users: %7, %5
  %5 = struct_element_addr %4 : $*square, #square.width // user: %6
  store %0 to %5 : $*Double                       // id: %6
  end_access %4 : $*square                        // id: %7
  %8 = tuple ()                                   // user: %9
  return %8 : $()                                 // id: %9
} // end sil function '$s4main6squareV4areaSdvs'

可以发现跟 width 属性的 gettersetter 完全一样。但是我们发现并没有 area 相关的存储变量。所以其实,计算属性根本不会有存储在实例的成员变量,那也就意味着计算属性不占用内存。

3.3 在汇编中计算属性的实现

将上面的代码编译成汇编代码,并在 s.width = 20.0 打上断点

计算属性的汇编代码.png

我们可以看到计算属性 area 的赋值和取值的本质就是 settergetter 方法。而存储属性 width 则是在进行一系列的 movstr 操作。

三. 属性观察者

1. 属性观察者定义

属性观察者用来观察属性值的变化, willSet 当属性将被改变调用,即使这个值与原有的值相同,而 didSet 在属性已经改变之后调用。它们的语法类似于 gettersetter

class Person{
    // 存储属性
    var  name: String = ""{
        willSet {
            print("name will set value \(newValue)")
        }
        didSet {
            print("name did set value \(oldValue)")
        }
    }
}

let p = Person()
p.name = "小明"

// 打印结果
name will set value 小明
name did set value 

我们通过 sil来看 willSetdidset是怎么被调用的

// Person.name.setter
sil hidden @$s4main6PersonC4nameSSvs : $@convention(method) (@owned String, @guaranteed Person) -> () {
// %0 "value"                                     // users: %22, %16, %12, %11, %2
// %1 "self"                                      // users: %20, %13, %11, %4, %3
bb0(%0 : $String, %1 : $Person):
  debug_value %0 : $String, let, name "value", argno 1 // id: %2
  debug_value %1 : $Person, let, name "self", argno 2 // id: %3
  %4 = ref_element_addr %1 : $Person, #Person.name // user: %5
  %5 = begin_access [read] [dynamic] %4 : $*String // users: %6, %8
  %6 = load %5 : $*String                         // users: %21, %9, %20, %7
  retain_value %6 : $String                       // id: %7
  end_access %5 : $*String                        // id: %8
  debug_value %6 : $String, let, name "tmp"       // id: %9
  // function_ref Person.name.willset
  %10 = function_ref @$s4main6PersonC4nameSSvw : $@convention(method) (@guaranteed String, @guaranteed Person) -> () // user: %11
  %11 = apply %10(%0, %1) : $@convention(method) (@guaranteed String, @guaranteed Person) -> ()
  retain_value %0 : $String                       // id: %12
  %13 = ref_element_addr %1 : $Person, #Person.name // user: %14
  %14 = begin_access [modify] [dynamic] %13 : $*String // users: %16, %15, %18
  %15 = load %14 : $*String                       // user: %17
  store %0 to %14 : $*String                      // id: %16
  release_value %15 : $String                     // id: %17
  end_access %14 : $*String                       // id: %18
  // function_ref Person.name.didset
  %19 = function_ref @$s4main6PersonC4nameSSvW : $@convention(method) (@guaranteed String, @guaranteed Person) -> () // user: %20
  %20 = apply %19(%6, %1) : $@convention(method) (@guaranteed String, @guaranteed Person) -> ()
  release_value %6 : $String                      // id: %21
  release_value %0 : $String                      // id: %22
  %23 = tuple ()                                  // user: %24
  return %23 : $()                                // id: %24
} // end sil function '$s4main6PersonC4nameSSvs'

我们可以看到在 Person.namesetter 方法中在赋值操作之前调用了 willSet 方法,然后进行赋值操作,最后再调用 didSet 方法。

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

在初始化期间设置属性时不会调用 willSetdidSet 观察者,只有在为完全初始化的实例分配新值时才会调用它们。

class Person{
    var  name: String = "unnamed"{
        willSet {
            print("name will set value \(newValue)")
        }
        didSet {
            print("name did set value \(oldValue)")
        }
    }
    
    init(name: String) {
        self.name = name
    }
}

let p = Person.init(name: "小明")

运行上述代码,可以发现没有任何打印结果

初始化方法.png

可以发现在初始化方法里是拿到 Person.name 属性的内存地址,然后将字符串直接拷贝到 Person.name 属性的内存地址中,并没有调用 getset 方法,是因为 Person.name 还没有初始化完成。

3. 观察者对计算属性的观察

上面的属性观察者只是对存储属性起作用,如果我们想对计算属性起作用怎么办?

class Square {
    var width: Double
    var area: Double {
        get {
            return width * width
        }
        set {
            print("do something \(newValue)")
            self.width = sqrt(newValue)
            print("do something \(oldValue)")
        }
    }
    init(width: Double) {
        self.width = width
    }
}

如果我们想对计算属性设置值之前对他进行操作,只需将相关代码添加到属性的 setter 方法中。对于计算属性我们没有必要给他添加 willSetdidSet,只会画蛇添足。

4. 在继承中的属性观察者

class Person {
    var age: Int {
        willSet {
            print("age will set value \(newValue)")
        }
        didSet {
            print("age did set value \(oldValue)")
        }
    }
    var height: Double
    
    init(_ age: Int, _ height: Double) {
        self.age = age
        self.height = height
    }
}

class subPerson :Person {
    override var age: Int {
        willSet {
            print("override age will set value \(newValue)")
        }
        didSet {
            print("override age did set value \(oldValue)")
        }
    }
    
    var name: String
    init(name: String) {
        self.name = name
        super.init(18, 185.0)
        self.age = 20
    }
}

let p = subPerson.init(name: "小明")

// 打印结果
override age will set value 20
age will set value 20
age did set value 18
override age did set value 18

我们可以看出对于继承关系的属性他的调用顺序是先调用子类的 willSet,再调用父类的 willSet,再进行赋值操作,再调用父类的 didSet,最后调用子类的 didSet。值得注意的是打印结果是在执行 self.age = 20 这段代码的时候打印的,因为上面提到过初始化期间设置属性时不会调用观察者,因此这里只会打印一次。

subPerson.age的setter方法的sil文件.png

通过 sil 文件我们也可以看出 subPerson.agesetter 方法中的调用顺序是先调用 subPerson.agewillSet 方法,在调用 Person.agesetter 方法,在调用 subPerson.agedidSet 方法。

四. 延迟存储属性

1. 延迟存储属性定义

延时加载存储属性是指当第一次被调用的时候才会计算其初始值的属性。在属性声明前使用 lazy 来标示一个延时加载存储属性。

class Person {
    lazy var age: Int = 18
}

var p = Person()

print(p.age)

print("end")

我们通过观察 p 的内存来观察延迟存储属性 age

p的内存.png

这个时候,可以看到 p.age 还没有调用时在 metadata + refcount 后面的内存空间是没有值的,接着我们在 print("end") 打个断点,让 age 访问到,并打印p 的内存
p的内存2.png

我们可以看到当 age 第一次被访问的时候这个内存空间有了值。

2. 延迟存储属性在sil中的实现

为了简化 sil 文件,将上述代码替换成

class Person {
    lazy var age: Int = 18
}

var p = Person()

var t = p.age
class Person {
  lazy var age: Int { get set }
  @_hasStorage @_hasInitialValue final var $__lazy_storage_$_age: Int? { get set }
  @objc deinit
  init()
}

通过在 sil 文件中 age 的声明我们可以看出延迟存储属性是可选值,因此在 age 还没有加载时为空,在第一次加载后有值。

// variable initialization expression of Person.$__lazy_storage_$_age
sil hidden [transparent] @$s4main6PersonC21$__lazy_storage_$_age029_12232F587A4C5CD8B1EEDF696793G2FCLLSiSgvpfi : $@convention(thin) () -> Optional<Int> {
bb0:
  %0 = enum $Optional<Int>, #Optional.none!enumelt // user: %1
  return %0 : $Optional<Int>                      // id: %1
} // end sil function '$s4main6PersonC21$__lazy_storage_$_age029_12232F587A4C5CD8B1EEDF696793G2FCLLSiSgvpfi'

$__lazy_storage_$_age 初始化表达式中可以看到默认的空值是 #Optional.none ,就是一个空值。

// Person.age.getter
sil hidden [lazy_getter] [noinline] @$s4main6PersonC3ageSivg : $@convention(method) (@guaranteed Person) -> Int {
// %0 "self"                                      // users: %14, %2, %1
bb0(%0 : $Person):
  debug_value %0 : $Person, let, name "self", argno 1 // id: %1
  %2 = ref_element_addr %0 : $Person, #Person.$__lazy_storage_$_age // user: %3
  %3 = begin_access [read] [dynamic] %2 : $*Optional<Int> // users: %4, %5
  %4 = load %3 : $*Optional<Int>                  // user: %6
  end_access %3 : $*Optional<Int>                 // id: %5
  switch_enum %4 : $Optional<Int>, case #Optional.some!enumelt: bb1, case #Optional.none!enumelt: bb2 // id: %6

// %7                                             // users: %9, %8
bb1(%7 : $Int):                                   // Preds: bb0
  debug_value %7 : $Int, let, name "tmp1"         // id: %8
  br bb3(%7 : $Int)                               // id: %9

bb2:                                              // Preds: bb0
  %10 = integer_literal $Builtin.Int64, 18        // user: %11
  %11 = struct $Int (%10 : $Builtin.Int64)        // users: %18, %13, %12
  debug_value %11 : $Int, let, name "tmp2"        // id: %12
  %13 = enum $Optional<Int>, #Optional.some!enumelt, %11 : $Int // user: %16
  %14 = ref_element_addr %0 : $Person, #Person.$__lazy_storage_$_age // user: %15
  %15 = begin_access [modify] [dynamic] %14 : $*Optional<Int> // users: %16, %17
  store %13 to %15 : $*Optional<Int>              // id: %16
  end_access %15 : $*Optional<Int>                // id: %17
  br bb3(%11 : $Int)                              // id: %18

// %19                                            // user: %20
bb3(%19 : $Int):                                  // Preds: bb2 bb1
  return %19 : $Int                               // id: %20
} // end sil function '$s4main6PersonC3ageSivg'

agegetter 方法中可以看到,首先执行 bb0代码块,在 bb0 中首先获取到 #Person.$__lazy_storage_$_age 的地址,并把内存地址的值读取到寄存器 %4,然后对 %4 进行枚举模式匹配,如果有值就走 bb1 的代码块,没有值就走 bb2 代码块。bb1中的具体操作就是直接返回原有的值。bb2中的具体的操作就是将 18 存储到 #Person.$__lazy_storage_$_age 的内存地址当中,并返回这个值。

注意
如果一个被标记为 lazy 的属性在没有初始化时就同时被多个线程访问,则无法保证该属性只会被初始化一次。

五. 类型属性

1. 类型属性的定义

实例属性属于一个特定类型的实例,每创建一个实例,实例都拥有属于自己的一套属性值,实例之间的属性相互独立。你也可以为类型本身定义属性,无论创建了多少个该类型的实例,这些属性都只有唯一一份。这种属性就是类型属性。
类型属性其实就是一个全局变量,并且只会被初始化一次。

class Person {
    // 多线程访问时也只会被初始化一次
    static var age: Int = 18
}

Person.age = 20

2. 类型属性的本质

我们通过 swiftc main.swift -emit-sil 命令将 main.swift 编译成 sil 文件来查看

class Person {
  @_hasStorage @_hasInitialValue static var age: Int { get set }
  @objc deinit
  init()
}

// one-time initialization token for age
sil_global private @$s4main6PersonC3age_Wz : $Builtin.Word

// static Person.age
sil_global hidden @$s4main6PersonC3ageSivpZ : $Int

我们可以看到 age 属性的声明只是多了一个 staic 关键字。 在这下方我们可以看到多了一个全局变量 s4main6PersonC3age_Wz,并且 Person.age 属性也变成了一个全局变量。

// main
sil @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 {
bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>):
  %2 = metatype $@thick Person.Type
  // function_ref Person.age.unsafeMutableAddressor
  %3 = function_ref @$s4main6PersonC3ageSivau : $@convention(thin) () -> Builtin.RawPointer // user: %4
  %4 = apply %3() : $@convention(thin) () -> Builtin.RawPointer // user: %5
  %5 = pointer_to_address %4 : $Builtin.RawPointer to [strict] $*Int // user: %8
  %6 = integer_literal $Builtin.Int64, 20         // user: %7
  %7 = struct $Int (%6 : $Builtin.Int64)          // user: %9
  %8 = begin_access [modify] [dynamic] %5 : $*Int // users: %9, %10
  store %7 to %8 : $*Int                          // id: %9
  end_access %8 : $*Int                           // id: %10
  %11 = integer_literal $Builtin.Int32, 0         // user: %12
  %12 = struct $Int32 (%11 : $Builtin.Int32)      // user: %13
  return %12 : $Int32                             // id: %13
} // end sil function 'main'

在初始化的时候我们可以看到调用了一个函数 Person.age.unsafeMutableAddressor 就是内存地址的访问,接下来我们定位到这个函数

// Person.age.unsafeMutableAddressor
sil hidden [global_init] @$s4main6PersonC3ageSivau : $@convention(thin) () -> Builtin.RawPointer {
bb0:
  %0 = global_addr @$s4main6PersonC3age_Wz : $*Builtin.Word // user: %1
  %1 = address_to_pointer %0 : $*Builtin.Word to $Builtin.RawPointer // user: %3
  // function_ref one-time initialization function for age
  %2 = function_ref @$s4main6PersonC3age_WZ : $@convention(c) () -> () // user: %3
  %3 = builtin "once"(%1 : $Builtin.RawPointer, %2 : $@convention(c) () -> ()) : $()
  %4 = global_addr @$s4main6PersonC3ageSivpZ : $*Int // user: %5
  %5 = address_to_pointer %4 : $*Int to $Builtin.RawPointer // user: %6
  return %5 : $Builtin.RawPointer                 // id: %6
} // end sil function '$s4main6PersonC3ageSivau'

在这个函数中,显示获取到最开始 Person 声明下方多出的全局变量 s4main6PersonC3age_Wz 的地址,然后转换成 Builtin.RawPointer 类型,然后又调用了一个函数 @$s4main6PersonC3age_WZ : $@convention(c) () -> () ,接着执行 builtin "once",最后返回的是全局变量的内存地址。接着定位到 @$s4main6PersonC3age_WZ : $@convention(c) () -> () 这个函数当中。

// one-time initialization function for age
sil private [global_init_once_fn] @$s4main6PersonC3age_WZ : $@convention(c) () -> () {
bb0:
  alloc_global @$s4main6PersonC3ageSivpZ          // id: %0
  %1 = global_addr @$s4main6PersonC3ageSivpZ : $*Int // user: %4
  %2 = integer_literal $Builtin.Int64, 18         // 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
} // end sil function '$s4main6PersonC3age_WZ'

这个函数当中,首先创建全局变量 s4main6PersonC3ageSivpZ 也就是我们的 age,接着获取这个全局变量的内存地址,接着构建 18 并且将 18 存放到全局变量的内存地址中。这个函数也就是在初始化 age 变量。
在上一步中 builtin "once"是在干什么呢?我们通过 swiftc main.swift -emit-ir 命令将 main.swift 编译成 IR 文件来查看

define hidden swiftcc i8* @"$s4main6PersonC3ageSivau"() #0 {
entry:
  %0 = load i64, i64* @"$s4main6PersonC3age_Wz", align 8
  %1 = icmp eq i64 %0, -1
  %2 = call i1 @llvm.expect.i1(i1 %1, i1 true)
  br i1 %2, label %once_done, label %once_not_done

once_done:                                        ; preds = %once_not_done, %entry
  %3 = load i64, i64* @"$s4main6PersonC3age_Wz", align 8
  %4 = icmp eq i64 %3, -1
  call void @llvm.assume(i1 %4)
  ret i8* bitcast (%TSi* @"$s4main6PersonC3ageSivpZ" to i8*)

once_not_done:                                    ; preds = %entry
  call void @swift_once(i64* @"$s4main6PersonC3age_Wz", i8* bitcast (void ()* @"$s4main6PersonC3age_WZ" to i8*), i8* undef)
  br label %once_done
}

s4main6PersonC3ageSivau.png

通过 xcrun swift-demangle s4main6PersonC3ageSivau命令我们可以看到 s4main6PersonC3ageSivau 就是在 sil 文件中的 Person.age.unsafeMutableAddressor 函数。那么在这里面我们可以看到有一个 @swift_once,可能就是我们在 sil 中看到的 builtin "once",我们去 Swift源码 中查看 @swift_once 到底干了什么。

/// Runs the given function with the given context argument exactly once.
/// The predicate argument must point to a global or static variable of static
/// extent of type swift_once_t.
void swift::swift_once(swift_once_t *predicate, void (*fn)(void *),
                       void *context) {
#ifdef SWIFT_STDLIB_SINGLE_THREADED_RUNTIME
  if (! *predicate) {
    *predicate = true;
    fn(context);
  }
#elif defined(__APPLE__)
  dispatch_once_f(predicate, context, fn);
#elif defined(__CYGWIN__)
  _swift_once_f(predicate, context, fn);
#else
  std::call_once(*predicate, [fn, context]() { fn(context); });
#endif
}

Once.cpp文件中我们可以找到这个方法,我们发现这里使用的 GCDdispatch_once。所以类型属性(static修饰的属性)只会被初始化一次。

3. 单例的写法

class Person {
    
    static let sharedInstace = Person()
    
    private init(){}
}

Person.sharedInstace

六. 属性在Mahco文件的位置信息

在之前的文章Swift探索(一): 类与结构体(上)中我们了解到 Swift对象的内存结构 Metadata 和在Swift探索(二): 类与结构体(下) 方法调度探索当中了解到 typeDescriptor 的结构

struct MataData {
  var kind: Int
  var superClass: Any.Type
  var cacheData: (Int, Int)
  var data: Int
  var classFlags: Int32
  var instanceAddressPoint: UInt32
  var instanceSize: UInt32
  var instanceAlignmentMask: UInt16
  var reserved: UInt16
  var classSize: UInt32
  var classAddressPoint: UInt32
  var typeDescriptor: UnsafeMutableRawPointer
  var iVarDestroyer: UnsafeRawPointer
}

struct TargetClassDescriptor {
    var flags: UInt32
    var parent: UInt32
    var name: Int32
    var accessFunctionPointer: Int32
    var fieldDescriptor: Int32
    var superClassType: Int32
    var metadataNegativeSizeInWords: UInt32
    var metadataPositiveSizeInWords: UInt32
    var numImmediateMembers: UInt32
    var numFields: UInt32
    var fieldOffsetVectorOffset: UInt32
    var Offset: UInt32
    var size: UInt32
    //V-Table
}

通过Swift源码我们了解到 Swift对象的属性存储在 fieldDescriptor 当中,并且 fieldDescriptor 的结构体如下

struct FieldDescriptor {
    var MangledTypeName: Int32  //回写之后的类型名称
    var Superclass: Int32 //  父类
    var Kind: uint16 // 标识
    var FieldRecordSize: uint16 // 记录当前的大小
    var NumFields: uint32 //有多少个属性
    var FieldRecords: [FieldRecord] // 每一个属性的具体细节
}

struct FieldRecord {
    var Flags: uint32 //标志位
    var MangledTypeName: Int32 // 这个属性的类型信息
    var FieldName: Int32 // 属性的名称
}

我们通过 MachO 来验证

1. 验证流程

class Person {
    var age = 18
    var name = "小明"
}

前面部分跟在在Swift探索(二): 类与结构体(下) 中查找 v-table 的流程一致,首先Section64(_TEXT,__swift5_types)中存放的就是 Descriptor

typeDescriptor的地址.png

因此得到typeDescriptor的地址为:

0xFFFFFF54 + 0x3F44 = 0x100003E98

虚拟地址的首地址.png

Load CommandsLC_SEGMENT_64(__PAGEZERO) 中可以看到虚拟地址的首地址和大小,因此上一步得到的地址 0x100003E98 减去虚拟内存的首地址 0x100000000 就是当前typeDescriptor 在虚拟内存中的偏移量( offset )。

0x100003E98 - 0x100000000 = 0x3E98

定位到 0x3E98

0x3E98.png

0x3E98 就是 TargetClassDescriptor 这个结构体类的首地址,后面存储的就是相应成员变量的内容,根据前面对源码的分析我们得到了 TargetClassDescriptor结构体中的 fieldDescriptor 前面有 4Int32 类型,也就是 44 字节,于是我们向后偏移 44 字节

向后偏移4个4字节.png

我们可以得到 fieldDescriptorMacho 中的偏移量,于是 fieldDescriptor的地址为

0x3EA8 + 0x74 = 0x3F1C

定位到 0x3F1C

0x3F1C.png

根据前面的分析我们知道 FieldDescriptor结构体中的 FieldRecord 要向后偏移16个字节,并且 FieldRecord 占用 12 个字节,于是可以看出 Section64(__TEXT,__swift5_fieldmd) 内就是存储的 fieldDescriptor ,因此
fieldDescriptor.png

那么 fieldRecords1FieldName 的地址和fieldRecords2FieldName 的地址就分别是

// fieldRecords1的fieldName
0x3F2C + 4 + 4 + 0xFFFFFFDF =  0x100003F13
// 减去虚拟内存地址
0x100003F13  - 0x100000000 =  0x3F13

// fieldRecords2的fieldName
0x3F3C  + 4 + 0xFFFFFFD7 =  0x100003F17
// 减去虚拟内存地址
0x100003F17  - 0x100000000 =  0x3F17

定位到 0x3F130x3F17

0x3F13和0x3F17.png

可以看到后面的 Value 就是 agename
通过两篇文章的 MachO 分析我们大致的了解到

  • Section64(__TEXT,__swift5_types) : 存储的 typeDescriptor
  • Section64(__TEXT,__swift5_fieldmd) : 存储的 fieldDescriptor
  • Section64(__TEXT,__swift5_reflstr) : 存储所有的属性名称
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,793评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,567评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,342评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,825评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,814评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,680评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,033评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,687评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,175评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,668评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,775评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,419评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,020评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,206评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,092评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,510评论 2 343

推荐阅读更多精彩内容