3、Swift属性探索

613 阅读7分钟

一、存储属性(Stored Property)

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

class CTTeacher{
    var age:Int
    var name:String
}

这里的agename就是存储属性

let 用来声明常量,常量的值一旦设置好便不能再被更改;

var 用来声明变量,变量的值可以在将来设置为不同的值。

1.1 从汇编的角度来看varlet

iShot2022-02-08 17.26.17.png

iShot2022-02-08 17.35.52.png

可以看出他俩都存在__common这个section里,是在同一个内存区域,并且相差8个字节。所以本质上let和var它就是一个内存地址,没什么区别。

1.2 从SIL的角度来看varlet

var age = 18
let x = 20

编译个sil康康:

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

@_hasStorage @_hasInitialValue let x: Int { get }

它俩都有@_hasStorage前缀,都是存储属性,重点是一个有set方法,一个没有set方法,也就是说let属性并没有set方法

二、计算属性

2.1 get() & set()

计算属性并不存储值,他们提供 gettersetter 来修改和获取值。计算属性必须定义为变量。我们写计算属性时必须包含类型,因为编译器需要知道返回值是什么类型。

struct square{
    
    var width: Double
    
    var area: Double{
        get{
            return width * width
        }
        set{
            self.width = newValue
        }
    }
}
  • 这里面的width,是存储属性,占用8个字节的空间;area是计算属性,不占用内存空间。

  • set()里的newValue是编译器自动生成的

iShot2022-02-09 17.30.59.png

  • newValue可以自己起个名字,不一定非要叫newValue
    var area: Double{
        get{
            return width * width
        }
        set(newArea){
            self.width = newArea
        }
    }

iShot2022-02-09 17.34.05.png

再看这段代码

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

areaheight有啥区别?

iShot2022-02-10 10.11.36.png

  • height@_hasStorage修饰,是个存储属性,area是计算属性
  • 他俩都没有set方法 所以虽然这俩属性似乎用法上一样,都只能取值(get方法),但本质不同。

还有一种写法:

struct square{
    
    var width: Double = 10
    private(set) var area:Double = 20
    let height:Double = 30
}

private(set)修饰area属性,意味着areaset方法只能在结构体内部调用

iShot2022-02-10 10.37.14.png iShot2022-02-10 10.35.44.png

2.2 willSet() & didSet()

class SubjectName{
    
    var subjectName:String = ""{
        willSet{
            print("subjectName will set value \(newValue)")
        }
        didSet{
            print("subjectName has been changed \(oldValue)")
        }
    }
}

let s = SubjectName()
s.subjectName = "NewName"

看一下sil:

// SubjectName.subjectName.setter
// setter方法
sil hidden @$s4main11SubjectNameC07subjectC0SSvs : $@convention(method) (@owned String, @guaranteed SubjectName) -> () {
// %0 "value"                                     // users: %22, %16, %12, %11, %2
// %1 "self"                                      // users: %20, %13, %11, %4, %3
bb0(%0 : $String, %1 : $SubjectName):
  debug_value %0 : $String, let, name "value", argno 1 // id: %2
  debug_value %1 : $SubjectName, let, name "self", argno 2 // id: %3
  %4 = ref_element_addr %1 : $SubjectName, #SubjectName.subjectName // 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 SubjectName.subjectName.willset
  // 调用willset方法
  %10 = function_ref @$s4main11SubjectNameC07subjectC0SSvw : $@convention(method) (@guaranteed String, @guaranteed SubjectName) -> () // user: %11
  %11 = apply %10(%0, %1) : $@convention(method) (@guaranteed String, @guaranteed SubjectName) -> ()
  retain_value %0 : $String                       // id: %12
  //给subjectName赋值
  %13 = ref_element_addr %1 : $SubjectName, #SubjectName.subjectName // 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 SubjectName.subjectName.didset
  // 调用didset方法
  %19 = function_ref @$s4main11SubjectNameC07subjectC0SSvW : $@convention(method) (@guaranteed String, @guaranteed SubjectName) -> () // user: %20
  %20 = apply %19(%6, %1) : $@convention(method) (@guaranteed String, @guaranteed SubjectName) -> ()
  release_value %6 : $String                      // id: %21
  release_value %0 : $String                      // id: %22
  %23 = tuple ()                                  // user: %24
  return %23 : $()                                // id: %24
} // end sil function '$s4main11SubjectNameC07subjectC0SSvs'

  • 计算属性不能添加willSet()didSet() iShot2022-02-10 11.45.32.png

2.3 有继承时 willSet() & didSet() 调用情况

来一段代码:

class CTTeacher{
    
    var age: Int{
        willSet{
            print("CTTeacher's age will set value \(newValue)")
        }
        didSet{
            print("CTTeacher's age has been changed \(oldValue)")
        }
    }
    
    var height:Double
    
    init(_ age:Int, _ height:Double) {
        self.age = age
        self.height = height
    }
}

class CTParTimeTeacher:CTTeacher{
    
    override var age: Int{
        willSet{
            print("CTParTimeTeacher's age will set value \(newValue)")
        }
        didSet{
            print("CTParTimeTeacher's age has been changed \(oldValue)")
        }
    }
    
    var subjectName:String
    
    init(_ subjectName:String) {
        self.subjectName = subjectName
        super.init(18, 109.0)
        self.age = 20
    }
}

let t = CTParTimeTeacher("CCT")

打印结果: iShot2022-02-10 13.50.33.png

bb0(%0 : $Int, %1 : $CTParTimeTeacher):
  debug_value %0 : $Int, let, name "value", argno 1 // id: %2
  debug_value %1 : $CTParTimeTeacher, let, name "self", argno 2 // id: %3
  strong_retain %1 : $CTParTimeTeacher            // id: %4
  %5 = upcast %1 : $CTParTimeTeacher to $CTTeacher // users: %11, %6
  %6 = ref_element_addr %5 : $CTTeacher, #CTTeacher.age // user: %7
  %7 = begin_access [read] [dynamic] %6 : $*Int   // users: %8, %9
  %8 = load %7 : $*Int                            // users: %10, %20
  end_access %7 : $*Int                           // id: %9
  debug_value %8 : $Int, let, name "tmp"          // id: %10
  strong_release %5 : $CTTeacher                  // id: %11
  
  // function_ref CTParTimeTeacher.age.willset
  // 调用CTParTimeTeacher的willset
  %12 = function_ref @$s4main16CTParTimeTeacherC3ageSivw : $@convention(method) (Int, @guaranteed CTParTimeTeacher) -> () // user: %13
  %13 = apply %12(%0, %1) : $@convention(method) (Int, @guaranteed CTParTimeTeacher) -> ()
  strong_retain %1 : $CTParTimeTeacher            // id: %14
  %15 = upcast %1 : $CTParTimeTeacher to $CTTeacher // users: %18, %17
  
  // function_ref CTTeacher.age.setter
  // 调用CTTeacher的set方法 由于CTParTimeTeacher继承自CTTeacher 所以set方法走父类合理
  %16 = function_ref @$s4main9CTTeacherC3ageSivs : $@convention(method) (Int, @guaranteed CTTeacher) -> () // user: %17
  %17 = apply %16(%0, %15) : $@convention(method) (Int, @guaranteed CTTeacher) -> ()
  strong_release %15 : $CTTeacher                 // id: %18
  
  // function_ref CTParTimeTeacher.age.didset
  // 调用CTParTimeTeacher的didset
  %19 = function_ref @$s4main16CTParTimeTeacherC3ageSivW : $@convention(method) (Int, @guaranteed CTParTimeTeacher) -> () // user: %20
  %20 = apply %19(%8, %1) : $@convention(method) (Int, @guaranteed CTParTimeTeacher) -> ()
  %21 = tuple ()                                  // user: %22
  return %21 : $()                                // id: %22
}

三、延迟存储属性

  • 延迟存储属性的初始值在其第一次使用时才进行计算。
  • 用关键字lazy来标识一个延迟存储属性
class Subject{
    lazy var age:Int = 18
}

var s = Subject()

print(s.age)

iShot2022-02-10 14.15.08.png

iShot2022-02-10 14.30.28.png

age是如何做到在首次访问才初始化的?看一下sil: iShot2022-02-10 14.37.31.png

age的后面有一个问号,也就是说它是一个可选类型

在看一下它初始化时的处理:初始化为一个Optional.none!,可以理解为oc里面的nil,也就是0x0000000000000000 iShot2022-02-10 14.51.57.png

  • 延迟存储属性的本质是一个可选类型

再看一下getter方法的sil

// Subject.age.getter
sil hidden [lazy_getter] [noinline] @$s4main7SubjectC3ageSivg : $@convention(method) (@guaranteed Subject) -> Int {
// %0 "self"                                      // users: %14, %2, %1
bb0(%0 : $Subject):
  debug_value %0 : $Subject, let, name "self", argno 1 // id: %1
  
  //访问__lazy_storage_$_age的地址
  %2 = ref_element_addr %0 : $Subject, #Subject.$__lazy_storage_$_age // user: %3
  
  %3 = begin_access [read] [dynamic] %2 : $*Optional<Int> // users: %4, %5
  
  //把内存地址的值,放到%4
  %4 = load %3 : $*Optional<Int>                  // user: %6
  end_access %3 : $*Optional<Int>                 // id: %5
  
  //switch匹配,如果是Optional.some,走bb1代码;如果是Optional.none,走bb2代码
  switch_enum %4 : $Optional<Int>, case #Optional.some!enumelt: bb1, case #Optional.none!enumelt: bb2 // id: %6

那看一下bb2代码块:(初始化赋值的代码)

bb2:                                              // Preds: bb0
  
  //构建值 int类型
  %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
  
  //把构建好的值,给到age
  %14 = ref_element_addr %0 : $Subject, #Subject.$__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

bb1代码块:(非第一次调用时走的代码)

bb1(%7 : $Int):                                   // Preds: bb0
  //把原有的int值,返回回去
  debug_value %7 : $Int, let, name "tmp1"         // id: %8
  br bb3(%7 : $Int)                               // id: %9

还有一种情况:

class Subject{
    lazy var age:Int{
        let s = MyClass()
    }()
}

后面跟一个闭包表达式,只有在第一次使用的时候才初始化。

  • 注意点:延迟存储属性 不能保证线程安全

四、static 类型属性

  • 类型属性其实就是一个全局变量
  • 类型属性只会被初始化一次 来段代码:
class CCTeacher{
    static var age:Int = 18
}
CCTeacher.age = 30

看一下sil:

// static CCTeacher.age
sil_global hidden @$s4main9CCTeacherC3ageSivpZ : $Int

在sil中,age被声明为一个全局变量,所以static本质上就是一个全局变量。

再看看如何访问age这个变量的:

// CCTeacher.age.unsafeMutableAddressor
sil hidden [global_init] @$s4main9CCTeacherC3ageSivau : $@convention(thin) () -> Builtin.RawPointer {
bb0:

  //拿到age的token的地址
  %0 = global_addr @$s4main9CCTeacherC3age_Wz : $*Builtin.Word // user: %1
  
  //一个指针类型的转换:把%0转换成一个RawPointer
 %1 = address_to_pointer %0 : $*Builtin.Word to $Builtin.RawPointer // user: %3
 
  // function_ref one-time initialization function for age
  // 调用age的初始化函数init
  %2 = function_ref @$s4main9CCTeacherC3age_WZ : $@convention(c) () -> () // user: %3
  
  //"once"  %1(token的地址)和%2(函数的内存地址)作为入参
  %3 = builtin "once"(%1 : $Builtin.RawPointer, %2 : $@convention(c) () -> ()) : $()
  
  %4 = global_addr @$s4main9CCTeacherC3ageSivpZ : $*Int // user: %5

  //返回全局变量的内存地址
  %5 = address_to_pointer %4 : $*Int to $Builtin.RawPointer // user: %6
  return %5 : $Builtin.RawPointer                 // id: %6
} // end sil function '$s4main9CCTeacherC3ageSivau'

%2那行,调用了一个函数 s4main9CCTeacherC3age_WZ,在sil里面找到:

// one-time initialization function for age
sil private [global_init_once_fn] @$s4main9CCTeacherC3age_WZ : $@convention(c) () -> () {
bb0:

  //初始化一个全局变量s4main9CCTeacherC3ageSivpZ,这个玩意儿通过还原
  //得到:static main.CCTeacher.age : Swift.Int
  //没错,就是在创建age
  //(控制台:xcrun swift-demangle s4main9CCTeacherC3ageSivpZ)
  alloc_global @$s4main9CCTeacherC3ageSivpZ       // id: %0
  
  //拿到全局变量内存地址
  %1 = global_addr @$s4main9CCTeacherC3ageSivpZ : $*Int // user: %4
  
  //构建int类型18
  %2 = integer_literal $Builtin.Int64, 18         // user: %3
  %3 = struct $Int (%2 : $Builtin.Int64)          // user: %4
  
  //把值存到age
  store %3 to %1 : $*Int                          // id: %4
  %5 = tuple ()                                   // user: %6
  return %5 : $()                                 // id: %6
} // end sil function '$s4main9CCTeacherC3age_WZ'

代码里边儿“once”那里,意思是只调用一次,在ir代码里可以找到:

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

调用了swift_once这个方法,我们在swift源码里可以找到(Once.cpp):

void swift::swift_once(swift_once_t *predicate, void (*fn)(void *),
                       void *context) {
#if 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
}

里面是dispatch_once_f,熟悉的GCD

五、内存独占的概念

那么staticlet组合起来,便成了单利:

class CCTeacher{
    static let shareInstance = CTTeacher()
    
    //把初始化器私有起来,外部就只能通过shareInstance访问,不能调init()
    private init(){}
}

如果是类型函数

class CCTeacher{

    static func test(){
        
        var tmp = 100
        print(tmp)
    }
}

CCTeacher.test()

直接静态派发,不走vtable

六、属性在macho中的位置

回顾一下MetadatatypeDescriptor存在于TEXT,swift5_types这个section里面

struct Metadata{
    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
}

typeDescriptor是属于TargetClassDescriptor这个类

class 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 类的属性存放在 fieldDescriptor 中,其结构:

class FieldDescriptor {
    MangledTypeName int32
    Superclass int32
    Kind uint16
    FieldRecordSize uint16
    NumFields uint32
    FieldRecords [FieldRecord]
}

其中numFields代表当前有多少个属性,FieldRecords记录了每个属性的信息,FieldRecords的结构体:

struct FieldRecord {
    Flags uint32  //标志位
    MangledTypeName int32  //属性的类型信息
    FieldName int32  //属性名称
}
  • 计算出 typeDescriptor 在 Mach-O 中的地址

FFFFFF30 + 3F4C = 0x100003E7C

0x100003E7C - 0x100000000 = 3E7C

iShot2022-02-10 17.23.58.png

得到3E7C,再定位到TEXT,__const这个section,找到3E7C,再向后偏移4个4字节

iShot2022-02-10 17.39.22.png 这里的98就是fieldDescriptor的偏移量,所以3E88 + 98 就可以得到FieldDescriptormach-o中的信息

3E88 + 98 = 3F24

再定位到 TEXT,__swift5_fieldmd 这个section,这里就能看到fieldDescriptor的内容,偏移4个字节,便是FieldRecords iShot2022-02-10 18.03.27.png

前两个对应的就是 FlagsMangledTypeName,从第三个开始就是 FieldName 也就是 ageage1 iShot2022-02-11 09.43.22.png

计算age的偏移量:3F34 + 两个4字节 + FFFFFFFD

3F34 + 8 + FFFFFFFD = 0x100003F39

0x100003F39 - 0x100000000 = 3F39

mach-o中的TEXT,__swift5_reflstr 这个section定位到3F39

QQ20220211-095830@2x.png

成功定位到ageage1,其中的61 67 65就是age61 67 67 31 就是age1