First, structure

In Swift, structure is a very important data structure. In swift’s standard library, most public types are structures, such as Bool, Int, Double, String, Array, Dictionary and other common types.

The structure is defined as follows:

struct FYPerson {
    var age: Int
    var name: String
}
Copy the code

1. Initialize the structure

All structures have an initializer that is automatically generated by the compiler to ensure that all members of the structure have initial values.

If you customize an initializer, the compiler will not generate one for you.

When we define initialization of a structure, we must ensure that all members of the structure have initial values. If we assign an initial value to a member variable of the structure, the generated initializer can pass the value to that member variable without passing it as a parameter.

Second, the class

Classes and structures are basically similar, with the following differences:

  1. Classes have inherited properties that structs do not
  2. Type conversions enable you to examine and interpret the type of a class instance at run time
  3. A class has a destructor that frees its allocated resources
  4. Reference counting allows multiple references to a class instance

1. Class initialization

Unlike structures, the compiler does not automatically provide member initializers for classes by default. So, if your class does not provide an initial value for a member variable, you must provide a specified initializer for the class to ensure that all members of the class you create have initial values

class FYPerson {
    var age: Int
    var name: String
    init(age: Int.name: String) {
        self.age = age
        self.name = name
    }
}
Copy the code

We can also create a convenience initializer for a class that must call another initializer from the same class.

class FYPerson {
    var age: Int
    var name: String
    init(age: Int.name: String) {
        self.age = age
        self.name = name
    }
    convenience init(a) {
        self.init(age: 18, name:"Kody")}}Copy the code

In Swift, there are a number of rules for constructing designated initializers and convenient initializers:

  • The specified initializer must ensure that all attributes introduced by its class are initialized before delegating up to the parent class initializer.
  • The specified initializer must delegate to the parent initializer before it can set new values for inherited properties. If you do not, the new value assigned by the specified initializer will be overwritten by the initializer in the parent class
  • The convenience initializer must first delegate to other initializers in the class before assigning new values to any properties (including those defined in the class). If this is not done, the new value assigned by the convenience initializer will be overwritten by other specified initializers in its class.
  • The initializer cannot call any instance methods, read the values of any instance attributes, or refer to self as a value until the first phase of initialization is complete.

Failable initializer: When initializing a class, it is possible that the input of external parameters is invalid, or the conditions for initialization are not met, resulting in initialization failure. So, we can use a failable initializer that allows the class to return nil when initialization fails. A failable initializer is defined as follows:

class FYPerson {
    var age: Int
    var name: String
    init? (age:Int, name: String) {
        if age < 18 { return nil }
        self.age = age
        self.name = name
    }
}

var p = FYPerson(age: 16, name: "FY") // will return nil
Copy the code

Required initializers: We can prefix the class initializer with the required modifier to indicate that all subclasses of the class must implement the initializer.

As shown in the code above, the FYMan class, which inherits from the FYPerson class, fails to implement an init(age:, name:) initializer.

Value types and reference types

There are two types in Swift: value types and reference types.

  • Value types: Each instance retains a unique copy of its data, typically in the form of a struct, enum, or tuple.
  • Reference type: Each instance shares the same data source, usually in the form of a class.

1. The difference between value types and reference types

Variables of value type are deep-copy when assigned; when an instance variable has a copy of the data. Assigning an instance variable of struct type to another variable does not affect the value of the other instance variable when modifying the attribute value of that instance variable.

By looking at the memory structure of the variable P, we see that instance variables of the struct type store values. When p is assigned to P1, variable P1 gets a copy of variable P. Modify the age attribute of variable P1, and the value of the age attribute of variable P remains unchanged.

Variables that reference types do not store concrete instance objects directly.

If we look at the memory structure of p and P1, we can see that the variables p and P1 store the memory address of a specific instance.

We checked the address values corresponding to variables P and P1 again, and found that when the age value of P1 changed, the age value corresponding to P also changed.

In fact, the reference type is the equivalent of Excel online. When we share the link with others, their changes are visible to us. The value type is the local Excel, and when we pass the local Excel to someone else, we make a copy of it to someone else, and their changes to the content are imperceptible to us.

Storage locations of classes and structs

Another difference between reference types and value types is where they are stored. Value types are stored on the stack, whereas reference types are stored on the heap. Let’s compare classes and structs by where they are stored.useframe varibale -L xxxGet the memory address and passcat addressCommand to find the memory location of the instance object.

  • Memory allocation for classes
    1. Open up 8 bytes of memory space storage on the stacktVariables,tStore in a variableLGTeacherThe address of the
    2. On the heap, the appropriate area of memory is found to open up memory and store itLGTeacherInstance object of
    3. At the end of the function, the pointer on the stack is destroyed, and the sample variable on the heap is found and reclaimed.

  • Memory allocation for structures

    Allocate space directly on the stack to store the value of the structure

How to use structs and classes

Structures and classes are stored in different places. Structures are stored on the stack, while classes are stored on the heap. Therefore, classes consume more time and memory to create and destroy. Therefore, creating a structure takes less time and uses less memory than creating a class.

When we want to create a new type, how do we decide to use a value type or a reference type? When you use the Cocoa framework, a lot of the apis are used by subclasses of NSObject, so you have to use the reference class. In other cases, there are several guidelines:

  • When to use value types:
    • When the == operator is used to compare instance data
    • When you want copies of that instance to remain independent
    • Data will be used by multiple threads
  • When to use reference types (class) :
    • When the == operator is used to compare instance identities
    • You want to create a shared, mutable object at some point

5. The life cycle of a class

Swift compilation process

Both OC and Swift backends of iOS development are compiled using LLVM, as shown below:

  • OC is compiled by the CLang compiler into IR, which is then regenerated into an executable file. O.
  • Swift is compiled into IR by Swift compiler and then generates executable files.

Related commands for Swift compilation

Swiftc main. Swif-dump-parse // Analyze and check the type of output AST swiftc main. Swif-dump-ast // generate intermediate language (SIL), Swiftc main. Swift - EMIT - Silgen // Generate intermediate Language (SIL) Swift-emit -sil // Generates LLVM intermediate language (.ll file) swifTC main. Swift-emit -ir // generates LLVM intermediate language (.bc file) swifTC Swift -emit-assembly // Compilation generates executable. Out file swiftc-o main.o main.swiftCopy the code

SIL file analysis

Generating SIL files

Let’s enter the example code in the project

class ClassTeacher{
    var age: Int = 10
    var name: String = "FY"
    
}
var t = ClassTeacher()
Copy the code

Then we enter the generate SIL file command in the project

 swiftc -emit-sil  ${SRCROOT}/SwiftTest/main.swift > ./main.sil && open main.sil
Copy the code

Executing in a project generates a SIL fileThe generated SIL file is as follows:

Parsing SIL files

Now let’s take a closer look at the SIL intermediate language based on the SIL file above. Because there is a lot of code, I will parse the main function.

The first is

class LGTeacher {
  @_hasStorage @_hasInitialValue var age: Int { get set }
  @_hasStorage @_hasInitialValue var name: String { get set }
  @objc deinit
  init()
}
Copy the code

Where @_hasstorage and @_hasinitialValue are identifiers, indicating that age and name in LGTeacher class are storage attributes with initial values. The LGTeacher class also has deinit and init methods.

// main
sil @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 {
bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>):
  alloc_global @$s4main1tAA9LGTeacherCvp          // id: %2
  %3 = global_addr @$s4main1tAA9LGTeacherCvp : $*LGTeacher // user: %7
  %4 = metatype $@thick LGTeacher.Type            // user: %6
  // function_ref LGTeacher.__allocating_init()
  %5 = function_ref @$s4main9LGTeacherCACycfC : $@convention(method) (@thick LGTeacher.Type) -> @owned LGTeacher // user: %6
  %6 = apply %5(%4) : $@convention(method) (@thick LGTeacher.Type) -> @owned LGTeacher // user: %7
  store %6 to %3 : $*LGTeacher                    // id: %7
  %8 = integer_literal $Builtin.Int32, 0          // user: %9
  %9 = struct $Int32 (%8 : $Builtin.Int32)        // user: %10
  return %9 : $Int32                              // id: %10
} // end sil function 'main'
Copy the code

So let’s look at the @main function, which is an entry function.

Such as %0, %1, and %2 are virtual registers that cannot be changed once assigned, so values will always accumulate.

Let’s look atalloc_global @$s4main1tAA9LGTeacherCvpAssigns a global variable. And this variable names4main1tAA9LGTeacherCvpIs the name of a variable that is muddled. We can get throughxcrun swift-demangleCommand to get the actual namemain.t: main. LGTeacher %3 = global_addr @$s4main1tAA9LGTeacherCvp : $*LGTeacher This command saves the global variable’s internal address.

%4 = metatype $@thick lgteacher.type This command is used to get the mettype of lgteacher.type.

%5 = function_ref @$s4main9LGTeacherCACycfC : $@convention(method) (@thick lgteacher.type) -> @owned LGTeacher

%6 = apply %5(%4) : $@convention(method) (@thick lgteacher.type) -> @owned LGTeacher Take the LGTeacher.Type metatype as an argument, and then store the function return value in the register %6, that is, create an instance of LGTeacher.

Store %6 to %3: $*LGTeacher This command stores the memory address of the instance object into the address of the global variable.

%8 = integer_literal $Builtin.Int32, 0          // user: %9
%9 = struct $Int32 (%8 : $Builtin.Int32)        // user: %10
Copy the code

These two commands build a variable of type Int32 and assign 0. In Swift Int is a structure.

Return %9: $Int32 return %9: $Int32

Next we examine the LGTeacher class’s __allocating_init() constructor.

// LGTeacher.__allocating_init()
sil hidden [exact_self_class] @$s4main9LGTeacherCACycfC : $@convention(method) (@thick LGTeacher.Type) -> @owned LGTeacher {
// %0 "$metatype"
bb0(%0 : $@thick LGTeacher.Type):
  %1 = alloc_ref $LGTeacher                       // user: %3
  // function_ref LGTeacher.init()
  %2 = function_ref @$s4main9LGTeacherCACycfc : $@convention(method) (@owned LGTeacher) -> @owned LGTeacher // user: %3
  %3 = apply %2(%1) : $@convention(method) (@owned LGTeacher) -> @owned LGTeacher // user: %4
  return %3 : $LGTeacher                          // id: %4
} // end sil function '$s4main9LGTeacherCACycfC'
Copy the code

First, we need to get the LGTeacher metatype, which can be understood as the ISA pointer in OC. Alloc_ref $LGTeacher = alloc_ref ();

Allocates an object of reference type T. The object will be initialized with retain count 1;
its state will be otherwise uninitialized. 
The optional objc attribute indicates that the object should be allocated using Objective-C's allocation methods (+allocWithZone:).
Copy the code

This function allocates memory in Swift by calling swift_allocObject, while in OC +allocWithZone: is used.

Finally, an instance object is generated by calling the lgteacher.init () method and returned as the return value.

Analyze the class instance variable creation process.

Let’s start by creating a pure Swift class

class LGTeacher {
    var age: Int = 18
    var name: String = "FUYU"
}
var t = LGTeacher()
Copy the code

Once it’s up and running, let’s just hit the breakpoint at__allocating_initOn the functionGo deep into the__allocating_initInside the function we’re going to find the callswift_allocObjectandinitTwo methods.Let’s do this againLGTeacherClass to inheritNSObjectclass

class LGTeacher: NSObject {
    var age: Int = 18
    var name: String = "FUYU"
}
var t = LGTeacher()
Copy the code

Into the__allocating_initInside the function we’ll see that this time we’re calling the OCobjc_allocWithZoneandobjc_msgSendMethods.Next we look at the Swift source code to find out how instance variables are created. First, let’s search for the heapObject.cpp fileswift_allocObjectFunction, you can see the internal callswift_slowAllocfunctionEnter theswift_slowAllocFunction we can see it calledmalloc From this, it can be concluded that the swift object is created through the following process to allocate memory

__allocating_init -> swift_allocObject -> _swift_allocObject_ -> swift_slowAlloc -> malloc

From the object creation analysis, we can see that Swift’s object memory structure isHeapObject. And in theHeapObjectIn, we can see that there are two properties, one ismetadata, one isrefCounts. The default size is 16 bytes.

Swift class structure analysis.

The OC class has only one ISA pointer, but the SWIFT class has metadata and refCounts

  • Metadata structure analysis

By looking at theHeapObjectSource code, we knowmetadataisHeapMetadataType, and this isTargetHeapMetadataAlias for the definition.Let’s look atTargetHeapMetadataThe source code, throughTargetHeapMetadataWe can see that if it is a pure Swift class, one will be passed inMetadataKindThe type ofkindParameters. If it is a class that OC interacts with, it is passedisa.Let’s keep goingMetadataKindType of code found to be oneuint32_ttypeIn the Swift,MetadataKindTypes have multiple definitions, as shown below

  • Class metadata structure guess

We learn fromTargetHeapMetadataAs you can see from the source of the class,TargetHeapMetadataInherited fromTargetMetadata, so we know that MetaData ultimately inherits fromTargetMetadata. We learn fromTargetMetadataLook in the source codeTargetMetadataThe create function of. The function is as follows:From this function we can see that with different types of Kind property,TargetMetadataIt’ll turn into something elseMetadata. soTargetMetadataIs the base class for all types of metatype.

When kind is Class, TargetMetadata is converted to TargetClassMetadata. Therefore, we can use TargetClassMetadata to find the MetaData structure of the Class.

To viewTargetClassMetadataClass source, we see this class inheritedTargetAnyClassMetadataClass, andTargetAnyClassMetadataAnd inheritedTargetHeapMetadataClass. Through source code analysis, we can get the classMetaDataProperty inheritance chain. namelyTargetClassMetadata -> TargetAnyClassMetadata -> TargetHeapMetadata -> TargetMetadataAt the same time, we can get the Swift class MetaData data structure by viewing the properties of these classes

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 
}
Copy the code
  • Validate the data structure of the class

HeapObject has two properties: Metadata and RefCount

So we define a structure to force our instance object to that type, and if the conversion succeeds, the analysis is correct. The code is as follows:

The conversion result is successful, indicating that our analysis is correct.

Next we verify that the metadata data structure is correct The above results show that the data structure of the class is correct.