Storage properties

We start by creating a LYPerson object and declaring two storage properties

class LYPerson {
    var age : Int = 19
    var height: Int = 188
}

let p = LYPerson()
Copy the code

We analyze the property distribution of P through the LLDB instruction. First, we get the memory address of object P

(lldb) po p
<LYPerson: 0x10062fcc0>
Copy the code

Here 0x10062Fcc0 is the address of the HeapObject of our instance object P. Let’s read the memory distribution of the HeapObject

(lldb) x/8gx 0x10062fcc0 0x10062fcc0: 0x0000000100008170 0x0000000200000003 0x10062fcd0: 0x0000000000000013 0x0000000000000014 // 19, 20 0x10062FCE0:0x0000000000000000 0x0000000000000000 0x10062FCF0:0x0000000000000000 0x0000000000000000 0x10062FCF0: 0x0000000000000006 0x0000000000000000Copy the code

We can conclude that its memory distribution is as follows

The storage property consumes the memory of the current instance object.

Calculate attribute

Let’s start with the following code

Class Square {width: Double = 8.0 var area: Double { get { return width * width } set (newValue){ width = sqrt(newValue) } } } let s = Square() print(s.area)Copy the code

The size of the memory occupied by the Square instance object is 24,

class_getInstanceSize(Square.self) // 24
Copy the code

As we know, the default size of a Swift object is 16, and in the Square class, the width property is an Int of type 8 bytes, which takes up no memory of the instance object for calculating the property.

Where do computed properties reside since they don’t occupy the memory space of the instance object? Let’s look at it through its SIL file

swiftc -emit-sil main.swift > ./main.sil && open main.sil
Copy the code
class Square {
  @_hasStorage @_hasInitialValue var width: Double { get set }
  var area: Double { get set }
  @objc deinit
  init()
}
Copy the code

The area attribute is essentially a get and set method stored in Metadata that does not occupy the memory of the instance object.

Property observer willSet didSet

A property observer can listen for changes in property values as they change

class Square { var area: Double = 0.0 {willSet {print("newValue --\(newValue), value --\(area)")} didSet {print("oldValue --\(oldValue), Value --\(area)")}} let s = Square() s.rea = 18 newValue --18.0, value --0.0 oldValue --0.0, value --18.0Copy the code

The lazy storage property is lazy

The initial value

Variables that use lazy must have a default initial value, otherwise the compilation will report lazy properties must have an initializer error.

Assignment time

Next, let’s look at the change in the value of the lazy attribute

(lldb) po p
<Person: 0x104004240> // 1
(lldb) x/4gx 0x104004240 // 2
0x104004240: 0x0000000100008160 0x0000000200000003
0x104004250: 0x0000000000000000 0x0000000000000000
(lldb) x/4gx 0x104004240 // 3
0x104004240: 0x0000000100008160 0x0000000200000003
0x104004250: 0x0000000000004c4c 0xe200000000000000
Copy the code
  • 1, the variablepMemory address of
  • 2, check thenameIf the attribute is not assigned,pMemory space. The value of name is 0000.
  • 3, when the first fetchnameProperties,p, you can see that the value of name is4C4CnamelyLL.

From the above we can see that deferred storage is assigned on the first access.

Effect on object size

class Person {
     lazy var age: Int = 0
}

class Man {
    var age: Int = 0
}


var p = Person()
print(class_getInstanceSize(Person.self)) // 32

var m = Man()
print(class_getInstanceSize(Man.self)) // 24
Copy the code

We can see that the Man object takes up 24 bytes and the Person object takes up 32 bytes. We first convert it to SIL files using swifTC:

swiftc -emit-sil main.swift | xcrun swift-demangle > ./main.sil && open main.sil
Copy the code
class Person {
  lazy var age: Int { get set }
  // 1
  @_hasStorage @_hasInitialValue final var $__lazy_storage_$_age: Int? { get set }
  @objc deinit
  init()
}

class Man {
// 2
  @_hasStorage @_hasInitialValue var age: Int { get set }
  @objc deinit
  init()
}
Copy the code
  • lazymodifiedIntAfter that, it becomes thetaInt?Type becomes selectable, as we can see from the comparison of 1 and 2.

In the setter method of the Age property of the Person object

// Person.age.setter sil hidden @main.Person.age.setter : Swift.Int : $@convention(method) (Int, @guaranteed Person) -> () { // %0 "value" // users: %4, %2 // %1 "self" // users: %5, %3 bb0(%0 : $Int, %1 : $Person): debug_value %0 : $Int, let, name "value", argno 1 // id: %2 debug_value %1 : $Person, let, name "self", argno 2 // id: %3 %4 = enum $Optional<Int>, #Optional.some! enumelt, %0 : $Int // user: %7 %5 = ref_element_addr %1 : $Person, #Person.$__lazy_storage_$_age // user: %6 %6 = begin_access [modify] [dynamic] %5 : $*Optional<Int> // users: %7, %8 store %4 to %6 : $*Optional<Int> // id: %7 end_access %6 : $*Optional<Int> // id: %8 %9 = tuple () // user: %10 return %9 : $() // id: %10 }Copy the code

We can also see that data of type Optional

is assigned to the age property. In the getter of the age property

// Person.age.getter sil hidden [lazy_getter] [noinline] @main.Person.age.getter : Swift.Int : $@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: %6Copy the code

Obtain the attribute value by switch_enum.

What is the size of Optional

?

Print (MemoryLayout<Optional<Int>>.size) // 9: actual size print(MemoryLayout<Optional<Int>>.stride) // 16: bytes aligned, actual sizeCopy the code

So the age: Int property becomes an Int with the lazy modifier? The object size changes as well. In getter methods, because there is no lock, there is no guarantee of thread safety when multiple threads are accessing at the same time.

Summary:

  • Deferred storage must have a default initial value.
  • Deferred storage is not assigned until the first access.
  • The deferred storage property has an effect on the size of the instance object.
  • The deferred storage property is not thread-safe.

The type attribute

The static keyword

Properties decorated with the static keyword are type properties.

class Person {
     static var age: Int = 0
}


let age = Person.age

print(age)
Copy the code

Let’s first convert it to SIL

class Person {
  @_hasStorage @_hasInitialValue static var age: Int { get set }
  @objc deinit
  init()
}
// static Person.age
sil_global hidden @static main.Person.age : Swift.Int : $Int // 1
Copy the code
  • 1, with the static modifier, becomes a global property.
// Person.age.unsafeMutableAddressor
sil hidden [global_init] @main.Person.age.unsafeMutableAddressor : Swift.Int : $@convention(thin) () -> Builtin.RawPointer {
bb0:
  %0 = global_addr @globalinit_029_12232F587A4C5CD8B1EEDF696793B2FC_token0 : $*Builtin.Word // user: %1
  %1 = address_to_pointer %0 : $*Builtin.Word to $Builtin.RawPointer // user: %3
  // function_ref globalinit_029_12232F587A4C5CD8B1EEDF696793B2FC_func0
  %2 = function_ref @globalinit_029_12232F587A4C5CD8B1EEDF696793B2FC_func0 : $@convention(c) () -> () // user: %3
  // 1
  %3 = builtin "once"(%1 : $Builtin.RawPointer, %2 : $@convention(c) () -> ()) : $() 
  %4 = global_addr @static main.Person.age : Swift.Int : $*Int // user: %5
  %5 = address_to_pointer %4 : $*Int to $Builtin.RawPointer // user: %6
  return %5 : $Builtin.RawPointer                 // id: %6
}
Copy the code
  • 1, used when assigning the age attributebuiltin "once"In the source code, the actual callswift_onceFunction, whose source is as follows
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
}
Copy the code
  • swift_onceEssentially calls GCD’sdispatch_once_fFunction to ensure that variables are initialized only once and are thread-safe.

Swift singleton

The static keyword is thread-safe and initialized only once, so we can use static to create singletons. This is how we create singletons in Swift.

class Animal {
    static let sharedInstance = Animal()
    private init() {
        
    }
}

let a = Animal.sharedInstance
Copy the code

conclusion

The storage property occupies the memory space of the current instance object. Computed properties do not occupy the memory space of the current instance object and are stored in metadata. The attribute observer monitors changes in the value of the attribute. The lazy storage property affects the size of an instance object and is not thread-safe. The type property static is thread-safe and is initialized only once.