在讲值类型和引用类型之前,需要先了解iOS的内存分区。

更多关于iOS内存分区的知识请看这篇文章。
通过一个例子来引入什么是值类型。
func test() {
    var a = 10
    var b = 20
}
test()
查看a和b当前栈区的地址:
(lldb) po withUnsafePointer(to: &a, { $0 } )
▿ 0x000000016d64f7d8
  - pointerValue : 6130300888
(lldb) po withUnsafePointer(to: &b, { $0 } )
▿ 0x000000016d64f7d0
  - pointerValue : 6130300880
查看b的内存情况:
(lldb) x/2g 0x000000016d64f7d0
0x16d64f7d0: 0x0000000000000014 0x000000000000000a
从上面的例子可以看出a比b的地址大了8个字节,因为Int是64 位。从而说明栈空间是连续的从高到低内存空间,而值类型的变量在空间中存储的就是值。
再添加一个变量c,把b赋值给它,然后再修改c的值:
func test() {
    var a = 10
    var b = 20
    var c = b
    c += 10
}
(lldb) v -L
0x000000016b7937d8: (Int) a = 10
0x000000016b7937d0: (Int) b = 20
0x000000016b7937c8: (Int) c = 30
从上面可以看出当c的值改变而b没有改变,因为值在传递的过程中是深拷贝。相当于传递了一份副本,而改变副本并不会影响本体的状态,状态不共享。从地址也可以看出b和c是两个地址,改变其中一个地址状态当然没理由另一个地址跟着改变。
总结一下值类型的特点
1、值类型的地址中存储的是值本身。
2、值类型的传递过程中,通过深拷贝传递了一个副本。
3、值传递过程中,不共享状态。
结构体
在swift中,结构体就是值类型。下面来探究一下结构体。
定义一个结构体:
struct Person {
    var name: String
    var age: UInt
}
编译成SIL文件:
struct Person {
  @_hasStorage var name: String { get set }
  @_hasStorage var age: UInt { get set }
  init(name: String, age: UInt)
}
从上面可以发现结构体内部的变量可以不赋初始值,但是调用初始化方法必须赋值。
那如果赋了初始值怎么样呢?看下面:
struct Person {
    var name: String = "dotry"
    var age: UInt = 26
}
struct Person {
  @_hasStorage @_hasInitialValue var name: String { get set }
  @_hasStorage @_hasInitialValue var age: UInt { get set }
  init(name: String = "dotry", age: UInt = 26)
  init()
}
可以看出赋了初始值编译器会生成两个初始化方法。
再看一下如果手动实现了初始化方法:
struct Person {
    var name: String = "dotry"
    var age: UInt = 26
    
    init(name: String = "dotry", age: UInt) {
        self.name = name
        self.age = age
    }
}
struct Person {
  @_hasStorage @_hasInitialValue var name: String { get set }
  @_hasStorage @_hasInitialValue var age: UInt { get set }
  init(name: String = "dotry", age: UInt)
}
如果有了自定义的初始化方法,此时便不会再生成默认的初始化方法。
接下来分析结构体的内存结构
struct Person {
    var age: UInt = 26
    var name: String = "dotry"
}
var p = Person()
var p1 = p
p1.age = 18
(lldb) po withUnsafePointer(to: &p, { $0 })
▿ 0x00000001046861b8
  - pointerValue : 4368916920
(lldb) po withUnsafePointer(to: &p1, { $0 })
▿ 0x00000001046861d0
  - pointerValue : 4368916944
(lldb) x/6g 0x00000001046861b8
0x1046861b8: 0x000000000000001a 0x0000007972746f64
0x1046861c8: 0xe500000000000000 0x0000000000000012
0x1046861d8: 0x0000007972746f64 0xe500000000000000
可已看出,p的的地址就是p.age的地址,p1的地址就是p1.age的地址。并且对于p1的修改不会影响到p,从而也可以证明结构体就是值类型。
接下来才通过SIL验证:
// Person.init(age:name:)
sil hidden @main.Person.init(age: Swift.UInt, name: Swift.String) -> main.Person : $@convention(method) (UInt, @owned String, @thin Person.Type) -> @owned Person {
// %0 "$implicit_value"                           // user: %3
// %1 "$implicit_value"                           // user: %3
// %2 "$metatype"
bb0(%0 : $UInt, %1 : $String, %2 : $@thin Person.Type):
  %3 = struct $Person (%0 : $UInt, %1 : $String)  // user: %4
  return %3 : $Person                             // id: %4
} // end sil function 'main.Person.init(age: Swift.UInt, name: Swift.String) -> main.Person'
// Person.init()
sil hidden @main.Person.init() -> main.Person : $@convention(method) (@thin Person.Type) -> @owned Person {
// %0 "$metatype"
bb0(%0 : $@thin Person.Type):
  //在栈区开辟一块内存,alloc一个self,即结构体Person
  %1 = alloc_stack $Person, let, name "self"      // users: %12, %4, %16, %17
  //创建一个值为26的Int
  %2 = integer_literal $Builtin.Int64, 26         // user: %3
  %3 = struct $UInt (%2 : $Builtin.Int64)         // users: %5, %14
  //将age的值存入进Person结构体的首地址
  %4 = struct_element_addr %1 : $*Person, #Person.age // user: %5
  store %3 to %4 : $*UInt                         // id: %5
  %6 = string_literal utf8 "dotry"                // user: %11
  %7 = integer_literal $Builtin.Word, 5           // user: %11
  %8 = integer_literal $Builtin.Int1, -1          // user: %11
  %9 = metatype $@thin String.Type                // user: %11
  // function_ref String.init(_builtinStringLiteral:utf8CodeUnitCount:isASCII:)
  %10 = function_ref @Swift.String.init(_builtinStringLiteral: Builtin.RawPointer, utf8CodeUnitCount: Builtin.Word, isASCII: Builtin.Int1) -> Swift.String : $@convention(method) (Builtin.RawPointer, Builtin.Word, Builtin.Int1, @thin String.Type) -> @owned String // user: %11
  %11 = apply %10(%6, %7, %8, %9) : $@convention(method) (Builtin.RawPointer, Builtin.Word, Builtin.Int1, @thin String.Type) -> @owned String // users: %14, %13
  %12 = struct_element_addr %1 : $*Person, #Person.name // user: %13
  store %11 to %12 : $*String                     // id: %13
  %14 = struct $Person (%3 : $UInt, %11 : $String) // users: %18, %15
  retain_value %14 : $Person                      // id: %15
  destroy_addr %1 : $*Person                      // id: %16
  dealloc_stack %1 : $*Person                     // id: %17
  //返回结构体自身
  return %14 : $Person                            // id: %18
} // end sil function 'main.Person.init() -> main.Person'
上面的SIL明确表明结构体是通过alloc_stack在栈区初始化的,并且结构体的地址就是结构体的首个成员变量的地址,后面的变量通过前一个变量地址偏移得到。
mutating
结构体的函数如果要改变自身的值是不允许的:
struct Person {
    var age: UInt = 26
    var name: String = "dotry"
    
    func celebrateBirthday() {
        age += 1    ⚠️Left side of mutating operator isn't mutable: 'self' is immutable
    }
}
此时编译器会报一个Left side of mutating operator isn't mutable: 'self' is immutable的错误。
通过SIL发现,原来self是一个用let修饰的常量。
// Person.celebrateBirthday()
sil hidden @main.Person.celebrateBirthday() -> () : $@convention(method) (@guaranteed Person) -> () {
// %0 "self"                                      // user: %1
bb0(%0 : $Person):
  debug_value %0 : $Person, let, name "self", argno 1 // id: %1
  %2 = tuple ()                                   // user: %3
  return %2 : $()                                 // id: %3
} // end sil function 'main.Person.celebrateBirthday() -> ()'
根据提示在方法前面加上mutating关键字之后,就可以改变自身的值,再通过SIL查看一下mutating做了什么事:
// Person.celebrateBirthday()
sil hidden @main.Person.celebrateBirthday() -> () : $@convention(method) (@inout Person) -> () {
// %0 "self"                                      // users: %3, %1
bb0(%0 : $*Person):
  debug_value_addr %0 : $*Person, var, name "self", argno 1 // id: %1
  %2 = integer_literal $Builtin.Int64, 1          // user: %8
  %3 = begin_access [modify] [static] %0 : $*Person // users: %15, %4
  %4 = struct_element_addr %3 : $*Person, #Person.age // users: %13, %5
  %5 = struct_element_addr %4 : $*UInt, #UInt._value // user: %6
  %6 = load %5 : $*Builtin.Int64                  // user: %8
  %7 = integer_literal $Builtin.Int1, -1          // user: %8
  %8 = builtin "uadd_with_overflow_Int64"(%6 : $Builtin.Int64, %2 : $Builtin.Int64, %7 : $Builtin.Int1) : $(Builtin.Int64, Builtin.Int1) // users: %10, %9
  %9 = tuple_extract %8 : $(Builtin.Int64, Builtin.Int1), 0 // user: %12
  %10 = tuple_extract %8 : $(Builtin.Int64, Builtin.Int1), 1 // user: %11
  cond_fail %10 : $Builtin.Int1, "arithmetic overflow" // id: %11
  %12 = struct $UInt (%9 : $Builtin.Int64)        // user: %13
  store %12 to %4 : $*UInt                        // id: %13
  %14 = tuple ()
  end_access %3 : $*Person                        // id: %15
  %16 = tuple ()                                  // user: %17
  return %16 : $()                                // id: %17
} // end sil function 'main.Person.celebrateBirthday() -> ()'
可以看到此时的self是由var修饰的变量。传递进来的是debug_value_addr地址,不再是debug_value值。所以可以通过地址对self的内部的成员变量进行更改。
inout
刚才在Person的内部更改age的值是通过mutating关键字实现的。如果要在外部可以直接更改吗?
struct Person {
    var age: UInt = 26
    var name: String = "dotry"
    
    mutating func celebrateBirthday() {
        age += 1
    }
}
func celebrateBirthdayOf(_ person: Person) {
    person.age += 1    ⚠️Left side of mutating operator isn't mutable: 'self' is immutable
}
var p = Person()
var p1 = p
celebrateBirthdayOf(p)
可以看到也不能够,应为swift的参数默认是由let修饰的。在函数内部不可更改。此时我们可以用inout修饰参数,这样参数就可以在函数内部更改了。
func celebrateBirthdayOf(_ person: inout Person) {
    person.age += 1
}
var p = Person()
var p1 = p
celebrateBirthdayOf(&p)
通过SIL查看一下inout做了什么事:
// celebrateBirthdayOf(_:)
sil hidden @main.celebrateBirthdayOf(inout main.Person) -> () : $@convention(thin) (@inout Person) -> () {
// %0 "person"                                    // users: %3, %1
bb0(%0 : $*Person):
  debug_value_addr %0 : $*Person, var, name "person", argno 1 // id: %1
  %2 = integer_literal $Builtin.Int64, 1          // user: %8
  %3 = begin_access [modify] [static] %0 : $*Person // users: %15, %4
  %4 = struct_element_addr %3 : $*Person, #Person.age // users: %13, %5
  %5 = struct_element_addr %4 : $*UInt, #UInt._value // user: %6
  %6 = load %5 : $*Builtin.Int64                  // user: %8
  %7 = integer_literal $Builtin.Int1, -1          // user: %8
  %8 = builtin "uadd_with_overflow_Int64"(%6 : $Builtin.Int64, %2 : $Builtin.Int64, %7 : $Builtin.Int1) : $(Builtin.Int64, Builtin.Int1) // users: %10, %9
  %9 = tuple_extract %8 : $(Builtin.Int64, Builtin.Int1), 0 // user: %12
  %10 = tuple_extract %8 : $(Builtin.Int64, Builtin.Int1), 1 // user: %11
  cond_fail %10 : $Builtin.Int1, "arithmetic overflow" // id: %11
  %12 = struct $UInt (%9 : $Builtin.Int64)        // user: %13
  store %12 to %4 : $*UInt                        // id: %13
  %14 = tuple ()
  end_access %3 : $*Person                        // id: %15
  %16 = tuple ()                                  // user: %17
  return %16 : $()                                // id: %17
} // end sil function 'main.celebrateBirthdayOf(inout main.Person) -> ()'
可以看到inout和mutating所做的事如出一辙,都是将参数改成由var修饰,并且接收的是一个debug_value_addr地址。这也很好理解为什么传递参数的时候要用&p,即取p的地址。
总结
- 值类型在内存中存储的就是
值。 - 值类型的赋值是一个
值传递的过程,也就是通过深拷贝拷贝出一个副本。副本和本体处在两个不同的区域,所以值传递不共享状态。 - 
结构体就是值类型。 - 结构体的地址就是结构体
首个成员变量的地址。 - 结构体内部函数需要更改内部成员变量的值,要用
mutating关键字。 - 
inout和mutating底层所做的事一样,都是将参数变为var修饰,并且接收的参数是地址。 
类
不管是在swift还是OC类都是引用类型,下面来探究一下类。
定义一个类。
class Student {
    var age: UInt = 18
    var name: String = "Jack"
}
类与结构体不同,没有给成员变量赋初始值编译器会报错。所以类的成员变量要么赋初始值,要么自己实现初始化方法,在初始化方法里面给成员变量赋上初始值。
接下来看看类在内存中的地址:
var s = Student()
var s1 = s
s1.age = 16
(lldb) po withUnsafePointer(to: &s, { $0 })
▿ 0x0000000100202388
  - pointerValue : 4297073544
(lldb) po withUnsafePointer(to: &s1, { $0 })
▿ 0x0000000100202390
  - pointerValue : 4297073552
(lldb) x/g 0x0000000100202388
0x100202388: 0x0000000283924090
(lldb) x/g 0x0000000100202390
0x100202390: 0x0000000283924090
(lldb) x/8g 0x0000000283924090
0x283924090: 0x0000000100201e30 0x0000000200000002
0x2839240a0: 0x0000000000000010 0x000000006b63614a
0x2839240b0: 0xe400000000000000 0x0000000000000000
可以看到,s和s1是一个全局区的地址。而他们地址中的值都是一个相同的堆区地址,这个堆区地址就保存了Student实例变量的信息。前面两个字节分别是metadata和refCounts,第三个字节是age的值,第四个字节和第五个字节是name的ASCII码。
总结
引用类型在内存中存储的是堆区地址,堆区的地址才是保存引用类型实例变量的信息。
引用类型的赋值是对堆区的地址进行引用,彼此共享状态。
如果在结构体里有引用类型的成员变量,改变一个结构体成员变量的值另一个会改变吗?
class Student {
    var age: UInt = 18
    var name: String = "Jack"
}
struct Person {
    var age: UInt = 26
    var name: String = "dotry"
    var s = Student()
}
var p = Person()
var p1 = p
p1.s.age = 16
(lldb) po withUnsafePointer(to: &p, { $0 })
▿ 0x0000000102832388
  - pointerValue : 4337116040
(lldb) po withUnsafePointer(to: &p1, { $0 })
▿ 0x00000001028323a8
  - pointerValue : 4337116072
(lldb) x/8g 0x0000000102832388
0x102832388: 0x000000000000001a 0x0000007972746f64
 // p成员变量s的地址0x000000028202ad00
0x102832398: 0xe500000000000000 0x000000028202ad00
0x1028323a8: 0x000000000000001a 0x0000007972746f64
 // p1成员变量s的地址0x000000028202ad00
0x1028323b8: 0xe500000000000000 0x000000028202ad00
(lldb) x/6g 0x000000028202ad00
0x28202ad00: 0x0000000102831e38 0x0000000200000002
// s的age为16
0x28202ad10: 0x0000000000000010 0x000000006b63614a
0x28202ad20: 0xe400000000000000 0x0000000000000000
以上可以看到虽然Person是结构体值类型,但Student是引用类型。Person的成员变量s保存的仍然是地址,在传递和赋值是s是按照`引用计数管理的。所以当p1中s的age改变,p也会跟着改变。
通过SIL也可以证明:
// main
sil @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 {
bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>):
  alloc_global @main.p : main.Person             // id: %2
  %3 = global_addr @main.p : main.Person : $*Person // users: %7, %10
  %4 = metatype $@thin Person.Type                // user: %6
  // function_ref Person.init()
  %5 = function_ref @main.Person.init() -> main.Person : $@convention(method) (@thin Person.Type) -> @owned Person // user: %6
  %6 = apply %5(%4) : $@convention(method) (@thin Person.Type) -> @owned Person // user: %7
  store %6 to %3 : $*Person                       // id: %7
  alloc_global @main.p1 : main.Person            // id: %8
  %9 = global_addr @main.p1 : main.Person : $*Person // users: %13, %11
  %10 = begin_access [read] [dynamic] %3 : $*Person // users: %12, %11
  copy_addr %10 to [initialization] %9 : $*Person // id: %11
  end_access %10 : $*Person                       // id: %12
  %13 = begin_access [read] [dynamic] %9 : $*Person // users: %17, %14
  %14 = struct_element_addr %13 : $*Person, #Person.s // user: %15
  %15 = load %14 : $*Student                      // users: %22, %20, %21, %16
  // 引用Student
  strong_retain %15 : $Student                    // id: %16
......
// Person.s.getter
sil hidden [transparent] @main.Person.s.getter : main.Student : $@convention(method) (@guaranteed Person) -> @owned Student {
// %0 "self"                                      // users: %2, %1
bb0(%0 : $Person):
  debug_value %0 : $Person, let, name "self", argno 1 // id: %1
  %2 = struct_extract %0 : $Person, #Person.s     // users: %4, %3
  // 引用Student
  strong_retain %2 : $Student                     // id: %3
  return %2 : $Student                            // id: %4
} // end sil function 'main.Person.s.getter : main.Student'
// Person.s.setter
sil hidden [transparent] @main.Person.s.setter : main.Student : $@convention(method) (@owned Student, @inout Person) -> () {
// %0 "value"                                     // users: %11, %8, %4, %2
// %1 "self"                                      // users: %5, %3
bb0(%0 : $Student, %1 : $*Person):
  debug_value %0 : $Student, let, name "value", argno 1 // id: %2
  debug_value_addr %1 : $*Person, var, name "self", argno 2 // id: %3
  // 引用Student
  strong_retain %0 : $Student                     // id: %4
  %5 = begin_access [modify] [static] %1 : $*Person // users: %10, %6
  %6 = struct_element_addr %5 : $*Person, #Person.s // users: %8, %7
  %7 = load %6 : $*Student                        // user: %9
  store %0 to %6 : $*Student                      // id: %8
  strong_release %7 : $Student                    // id: %9
  end_access %5 : $*Person                        // id: %10
  strong_release %0 : $Student                    // id: %11
  %12 = tuple ()                                  // user: %13
  return %12 : $()                                // id: %13
} // end sil function 'main.Person.s.setter : main.Student'
一共三次retain,main中一次,Student.getter和Student.setter各一次。
打印引用计数验证也是三次。
(lldb) p CFGetRetainCount(p.s)
(CFIndex) $R8 = 3
注意:在结构体中应尽量避免含有引用类型的成员变量