Instance-related attributes in Swift can be divided into two categories: Stored Property and Computed Property. Next we’ll look at properties in Swift.

Stored Property

A storage property is a constant or variable that is part of an instance of a particular class and structure. A store property is either a variable store property (introduced by the var keyword) or a constant store property (introduced by the let keyword).

Swift has an explicit rule regarding storage properties that an initial value must be set for all storage properties when an instance of a class or structure is created. You can set an initial value for a storage property in the initializer, and you can assign a default property value as part of the definition.

struct SHPoint {
    var x: Double = 10
    let y: Double = 20
}

class SHPerson {
    var age: Int
    let name: String

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

let point = SHPoint(a)let person = SHPerson(18, name: "Coder_ zhang SAN")
Copy the code

Similar to the concept of member variables, which are stored in the memory of the instance, structures and classes can define storage properties, but enumerations cannot.

Ii. Computed Property

In addition to storing properties, classes, structures, and enumerations can also define computed properties. Computed properties do not store values. They provide getters and setters to modify and retrieve values. A stored property can be a constant or variable, but a calculated property must be defined as a variable. At the same time we must include the type when we write the computed property, because the compiler needs to know what the expected return value is.

1. Calculation of attribute writing

In Swift, setters and getters that override an instance can be modified with set and get, and setters passing in a new default value called newValue can also be customized. In Swift, in a method that has a return value, you can simply omit return if only return is inside the method.

struct SHCircle {
    var radius: Double
    var diameter: Double {
// set(newDiameter) {
// radius = newDiameter / 2
/ /}
        set {
            radius = newValue / 2
        }
        get {
            radius * 2}}}Copy the code

2. Read-only computing properties

Read-only computing properties: Only GET, no set.

struct SHCircle {
    var radius: Double

    var diameter: Double {
        get {
            radius * 2}}}Copy the code

Or we could write:

struct SHCircle {
    var radius: Double
    private (set) var diameter: Double

    init(_ radius: Double) {
        self.radius = 10
        diameter = radius * 2}}Copy the code

So what’s the difference between these two read-only computations? The former cannot be assigned either internally or externally. The latter can be assigned internally, but not externally.

3. Calculate the nature of the property

So what’s the difference between the nature of a calculated property and a stored property? We can see how the code in the.sil file is implemented. To learn about.sil files, see the previous two articles, Structures and Classes and Methods.

The viewController.swift file has the following code:

import UIKit

struct SHCircle {
    var radius: Double
    var diameter: Double {
        set {
            radius = newValue / 2
        }
        get {
            radius * 2}}}class ViewController: UIViewController {
    override func viewDidLoad(a) {
        super.viewDidLoad()
        var c = SHCircle(radius: 10)
        c.radius = 20
        c.diameter = 30
        let _ = c.diameter
    }
}
Copy the code

3.1. Declaration of SHCircle structure in SIL

Let’s look at how the SHCircle structure is declared in sil:

struct SHCircle {
    @_hasStorage var radius: Double { get set }
    var diameter: Double { get set }
    init(radius: Double)
}
Copy the code

Diameter {get set}} radius: diameter {get set} Radius: @hasstorage for diameter radius: @hasstorage for diameter radius: @hasstorage for diameter radius Diameter does not have the @_hasstorage modifier. Diameter only has setter and getter methods.

3.2. Getters and setters for radius attributes are implemented internally in sil

Let’s first look at the internal.sil implementation of the getter and setter for the RADIUS property:

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

As you can see, when calling the getter for RADIUS, the underlying SIL gets the radius value from the SHCircle and returns it. When the setter for RADIUS is called, the address of the SHCircle instance is taken and radius is modified.

The getter and setter for diameter property are implemented internally in the SIL

Let’s look at the internal sil implementation of the setter and getter for diameter property:

// SHCircle.diameter.setter
sil hidden @$s14ViewController8SHCircleV8diameterSdvs : $@convention(method) (Double.@inout SHCircle) - > () {// %0 "newValue" // users: %5, %2
    // %1 "self" // users: %8, %3
    bb0(%0 : $Double.%1 : $*SHCircle):
    debug_value %0 : $Double.let, name "newValue", argno 1 // id: %2
    debug_value_addr %1 : $*SHCircle.var, name "self", argno 2 // id: %3
    %4 = float_literal $Builtin.FPIEEE64.0x4000000000000000 // 2 // user: %6
    %5 = struct_extract %0 : $Double#,Double._value // user: %6
    %6 = builtin "fdiv_FPIEEE64"(%5 : $Builtin.FPIEEE64.%4 : $Builtin.FPIEEE64) : $Builtin.FPIEEE64 // user: %7
    %7 = struct $Double (%6 : $Builtin.FPIEEE64) / /user: 10% % = 8begin_access [modify] [static] %1 : $*SHCircle // users: %11, %9
    %9 = struct_element_addr %8 : $*SHCircle#,SHCircle.radius // user: % 10store% 7to %9 : $*Double                       // id: % 10end_access %8 : $*SHCircle                      // id: % 11% = 12tuple(a) / /user: % 13return %12 : $(a) / /id: %} / / 13end sil function '$s14ViewController8SHCircleV8diameterSdvs'
Copy the code

Notice that the internal implementation of the DIAMETER property setter automatically generates a constant named newValue and assigns a value passed in externally to newValue.

// SHCircle.diameter.getter
sil hidden @$s14ViewController8SHCircleV8diameterSdvg : $@convention(method) (SHCircle) - >Double {
    // %0 "self" // users: %2, %1
    bb0(%0 : $SHCircle):
    debug_value %0 : $SHCircle.let, name "self", argno 1 // id: %1
    %2 = struct_extract %0 : $SHCircle#,SHCircle.radius // user: %4
    %3 = float_literal $Builtin.FPIEEE64.0x4000000000000000 // 2 // user: %5
    %4 = struct_extract %2 : $Double#,Double._value // user: %5
    %5 = builtin "fmul_FPIEEE64"(%4 : $Builtin.FPIEEE64.%3 : $Builtin.FPIEEE64) : $Builtin.FPIEEE64 // user: %6
    %6 = struct $Double (%5 : $Builtin.FPIEEE64) / /user: % 7return %6 : $Double                             // id: % 7} / /end sil function '$s14ViewController8SHCircleV8diameterSdvg'
Copy the code

There is no diameter related stored variable in the setter and getter for diameter property. So in fact, evaluated properties don’t have member variables stored in the instance at all, which means evaluated properties take up no memory.

3.4. Calculate attribute assembly implementation

For a more intuitive feel, let’s look directly at the final compiled assembly code. We set the breakpoint at C.diameter = 30, and the assembly code looks like this:

By assembly observation, the essence of c.diameter = 30 is to call the setter and let _ = c.diameter is to call the getter.

It follows that evaluating properties is essentially a method (function) and does not consume instance memory.

The following table describes the properties of the Lazy Stored Property.

1. Lazy use

  • uselazyYou can define a deferred storage property that is initialized only when the property is first used.
  • lazyThe property must bevar, not aletBecause theletYou must have the value before the instance’s initialization method completes.
  • If multiple threads access simultaneously for the first timelazyProperty, there is no guarantee that the property will be initialized only once.
/ / definition:
class SHCar {
    init(a) {
        print("SHCar init!")}func run(a) {
        print("SHCar is running!")}}class SHPerson {
    lazy var car = SHCar(a)init(a) {
        print("SHPerson init!")}func goOut(a) { car.run() }
}
Copy the code
/ / call:
let p = SHPerson(a)print("-- -- -- -- -- -- -- --")
p.goOut()
Copy the code
Print result:SHPerson init!
--------
SHCar init!
SHCar is running!
Copy the code

2. Lazy

Note that:

  • Only when the structure contains a deferred storage propertyvarTo access the deferred storage property because the structure’s memory needs to be changed when the deferred property is initialized.
struct SHPoint {
    var x = 0.0
    var y = 0.0
    lazy var z = 0.0
}
Copy the code

3. Implementation of lazy calls in the underlying SIL

After generating the sil file, the SHPerson declaration is defined as follows:

class SHPerson {
    lazy var car: SHCar { get set }
    @_hasStorage @_hasInitialValue final var $__lazy_storage_$_car: SHCar? { get set }
    init(a)
    func goOut(a)
    @objc deinit
}
Copy the code

Storing properties With lazy modification, in addition to having the property of storing properties, the underlying SIL code generates a line of code. Notice that this line of code has a final modifier, indicating that lazy attributes cannot be overridden. Also, it is optional. Having options means that it actually started off with a value, but it was a nil value.

Let’s look at the implementation of the car property getter:

// SHPerson.car.getter
sil hidden [lazy_getter] [noinline] @$s14ViewController8SHPersonC3carAA5SHCarCvg : $@convention(method) (@guaranteed SHPerson) - >@owned SHCar {
    // %0 "self" // users: %17, %2, %1
    bb0(%0 : $SHPerson):
    debug_value %0 : $SHPerson.let, name "self", argno 1 // id: %1
    %2 = ref_element_addr %0 : $SHPerson#,SHPerson.$__lazy_storage_$_car // user: %3
    %3 = begin_access [read] [dynamic] %2 : $*Optional<SHCar> // users: %4, %6
    %4 = load %3 : $*Optional<SHCar>                // users: %7, %5
    retain_value %4 : $Optional<SHCar>              // id: %5
    end_access %3 : $*Optional<SHCar>               // id: %6
    switch_enum %4 : $Optional<SHCar>.case #Optional.some!enumelt: bb1, case #Optional.none!enumelt: bb2 // id: %7

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

    bb2:                                              // Preds: bb0
    %11 = metatype $@thick SHCar.Type               // user: %13
    // function_ref SHCar.__allocating_init()
    %12 = function_ref @$s14ViewController5SHCarCACycfC : $@convention(method) (@thick SHCar.Type) - >@owned SHCar // user: %13
    %13 = apply %12(%11) : $@convention(method) (@thick SHCar.Type) - >@owned SHCar // users: %16, %15, %23, %14
    debug_value %13 : $SHCar.let, name "tmp2"      // id: %14
    strong_retain %13 : $SHCar                      // id: %15
    %16 = enum $Optional<SHCar>, #Optional.some!enumelt, %13 : $SHCar // user: 17 = % to 20%ref_element_addr %0 : $SHPerson#,SHPerson.$__lazy_storage_$_car // user: 18 = % 18%begin_access [modify] [dynamic] %17 : $*Optional<SHCar> // users: %20, %19, %22
    %19 = load %18 : $*Optional<SHCar>              // user: % 21store% 16to %18 : $*Optional<SHCar>            // id: % 20release_value %19 : $Optional<SHCar>            // id: % 21end_access %18 : $*Optional<SHCar>              // id: % 22br bb3(%13 : $SHCar) / /id: %23

    // %24                                            // user: % 25bb3(%24 : $SHCar) : / /Preds: bb2 bb1
    return %24 : $SHCar                             // id: % 25} / /end sil function '$s14ViewController8SHPersonC3carAA5SHCarCvg'
Copy the code

The code is a bit long, but look at bb0, which has the following line:

switch_enum %4 : $Optional<SHCar>.case #Optional.some!enumelt: bb1, case #Optional.none!enumelt: bb2 // id: %7
Copy the code
  • It determines whether the car attribute has a value based on the options, if so, go bb1, otherwise go bb2.

  • The shcar.__allocating_init () method is called in bb2, so bb2 is initializing the car property.

  • Finally all go bb3, return the value of car.

Using lazy modifiers at the bottom is just an option to determine if the modified property has a value, and no thread-safety code is seen. So lazy is not thread-safe, and interested people can test the thread-safe nature of the lazy modifier for themselves in a multi-threaded environment.

Property Observer

1. WillSet and didSet

  • The property observer will look for changes to the property value, a willSet is called when the property is about to change, even if the value is the same as the original value, whereas didSet is called after the property has changed.

  • This is a bit like using setters in OC, such as the Model passed when setting the cell’s data source. However, attribute observers are more like KVO of OC, which are used to listen for changes in the value of an attribute, and both can listen for newValue and oldValue.

  • WillSet will pass a newValue, default is newValue, didSet will pass an oldValue, default is oldValue.

  • Setting property values in an initializer does not trigger willSet and didSet. Setting an initial value at property definition also does not trigger willSet and didSet.

The code is as follows:

class SHPoint {
    var x: Double {
        willSet {
            print("x willSet newValue: \(newValue)")}didSet {
            print("x didSet oldValue: \(oldValue)")}}var y: Double = 20 {
        willSet {
            print("y willSet newValue: \(newValue)")}didSet {
            print("y didSet oldValue: \(oldValue)")}}init(x: Double) {
        self.x = x
    }
}
Copy the code
print("begin")
let p = SHPoint(x: 10)
print("-- -- -- -- --")
p.x = 15
p.y = 30
print("end")
Copy the code
begin
-----
x willSet newValue: 15.0
x didSet oldValue: 10.0
y willSet newValue: 30.0
y didSet oldValue: 20.0
end
Copy the code

2. Attribute viewer is used in descending order in inheritance relation

Add a SHRect class that inherits from SHPoint and override the x and y attributes in SHRect as follows:

class SHRect: SHPoint {
    override var x: Double {
        willSet {
            print("override x willSet newValue: \(newValue)")}didSet {
            print("override x didSet oldValue: \(oldValue)")}}override var y: Double {
        willSet {
            print("override y willSet newValue: \(newValue)")}didSet {
            print("override y didSet oldValue: \(oldValue)")}}var width: Double = 100
    var height: Double = 100
}
Copy the code

Call as follows:

print("begin")
let p = SHRect(x: 10)
print("-- -- -- -- --")
p.x = 15
p.y = 30
print("end")
Copy the code
begin
-----
override x willSet newValue: 15.0
x willSet newValue: 15.0
x didSet oldValue: 10.0
override x didSet oldValue: 10.0
override y willSet newValue: 30.0
y willSet newValue: 30.0
y didSet oldValue: 20.0
override y didSet oldValue: 20.0
end
Copy the code

Let’s summarize the call order: subclass willSet -> parent willSet -> parent didSet -> subclass didSet.

3. WillSet and didSet low-level calls

After we generate sil code, let’s look at the setter for x in SHPoint:

// SHPoint.x.setter
sil hidden @$s14ViewController7SHPointC1xSdvs : $@convention(method) (Double.@guaranteed SHPoint) - > () {// %0 "value" // users: %13, %10, %2
// %1 "self" // users: %16, %11, %10, %4, %3
bb0(%0 : $Double.%1 : $SHPoint):
debug_value %0 : $Double.let, name "value", argno 1 // id: %2
debug_value %1 : $SHPoint.let, name "self", argno 2 // id: %3
%4 = ref_element_addr %1 : $SHPoint#,SHPoint.x // user: %5
%5 = begin_access [read] [dynamic] %4 : $*Double // users: %6, %7
%6 = load %5 : $*Double                         // users: %8, %16
end_access %5 : $*Double                        // id: %7
debug_value %6 : $Double.let, name "tmp"       // id: %8
// function_ref SHPoint.x.willset
%9 = function_ref @$s14ViewController7SHPointC1xSdvw : $@convention(method) (Double.@guaranteed SHPoint) - > ()// user: %10
%10 = apply %9(%0.%1) : $@convention(method) (Double.@guaranteed SHPoint) - > ()%11 = ref_element_addr %1 : $SHPoint#,SHPoint.x // user: %12
%12 = begin_access [modify] [dynamic] %11 : $*Double // users: %13, %14
store %0 to %12 : $*Double                      // id: %13
end_access %12 : $*Double                       // id: %14
// function_ref SHPoint.x.didset
%15 = function_ref @$s14ViewController7SHPointC1xSdvW : $@convention(method) (Double.@guaranteed SHPoint) - > ()// user: %16
%16 = apply %15(%6.%1) : $@convention(method) (Double.@guaranteed SHPoint) - > ()%17 = tuple ()                                  // user: %18
return %17 : $()                                // id: %18
} // end sil function '$s14ViewController7SHPointC1xSdvs'
Copy the code

If you add willSet and didSet, the underlying SIL code will call the willSet and didSet methods in the setter for the property. The two methods take two arguments, the first of which should correspond to newValue and oldValue.

Let’s look at the setter call after rewriting x in SHRect:

// SHRect.x.setter
sil hidden @$s14ViewController6SHRectC1xSdvs : $@convention(method) (Double.@guaranteed SHRect) - > () {// %0 "value" // users: %17, %13, %2
// %1 "self" // users: %15, %14, %5, %4, %20, %13, %3
bb0(%0 : $Double.%1 : $SHRect):
debug_value %0 : $Double.let, name "value", argno 1 // id: %2
debug_value %1 : $SHRect.let, name "self", argno 2 // id: %3
strong_retain %1 : $SHRect                      // id: %4
%5 = upcast %1 : $SHRect to $SHPoint            // users: %11, %6
%6 = ref_element_addr %5 : $SHPoint#,SHPoint.x // user: %7
%7 = begin_access [read] [dynamic] %6 : $*Double // users: %8, %9
%8 = load %7 : $*Double                         // users: %10, %20
end_access %7 : $*Double                        // id: %9
debug_value %8 : $Double.let, name "tmp"       // id: %10
strong_release %5 : $SHPoint                    // id: %11
// function_ref SHRect.x.willset
%12 = function_ref @$s14ViewController6SHRectC1xSdvw : $@convention(method) (Double.@guaranteed SHRect) - > ()// user: %13
%13 = apply %12(%0.%1) : $@convention(method) (Double.@guaranteed SHRect) -> ()
strong_retain %1 : $SHRect                      // id: %14
%15 = upcast %1 : $SHRect to $SHPoint           // users: %18, %17
// function_ref SHPoint.x.setter
%16 = function_ref @$s14ViewController7SHPointC1xSdvs : $@convention(method) (Double.@guaranteed SHPoint) - > ()// user: %17
%17 = apply %16(%0.%15) : $@convention(method) (Double.@guaranteed SHPoint) -> ()
strong_release %15 : $SHPoint                   // id: %18
// function_ref SHRect.x.didset
%19 = function_ref @$s14ViewController6SHRectC1xSdvW : $@convention(method) (Double.@guaranteed SHRect) - > ()// user: %20
%20 = apply %19(%8.%1) : $@convention(method) (Double.@guaranteed SHRect) - > ()%21 = tuple ()                                  // user: %22
return %21 : $()                                // id: %22
} // end sil function '$s14ViewController6SHRectC1xSdvs'
Copy the code

Notice that after calling the setter for X in SHRect, it calls its own WillSet, then its own setter for SHPoint, and then its own didSet. So at this point, you know where the order of calls in point 2 came from.

4. Global variables, local variables

Attribute observer, the function of calculating attributes, can also be applied to global variables, local variables.

Features of using attribute viewer:

// Global variables
var num: Int = 0 {
    willSet {
        print("willSet num newValue: \(newValue)")}didSet {
        print("didSet num oldValue: \(oldValue), currentValue: \(num)")}}Copy the code
// Local variables
func test(a) {
    var age = 10 {
        willSet {
            print("willSet age newValue: \(newValue)")}didSet {
            print("didSet age oldValue: \(oldValue), currentValue: \(age)")
        }
    }
    age = 11
}
Copy the code
/ / call
num = 11
print("-- -- -- -- -- -- --")
test()
Copy the code
Print the following:willSet num newValue: 11
didSet num oldValue: 0, currentValue: 11
-------
willSet age newValue: 20
didSet age oldValue: 10, currentValue: 20
Copy the code

The ability to use calculated properties:

// Global variables
var num: Int {
    get {
        return 10
    }
    set {
        print("setter num: \(newValue)")}}Copy the code
// Local variables
func test(a) {
    var age: Int {
        get {
            return 10
        }
        set {
            print("setter age: \(newValue)")
        }
    }
    age = 20
}
Copy the code
/ / call
num = 11
print("-- -- -- -- -- -- --")
test()
Copy the code
Setnum:11
-------
setter age: 20
Copy the code

Type Property

Strictly speaking, properties can be divided into Instance properties and Type properties.

1. Instance properties

  • Stored Instance Property: Stored in the memory of the Instance. Each Instance has one copy.

  • Computed Instance Property

Instance properties can only be accessed by instance, and the properties mentioned above are instance properties.

2. Type attributes

  • Stored Type Property: During the entire run of the program, there is only one piece of memory (similar to global variables).

  • Computed Type Properties (Computed Type Property)

  • You can define type attributes using static, or the class keyword if it is a class.

Type property, which can only be accessed by type. Define a type property in the structure, which can be modified static.

struct SHCar {
    static var count: Int = 0
}

SHCar.count + = 1;
Copy the code
  • Unlike store instance properties, you have to initialize the store type properties, because there is no init initializer for a type to initialize the store properties like there is for an instance.

  • The storage type attribute is lazy by default and is initialized only when used for the first time, even if accessed by multiple threads at the same time.

  • The storage type attribute can be let.

  • Enumerated types can also define type properties (store type properties, compute type properties).

3. Singleton mode

3.1 OC Singleton Mode compared with Swift Singleton Mode

The code for designing a singleton in OC looks like this:

+ (SHNetworkManager *)sharedManager;
Copy the code
+ (SHNetworkManager *)sharedManager {
    static SHNetworkManager* instance = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        instance = [[SHNetworkManager alloc] init];
    });
    return instance;
}
Copy the code

The code for the singleton designed in Swift looks like this:

public class SHNetworkManager {
    public static let shared = SHNetworkManager(a)private init(a){}}Copy the code

Or:

public class SHNetworkManager {
    public static let shared = {
        / /...
        return SHNetworkManager() ()}private init(a){}}Copy the code

Use the private modifier before init to make the modified initializer private, so that external access can only be accessed through shared.

In OC, when designing the singleton pattern, we usually create instances with dispatch_once to ensure that there is only one copy of the singleton object in memory for thread-safe purposes. Swift singleton does not need to write dispatch_once for instance creation. How can Swift singleton ensure thread safety? Please look down.

3.2 Nature of Swift singleton pattern

After compiling the SIL code, the singleton is declared as follows:

@_hasMissingDesignatedInitializers public class SHNetworkManager {
    @_hasStorage @_hasInitialValue public static let shared: SHNetworkManager { get }
    private init(a)
    @objc deinit
}
Copy the code

Let’s look at how shared’s getter is implemented:

// static SHNetworkManager.shared.getter
sil [transparent] @$s14ViewController16SHNetworkManagerC6sharedACvgZ : $@convention(method) (@thick SHNetworkManager.Type) - >@owned SHNetworkManager {
    // %0 "self" // user: %1
    bb0(%0 : $@thick SHNetworkManager.Type):
    debug_value %0 : $@thick SHNetworkManager.Type.let, name "self", argno 1 // id: %1
    // function_ref SHNetworkManager.shared.unsafeMutableAddressor
    %2 = function_ref @$s14ViewController16SHNetworkManagerC6sharedACvau : $@convention(thin) () -> Builtin.RawPointer // user: %3
    %3 = apply %2() : $@convention(thin) () -> Builtin.RawPointer // user: %4
    %4 = pointer_to_address %3 : $Builtin.RawPointer to [strict] $*SHNetworkManager // user: %5
    %5 = load %4 : $*SHNetworkManager               // users: %7, %6
    strong_retain %5 : $SHNetworkManager            // id: %6
    return %5 : $SHNetworkManager                   // id: %7
} // end sil function '$s14ViewController16SHNetworkManagerC6sharedACvgZ'
Copy the code

In the getter, the unsafeMutableAddressor method is called. The unsafeMutableAddressor implementation is as follows:

// SHNetworkManager.shared.unsafeMutableAddressor
sil [global_init] @$s14ViewController16SHNetworkManagerC6sharedACvau : $@convention(thin) () -> Builtin.RawPointer {
bb0:
%0 = global_addr @$s14ViewController16SHNetworkManagerC6shared_Wz : $*Builtin.Word // user: %1
%1 = address_to_pointer %0 : $*Builtin.Word to $Builtin.RawPointer // user: %3
// function_ref one-time initialization function for shared
%2 = function_ref @$s14ViewController16SHNetworkManagerC6shared_WZ : $@convention(c)() - > ()// user: %3
%3 = builtin "once"(%1 : $Builtin.RawPointer.%2 : $@convention(c) () -> ()) : $()
%4 = global_addr @$s14ViewController16SHNetworkManagerC6sharedACvpZ : $*SHNetworkManager // user: %5
%5 = address_to_pointer %4 : $*SHNetworkManager to $Builtin.RawPointer // user: %6
return %5 : $Builtin.RawPointer                 // id: %6
} // end sil function '$s14ViewController16SHNetworkManagerC6sharedACvau'
Copy the code

Attention! This code has the sensitive word “once” because OC creates singletons with dispatch_once and has comments on them: Function_ref one-off initialization function for shared. But it doesn’t seem to tell you much.

Next I compile the code into IR to look for once globally. Swiftc-emit – ir-target x86_64-apple-ios13.5-simulator – SDK $(xcrun — show-sdK-path — SDK iphonesimulator) ViewController. Swift > ViewController. Ll.

Compile and find the code associated with once as follows:

define swiftcc i8* @"$s14ViewController16SHNetworkManagerC6sharedACvau"(#)0 {
    entry:
    %0 = load i64, i64* @"$s14ViewController16SHNetworkManagerC6shared_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* @"$s14ViewController16SHNetworkManagerC6shared_Wz", align 8
    %4 = icmp eq i64 %3.-1
    call void @llvm.assume(i1 %4)
    ret i8* bitcast (%T14ViewController16SHNetworkManagerC** @"$s14ViewController16SHNetworkManagerC6sharedACvpZ" to i8*)

    once_not_done:                                    ; preds = %entry
    call void @swift_once(i64* @"$s14ViewController16SHNetworkManagerC6shared_Wz", i8* bitcast (void ()* @"$s14ViewController16SHNetworkManagerC6shared_WZ" to i8*), i8* undef)
    br label %once_done
}
Copy the code

Look at the method of mixed after writing s14ViewController16SHNetworkManagerC6sharedACvau, This method in the code of sil is SHNetworkManager Shared. UnsafeMutableAddressor this method, which is the corresponding.

See the once_not_done: code, which calls a function: swift_once and jumps to once_done to return an instance of SHNetworkManager.

So what is swift_once? It starts with swift, and I guess it’s a function inside SWIFT. Next I found it in the Swift source code. Its implementation is in the once-cpp file in the source code

Its source code implementation is as follows:

/// 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
}
Copy the code

Note that if it is __APPLE__, dispatch_once_f is called, which is a GCD function in iOS. So the singleton in Swift is the same as OC in the bottom layer, which calls the GCD function. This function is not executed after a single execution to ensure thread-safety.

The location of attributes in the Mach-O file

1. Explore the structure of attribute storage information source

You saw in the previous article “Methods” that the methods of a class are stored in VTable.

The essence of the Swift class is a structure pointer to a HeapObject, in which there is a metadata with a Description member variable. Description is a class called TargetClassDescriptor, and in that TargetClassDescriptor you have a VTable that stores the Swift class method.

So where do class attributes live in Swift? Let’s review the structure of 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
}
Copy the code

Swift attributes of a class that is stored in the fieldDescriptor, its source defined in the Metadata. H TargetTypeContextDescriptor in class, to note here, TargetClassDescriptor inherited from TargetTypeContextDescriptor.

The source code is defined as follows:

/// A pointer to the field descriptor for the type, if any.
TargetRelativeDirectPointer<Runtime, const reflection::FieldDescriptor./*nullable*/ true> Fields;
Copy the code

I couldn’t find anything about Fields at first, but I saw the FieldDescriptor, and I searched the current file for the FieldDescriptor, and I found this code:

namespace reflection {
    class FieldDescriptor;
}
Copy the code

Now that I see it, I can click to see the implementation of the FieldDescriptor class as follows:

I’m going to sort it out, so the structure of the FieldDescriptor looks like this:

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

FieldRecords records the information for each attribute. The FieldRecords member variable is not actually found in the source code, but it is written here for ease of understanding. The definition of FieldRecords in the source code looks like this:

using const_iterator = FieldRecordIterator;
Copy the code

And there’s a couple of methods in the FieldDescriptor:

const_iterator begin() const {
    auto Begin = getFieldRecordBuffer();
    auto End = Begin + NumFields;
    return const_iterator { Begin.End };
}

const_iterator end() const {
    auto Begin = getFieldRecordBuffer();
    auto End = Begin + NumFields;
    return const_iterator { End.End };
}

llvm::ArrayRef<FieldRecord> getFields() const {
    return {getFieldRecordBuffer(), NumFields};
}
Copy the code

This iterator is a FieldRecordIterator, and its source code structure is as follows:

struct FieldRecordIterator {
    const FieldRecord *Cur;
    const FieldRecord * const End;

    FieldRecordIterator(const FieldRecord *Cur, const FieldRecord * const End) : Cur(Cur), End(End) {}

    const FieldRecord &operator*() const {
        return *Cur;
    }

    const FieldRecord *operator->() const {
        return Cur;
    }

    FieldRecordIterator &operator++() {
        ++Cur;
        return *this;
    }

    bool operator= =(const FieldRecordIterator &other) const {
        return Cur = = other.Cur && End = = other.End;
    }

    bool operator! =(const FieldRecordIterator &other) const {
        return !(*this = =other); }};Copy the code

Seeing that its member variable is of type FieldRecord, let’s look at its structure in the source code:

class FieldRecord {
    const FieldRecordFlags Flags;

    public:
    const RelativeDirectPointer<const char> MangledTypeName;
    const RelativeDirectPointer<const char> FieldName;

    FieldRecord(a)= delete;

    bool hasMangledTypeName() const {
        return MangledTypeName;
    }

    StringRef getMangledTypeName() const {
        return Demangle::makeSymbolicMangledNameStringRef(MangledTypeName.get());
    }

    StringRef getFieldName() const {
        return FieldName.get();
    }

    bool isIndirectCase() const {
        return Flags.isIndirectCase();
    }

    bool isVar() const {
        return Flags.isVar(); }};Copy the code

It has three member variables: Flags, MangledTypeName, and FieldName. Flags: MangledTypeName is the type of the attribute, FieldName is the name of the attribute.

At this point, we have basically determined the structure and location of the properties stored in the underlying source code. Our search process is as follows:

  • Find HeapObject.
  • Find HeapMetadata from HeapObject.
  • Follow up, HeapMetadata is the alias of TargetHeapMetadata.
  • findTargetHeapMetadataStructure, and found itFieldsMember variables.
  • throughFieldsTo find theFieldDescriptorAnd find the definition ofFieldRecords.
  • The FieldRecord is found using the FieldRecordIterator.

2. The process of finding attribute information in mach-O files

After knowing the structure and location of attribute information in the source code, I needed to find something about attributes in Mach-O at this point. Create a new project, note that the name can not have Chinese, add the following code to compile:

class SHPerson {
    var age = 18
    var name = "Coder_ zhang SAN"
}
Copy the code

Open the project executable with MachOView.

Find the memory address of the Descriptor in the Mach-o file. After adding the memory addresses, subtract the base address of virtual memory, which is 0x100000000. The calculation process is as follows:

0x3F40 + 0xFFFFFF54 = 0x100003E94
0x100003E94 - 0x100000000 = 0x3E94
Copy the code

The result is 3E94. The memory address of 3E94 is in the mach-o file location as follows:

As you can see, 3E94 is in the mach-o file const, 50 00 00 80 is the beginning of the memory address of 3E94. To find the fieldDescriptor, we need to offset the size of the four member variables in front of the fieldDescriptor, So that’s four four bytes.

Offset to 74 00 00 00, 74 00 00 00 00 is actually the offset information of the fieldDescriptor, so to find the exact location of the fieldDescriptor in the mach-o file, add 74 00 00 00 00, The calculation process is as follows:

0x3EA0 + 0x4 + 0x74 = 0x3F18
Copy the code

The result of the calculation is zero3F18.3F18Memory address inMach-OThe location of the file is as follows:

As shown, 3F18 is in the position of fieldmd in the Mach-o file.

3F18 is the start address of the FieldDescriptor structure. To find the location of the FieldRecords, you need to offset the size of the member variable before the FieldRecords, 3 4bytes, 2 2bytes, so 4 4bytes.

So, 02, 00, 00, 00 is Flags. DC FF FF FF is MangledTypeName. DF FF FF FF FF is FieldName, but DF FF FF FF FF is the FieldName offset.

0x3F28 + 0x4 + 0x4 + FF FF FF DF = 0x100003F0F
// Need to subtract the base address of virtual memory
0x100003F0F - 0x100000000 = 0x3F0F
Copy the code

The result of the calculation is 3F0F. The memory address of 3F0F is in the location of the Mach-o file as follows:

As shown, 3F0F is at the position of reflstr in the Mach-o file. 61 67 65 00 is the information for the attribute age, 6E 61 6D 65 is the information for the attribute name.

At this point, the location of the Swift attribute information in the Mach-O file is clear.