preface

As the whole IT industry becomes more and more introverted, IT is necessary to update its knowledge base. Swift is a must if you want to continue iOS development. Without further ado, let’s step into the Swift world

The preparatory work

  • Swift Official Documents
  • Swift source code
  • SIL Official Documentation
  • StructVsClassPerformance test cases

Introduction to classes and structures

Classes and structures in OC are familiar, but they may be unfamiliar in Swift. Let’s take a look at classes and structures in Swift

SwiftClass definition in

class LWPerson{
     / / property
    var age  = 20;
    var name = "LW"
    // Member initializer
    init(_ age: Int,_ name: String){
        self.age = age
        self.name = name
    }
    // The default initializer is automatically provided by the system
    init() {}}Copy the code

In this code, we create a LWPerson class with two attributes age and name, a member initializer init(_ AGE: Int,_ name: String), and a default initializer init(). The default initializer is provided by the system. The keyword for declaring a class is class

SwiftThe definition of a structure in

Structures are defined by the struct keyword

struct WTeacher{
     / / property
    var age  = 20;
    var name = "LW"
    // Member initializer
    init(_ age: Int,_ name: String){
        self.age = age
        self.name = name
    }
    // The default initializer is automatically provided by the system
    init() {}}Copy the code

Structs are defined very similarly to classes, and their importance in Swift is almost the same. What are the similarities and differences between classes and structures

Similarities and differences between classes and structures

Classes and structures are similar

  • Defining storage properties
  • Define methods
  • Define subscripts to provide access to their values using subscript syntax
  • Define initializers
  • useextensionTo extend the functionality
  • Follow a protocol to implement a function

The following uses a structure as an example to achieve the above similarity in code, which is shown below

 / / agreement
protocol testProtocol {
    func testFunc(a)
}

struct LWPerson {
    // Define storage properties
    var age = 20
    var name = "LW"
    
    // Define the method
    func personFunc() {print("Define method")}// Define subscripts to provide access to their values using subscript syntax
    subscript(index:Int) -> Any {
        set {
            if index == 0 {
                age = newValue as! Int
            }else{
                name = newValue as! String
            }
        }
        get{
            if index == 0 {
               return age
            }else{
               return name
            }
        }
    }
    // Define the initializer
    init(_ age: Int,_ name: String){
        self.age = age
        self.name = name
    }
}

// Use 'extension' to extend the functionality
// Follow the protocol to implement some function
extension LWPerson:testProtocol{
    // Extended functionality
    func extensionFunc() {print("Extended functions of structures.")}// Protocol implementation function
    func testFunc() {
         print("Protocol method")
    }
}

func text(){
    var p = LWPerson(10."LW")
    p[0]  = 100
    print(p[0])
    p.extensionFunc()
    p.testFunc()}text(a)Copy the code

The running results are as follows:

The running results show that the above functional structures and classes can be realized

Differences between classes and structures

  • Classes have inherited properties that structs do not
  • Type conversions enable you to examine and interpret the type of a class instance at run time (more on that later)
  • A class has a destructor that frees its allocated resources
  • Reference counting allows multiple references to a class instance

The above differences are implemented in code as follows

The result shows that classes have inheritance properties and destructors and structs do not

The results show that p, P1, and P2 are stored at the same address, and the structure changes the value to affect only itself. So classes are reference types, so reference counting allows multiple references to a class instance, whereas structs are value types and therefore do not. Let’s explore the difference between reference types and value types

Reference types

Reference type: all variables hold the same address, i.e. share a piece of data. When data is modified, all variable values are affected. This is what people call a shallow copy of a pointer. Common reference types in Swift are class and closure. Let’s verify this with the code

class LWTest{
    var age = 10
}

let test = LWTest()
let tTest = test
let newTest = test
Copy the code

The LLDB debugging result is as follows

(lldb) po test
<LWTest: 0x600000bb2000>

(lldb) po tTest
<LWTest: 0x600000bb2000>

(lldb) po newTest
<LWTest: 0x600000bb2000>
Copy the code

Debugging results show that the instance variables Test, tTest, and newTest store the same address 0x600000BB2000, which means they point to the same block of memory, resulting in a simple diagram

Summary: Reference types are equivalent to Excel online. When we share links with others, we can see that when others modify them online, it is equivalent to modifying the source data

Value types

Value type: That is, each variable keeps a copy of the data. When the value of each variable is changed, only the current variable is affected. Other variables are not affected. This is what is often called a value copy or deep copy. Common value types in Swift include struct, enum, tuple, Double, and so on. Let’s verify this with the code

struct LWPerson {
    var age = 20
    var name = "LW"
}

var p = LWPerson(age: 18, name: "Ha ha")
var p1 = p
p.age = 30
Copy the code

The LLDB debugging result is as follows

(LLDB) Po p ▿ lwperson-age:30
  - name : "Ha ha"(LLDB) Po p1 ▿ lwperson-age:18
  - name : "Ha ha"
Copy the code

The debugging results show that the variable, p and P1 are not stored in the address of the instance object but in the specific value, and the value of P1 will not be affected after the modification of the attribute in P

Conclusion: The value type is the local Excel. When we pass the local Excel to someone else, we make a copy of it to someone else. When they change the content, the others won’t be affected

Note: The most obvious difference between reference types and value types is where they are stored. Typically, reference types are stored on the heap and value types are stored on the stack

Where classes and structures are stored

The storage location of classes and structures is explored separately because I feel it is really important. Before exploring this question, let’s take a look at the memory regions (commonly known as the five regions of memory)

  • Stack area: Stores local variables and the context in which the function is running
    • The stack area is a contiguous block of memory, usually fromHigh address–> Lower addressFor storage
    • Stack areas are typically allocated at run time, iniOSIn thex86Architecture to0x7At the beginning
  • Heap area: Stores all objects
    • The heap area is discontinuous memory (easy to add and delete, not easy to query), usually fromLower address–> High addressFor storage
    • The allocation of heap space is dynamiciOSIn thex86Architecture to0x6At the beginning
  • Global static area: Stores global and static variables
    • The area isMemory allocated at compile timeIn theiOSIn thex86Under the framework generally0x1At the beginning
    • During the running of the program, the memory data exists all the time and is released by the system after the program ends
  • Constant area: Stores constants: integers, strings, etc
  • Code area: The binary into which the program is compiled

The global static area, constant area, and code area can be collectively referred to as the global area, because stack area is usually stored from high -> low address, and heap area is stored from low -> high address, thus causing the stack overflow situation indicated in the figure

Let’s explore this in code

class LWTest{
    var age = 10
}

struct LWPerson {
    var age = 20
    var name = "LW"
}

text()

func text(){
    var p = LWPerson(age: 18, name: "Ha ha")
    
}

var lw = LWTest()
var age = 10
Copy the code

The LLDB debugging result is as follows

The result of the diagram shows that structs are typically stored on the stack for value types, while classes are typically stored on the heap for reference types

Note: Adding a property to a structure that refers to a type does not change the type of the structure itself, that is, the value type

The result in the figure shows that the t1 variable stores the address of the instance object, the structure or the value type

In Mach-O, there are multiple segments, each of which is divided into several sections

  • TEXT.text: machine code
  • TEXT.cstring: a hard-coded string
  • TEXT.const: initialized constant
  • DATA.data: Initialized mutable (static/global) data
  • DATA.const: uninitialized constants
  • DATA.bss: uninitialized (static/global) variables
  • DATA.common: uninitialized symbol declaration

Class and struct selection use

Do you use structs in Swift development? Or use classes? Structures are generally preferred. Of course, depending on the functional modules of your own project, use structures when they are available. Such as encapsulating simple models that don’t need to inherit a class. So what are the reasons for using constructs in preference?

  • Structs are value types that are generally allocated in the stack areaMemory is continuousWhen the stack pointer refers to the time when the structure needs to be opened up, it will open a piece of memory space on the stack according to the size of the structure, and then copy the value in the structure to the stack. When the function is finished, the stack space will be automatically reclaimed and the memory will be released automatically, so the performance consumption is low and the speed is fast
  • Class is a reference type, memory allocation is in the heap, the heapMemory is discontinuous.Not easy to find. First, you need to find an available chunk of memory in the heap, and then return the memory address to the heap. When memory is released, the heap is searched for the release according to the memory address stored in the stack. This process consumes a lot of performance and is slow

We can also intuitively test the time allocation of current structures and classes by using StructVsClassPerformance on Github.

The running results show that the time consuming of using the structure is less than that of using the class, so those who can use the structure in the development process have the preference to use the structure

Initializers for classes and structures

Structure initializer

Initializers of structures are generally divided into two types: member initializers automatically generated by the system and custom initializers, which will be explored in detail below

Structure default initializer

If there are stored attributes in the structure and there is no custom initializer for member variables. A member initializer is automatically generated for you at compile time. The following code

Custom structure initializer

struct LWPerson {
    var age :Int
    var name :String
    
    init(_ age: Int,_ name: String){
        self.age = age
        self.name = name
    }
}
let lw = LWPerson(10."Ha ha")
Copy the code

If you customize a struct initializer, the compiler will not automatically generate other initializers. Properties in a structure can be declared without assignment, but in a custom initializer, each property must be assigned or the compiler will report an error

Class initializer

The compiler does not provide a member initializer for a class by default

Class default initializer

By default, a class provides an initializer init(){}, but the default initializer does not assign values to any properties, so you must set an initial value for all properties when declaring properties in a class. The following code

class LWPerson {
    var age :Int = 10
    var name :String = "Ha ha"
}

let lw = LWPerson(a)Copy the code

Custom class initializer

Because you want to set an appropriate initial value for all attributes in a class, you must provide the specified initializer in. The following code

 class LWPerson {
    var age :Int
    var name :String
    // Specify the initializer
    init(_ age: Int,_ name:String){
        self.age = age
        self.name = name
    }
}

let lw = LWPerson(10."Ha ha")
Copy the code

Convenient initializer

Designated initializer in general, there is only one equivalent to expose interfaces, only through the designated initializer to initialize all attributes, although there is only one designated initializer, convenient but initializers can have a lot of convenient (note: the initializer must be from the same class to invoke another initializer) code is as follows

class LWPerson {
    var age :Int
    var name :String
    init(_ age: Int,_ name:String){
        self.age = age
        self.name = name
    }
    convenience init (_ age: Int){
        Name = "hahaha" // It is not allowed to assign its attribute or use self as a value before initialization
        self.init(age, "lG")
        self.name = "Ha ha ha."
    }
    convenience init (_ name: String){
        self.init(20, name)
    }
}

let lw = LWPerson.init(10)
Copy the code

Note: It is not allowed to assign its attribute or use self as a value before calling self.init(age, “lG”), since self has not been initialized

throughlldbDebug before the initializer is called, at this timeselfInitialization is not complete, after the initializer call is completeselfInitialization is complete

  • SwiftOfficial documentation is also given to explain the documentation inInitializationIn theTwo-Phase InitializationSection gives4Test criteria. Among them the first3And the first4Point gives a detailed explanation

The inheritance relation initializer

Create the LWTeacher class to inherit the LWPerson class as follows

class LWPerson {
    var age :Int
    var name :String
    init(_ age: Int,_ name:String){
        self.age = age
        self.name = name
    }
 
}

class LWTeacher:LWPerson{
    var height:Float
    init (_ height:Float){
        self.height = height
        super.init(10."!")
        The specified initializer must ensure that all of its attributes are initialized before delegating to the parent class
        //self.height = height 
    }
}

let lw = LWTeacher.init(180.0)
Copy the code

In the specified initializer of the LWTeacher class, all attributes must be initialized to delegate to the parent class. Why is this? For security, debug through LLDB

  • lldbDebug results at this timeselfIt’s already initialized, which means thatLWTeacherAll attributes of the class have been initialized. If the attributes of the current class are not initialized, it will be problematic and unsafe to use later
  • SwiftOfficial documentation is also given to explain the documentation inInitializationIn theTwo-Phase InitializationSection gives4Test criteria

As mentioned above, an object’s memory is not considered fully initialized until the initial state of all its storage properties is known. To satisfy this rule, the specified initializer must ensure that all of its own attributes are initialized before it passes the chain.

Initializers can fail

This is easy to understand, which means that the initialization failed because of invalid parameters or external conditions. Such Swift failable initializers write return nil statements to indicate under what circumstances the failable initializers trigger initialization failures. It’s very simple to write

 class LWPerson {var age :Int var name :String init? (_ age: Int,_ name:String){if age < 18 {return nil} < 18 is against the rule
        self.age = age
        self.name = name
    }
}

let lw = LWPerson(10."Ha ha")
Copy the code

Necessary initializer

Add the required modifier before the class initializer to indicate that all subclasses of the class must implement the initializer

Conclusion:

  • 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 properties, or reference any instance properties until the first phase of initialization is completeselfAs the value

Class life cycle

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

  • OCthroughclangCompiler, compiles toIR, and then regenerate it into an executable file.o(Here is our machine code)
  • SwiftbySwiftThe compiler compiles toIR, and then generate the executable file

Swift code to generate executable file details

  • SwiftSource through-dump-parse(lexical analysis) generationParseThe abstract syntax tree
  • Parseafter-dump-ast(parsing) generationSemaCheck if syntax is correct (errors will be reported when compiling)
  • SemaDegraded to formSILGen, i.e.,SILThe middle code
  • SILGenafter-emit-silgengenerateRaw SIL, that is, nativeSIL
  • Raw SILafter-emit-silgenerateSILOpt Canonical SIL, that is, after optimizationSIL
  • SILOpt Canonical SILDemotion to generateIRGen, i.e.,IRThe middle code
  • IRGenafter-emit-irgenerateIRAnd finally become machine code

Analysis output AST

  swiftc main.swift -dump-parse    // Analyze the output AST
  
  swiftc main.swift -dump-ast      // Analyze and check the type output AST
  
  swiftc main.swift -emit-silgen   // Generate intermediate language (SIL), not optimized

  swiftc main.swift -emit-sil      // Generate intermediate language (SIL), optimized
  
  swiftc main.swift -emit-ir       // Generate LLVM intermediate language (.ll file)

  swiftc main.swift -emit-bc       // Generate LLVM intermediate language (.bc file)

  swiftc main.swift -emit-assembly // Generate assembly

  swiftc -o main.o main.swift      // Compile to generate an executable.out file
Copy the code

SIL file analysis

First, generate SIL files. This section describes two ways to generate SIL files

  • Terminal display directly, input in terminalswiftc main.swift -emit-sil

This terminal display, if you feel uncomfortable. So there’s another way to generate it directly.silfile

  • Script generated.silfile

Here’s a quick look at the main.sil file

Specific each line is not detailed to everyone analysis, if you have questions can leave a message below. aboutSILGrammar rules, if you do not understand please checkSIL Official Documentation

  • s4main9LGTeacherCACycfCIs based onLGTeacher.__allocating_init()A global search generated by obfuscations4main9LGTeacherCACycfCTo locate the function implementation,SILThe source code is as follows

__allocating_init()It mainly realizes two functions:alloc_ref $LGTeacherA block of memory is allocated on the heap and then associated according to the meta-type.LGTeacher.init()Initialize all attributes

Alloc_ref $LGTeacher Details can be found in the SIL official documentation

The normal Swift class allocates memory on the heap via alloc_ref, whereas the Swift class with Objc identity allocates memory via +allocWithZone:

Class initialization process

From the above analysis of the instantiated object SIL source code, there is a general understanding. The first step is to allocate memory, and then initialize all attributes. The following through assembly + Swift source code to explore in detail

SwiftClass assembly

To create aLGTeacherClass, then create an instantiated object of that type and run the code with a breakpointCompile results andSILThe result of the analysis is the same as the callLGTeacher.__allocating_init()Up and down on thecontrol+Step intoEnter theLGTeacher.__allocating_init()methods

  • swift_allocObject= to open up memory
  • init(): Initializes all attributes

Assembly jump inside an assembly address, at this time can not track down, can only go to the bottom source code to find, will be explored in detail

SwiftIn the inheritanceNSObjectThe class of assembly

To create aLGTeacherClass inherited fromNSObject, then create an instantiated object of that type and run the code with a breakpoint

  • objc_allocWithZone:OCThe underlying method, the function of the method is to open up memory
  • "init"Through:objc_msgSendMode to send messages

Conclusion: The initialization process of a class basically requires two steps

  • Allocating memory
  • throughinitMethod completes the initialization of all properties

swift_allocObjectTo explore the

Search globally for swift_allocObject in the Swift underlying source code and locate the implementation of swift_allocObject in the heapObject. CPP file

Swift_allocObject has three parameters metadata, requiredSize and requiredAlignmentMask

  • metadata: meta type, equivalent toOCIn theisa
  • requiredSize: Specifies the memory size to be allocated
  • requiredAlignmentMask: byte alignment, for example8Byte alignment

Swift_allocObject calls the CALL_IMPL method and enters the CALL_IMPL method

CALL_IMPL wraps swift_allocObject as _swift_allocObject_. The global search for _swift_allocObject_ looks like this

_swift_allocObject_There are two main methods implemented in theswift_slowAllocandHeapObject(metadata)

swift_slowAllocTo explore the

swift_slowAllocMethod is calledmallocTo open up memory

Swift object memory structure

HeapObject (metadata)

Figure showsHeapObjectThere are two variablesmetadataandrefCounts.metadataIt’s a pointer so it holds an address,metadatasimilarOCIn theisa.refCountsReference counting

Summary: Swift object memory allocation process

  • __allocating_init –> swift_allocObject –> _swift_allocObject_ –>

swift_slowAlloc –> malloc

  • SwiftObject memory structureHeapObject (OC objc_object), has two properties: one ismetadata, one isrefCount, occupied by default16Size in bytes.

Probe into Swift class structure

The metadata type is HeapMetadata, so let’s explore the HeapMetadata structure

HeapMetadataTo explore the

HeapMetadataisTargetHeapMetadataAlias for this type, click enterTargetHeapMetadatastructure

TargetHeapMetadataTo explore the

The source code in the figure shows:TargetHeapMetadatainheritanceTargetMetadataIf it’s a pureSwiftClass, then the type isMetadataKind, if necessary withobjcInteraction, then the type passed in isisa

MetadataKindTo explore the

Uint32_t MetadataKind is a uint32_t type defined as follows

It is obvious that MetadataKind does not match the structure of the class we are looking for, so we can only look for TargetHeapMetadata’s parent TargetMetadata

TargetMetadataTo explore the

TargetMetadataThere is more code in the structure, guess because depending on the differenceMetadataKindCreate different types after searching as shown below

In the getTypeContextDescriptor method, MetadataKind is used to distinguish between different types. TargetClassMetadata may be the base class of the metatype. If it is of Class type, the current pointer this is forcibly converted to TargetClassMetadata

TargetClassMetadataTo explore the

TargetClassMetadata inherits TargetAnyClassMetadata, and TargetClassMetadata itself has a lot of attributes. Now we need to find all the attributes, so we need to find all the attributes in the parent class

TargetAnyClassMetadataTo explore the

  • TargetAnyClassMetadatainheritanceTargetHeapMetadata.TargetHeapMetadatainheritanceTargetMetadata
  • TargetAnyClassMetadataThe structure of theSuperclass.CacheData[2].DataAnd so on attributes, very familiar feeling andOCClass structure is similar in

The data structure of the sorted Swift class is as follows

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

So far, we have obtained the data structure of Swift class by means of source code

Pointer rebinding validates the data structure

Through the analysis of the underlying source code, we already know the structure of the instance object and the structure of the class. Now, the method of pointer rebinding is used to verify whether the results of the next study are correct

Rebinding of an instance object

First define an instance object structure HeapObject, then create an instance object of type LWTeacher. The following code

struct HeapObject{
    var metadata:UnsafeRawPointer //UnsafeRawPointer: native pointer in swift
    var refCount:UInt64
}

class LWTeacher{
    var age :Int = 10
    var name :String = "Ha ha"
}
 
let lw = LWTeacher(a)Copy the code

The address in the instance variable lw points to the HeapObject structure, so all we need to do is rebind the lW pointer to the HeapObject structure type. The following code

let lw = LWTeacher(a)// Get a native pointer to the instance object
let objcRoWPointer = Unmanaged.passRetained(lw as AnyObject).toOpaque(a)print(objcRoWPointer)
// Bind the native pointer to type HeapObject
let objcPtr = objcRoWPointer.bindMemory(to: HeapObject.self, capacity: 1)
// objcptr. pointee Access pointer this is the syntax in swift
print(objcPtr.pointee)
Copy the code

The result and LLDB debugging result are as follows

  • objcRoWPointerIs a native pointer of typeUnsafeMutableRawPointerType, instance variablelwisLWTeacherType, but they store the same address. The difference is the type
  • To view0x0000600001bcaee0The contents of the address and the final printed resultHeapObject(metadata: 0x0000000101342198, refCount: 8589934595)It’s the samerefCount: 8589934595becauserefCountUsing aUInt64Type is received and converted to16The result of base is0x0000000200000003

It verifies that the instance object in Swift is HeapObject structure in nature

Class rebinding

Start by defining the structural Metadata of a class. The following code

struct Metadata{
var kind: Int
var superClass: UnsafeRawPointer
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

Next, we continue to verify the class rebinding by first retrieving the metadata in the HeapObject via objcptr.pointee.metadata, since metadata refers to the structure of the class. The following code

/ / objcPtr. Pointee. Metadata memory address, to rebind it into metadata types
//MemoryLayout
      
       . Stride Indicates the size of Metadata
      
let metadataStr = objcPtr.pointee.metadata.bindMemory(to: Metadata.self, capacity: MemoryLayout<Metadata>.stride)
print(metadataStr.pointee)
Copy the code

The result and LLDB debugging result are as follows

MetadataPrint out the results andlldbDebugging results after16and10The conversion to base is the same. And it also proved thatSwiftThe essence of a class inMetadataThe structure of the body

conclusion

Through Swift learning, we can roughly understand that Swift and OC are still very different. We can compare and think in the learning process, but try not to use the idea of OC. Reading the underlying code may get confusing at first, and you may even want to give up, but it’s a process you have to go through. In the process of learning, the official document is a very important material. You can refer to it if you don’t understand it. Finally, this idea of rebinding needs to be grasped and understood. Swift’s learning process is still going on, and I hope it can continue