Since Apple launched Swift in 2014, it has gone through several iterations of versions. Swift runs faster, is more efficient, and integrates many high-level language grammar features. This article first understands the similarities and differences between classes and structures and initialization of classes through cases, and analyzes the life cycle of classes through source code.

Introduction to classes and structures

Let’s look at the definitions of classes and structs:

Class ATTeacher {var age: Int var name: String init(age: Int, name: struct) String) { self.age = age self.name = name } }Copy the code

The same

  • Defines attributes that store values
  • Define methods
  • Define subscripts to provide access to their values using subscript syntax
  • Define initializers
  • useextensionTo expand the function
  • Follow a protocol to provide a function

The difference between

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

Legend analysis

Class (class)

Classes are reference types. This means that a variable of a class type does not store the specific instance object directly, but rather refers to the memory address where the specific instance is currently storedAfter variable T is assigned to T1, the figure is as follows:The above is that when class instance T is assigned to T1, they refer to the same memory address.

Structure (struct)

Structs are value types, and structs are very simple to define. Whereas class variables store addresses, value types store concrete instances (or concrete values).

struct ATStudent {
  var age: Int
  var name: String
}

var s = ATStudent(age: 18, name: Atom)
var s1 = s
Copy the code

In fact, reference types are equivalent to onlineExcelWhen we share the link with others, we can see the changes made by others. The value type is equivalent to localExcelWhen we put the localExcelWhen you pass it on to someone else, you make a copy of it to someone else, and their changes to the content are imperceptible to us.

Code debugging

Class (class)

Through the above definition analysis, it can be concluded thatclassIs the reference type, andstructIs a value type. Let’s verify the analysis with actual code. The definition of theclassThe code is the same as above, then run breakpoint:Initialize theATTeacherI’m going to assign to t, and I’m going to assign to ttAssigned tot1, obtained by printing outtandt1The memory address is the same, then change the instancetIn theageProperty value, outputt1In theageAnd look at the result.The results showed thatt1.ageIs equal to the20This verifies that the class is a reference type and that they refer to the same piece of memory address.

Structure (struct)

Again, change the above class to a structure and run it again:Found that the output is all values, modifys1.ageBefore,sands1All values are the same when modifieds1.ageLater,s1The value is updated, butsThe value of alpha is the same as it was beforestructIs a value type.

Memory area concept diagram

Another obvious difference between reference types and value types is where they are stored: typically, value types are stored on the stack and reference types are stored on the heap. Let’s take a look at the common memory region distribution:To verify this, define a function as follows:

func test() {
  var age: Int = 10
  print(age)
}

test()
Copy the code

inprintMake a breakpoint and printageWhere tools are used to view variablesageStorage area in memory.

Storage information for memory areas
  • Stack: The context in which local variables and functions are run
  • Heap: Stores all objects
  • Global: Stores Global variables. Constant; Code section
  • Segment & Section: A Mach-o file has multiple segments, each with a different function. Each Section is then divided into smaller sections
  • TEXT. TEXT: machine code
  • Text. cstring: hardcoded string
  • Text. const: initialized constant
  • Data. DATA: variable (static/global) DATA initialized. Const: constant that has not been initialized
  • Data.bss: uninitialized (static/global) variable
  • DATA.common: Symbol declaration that is not initialized
Distribution of classes and structures in memory

Use the frame variable command to see how variables in the structure are distributed in memory, again using the previous code

The help frame variable command is available in helpCopy the code

Breakpoint debug outputageThat’s the first address of the structure,nameinageincrease8Bytes. So the current structure in memory looks like this:When we initializeATTeacher“, will request 24 bytes of memory on the stack,ageandnameThe addresses in memory are contiguous. After the function is executed, the system will destroy the requested stack memory space, and the stack pointer will be restored to point to the location. Other things being equal, we are instructAdd aclassFor example, does this affect the distribution of structures in vivo?As you can see, adding classes does not change the originalstructMemory affects,ATPersonWill first allocate memory space on the heap to store variables on the stackpIn the. And again we putstructtoclassTo run again.It can be seen thatclassRequested heap memory space.

Class initialization

Current class compilers do not automatically provide member initializers by default, but they do provide default initializers for structures (provided we do not specify initializers ourselves).

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

Specify initializers & convenience initializers

Instances of classes and structures in Swift must be created with an appropriate initial value for all storage properties. So class ATPerson must provide a designated initializer, and we can also provide a convenience initializer for the current class (note: the convenience initializer must call another initializer from the same class).

Class ATPerson{var age: Int var name: String # String) {self.age = age self.name = name} # Convenience init() {self.init(18, "Atom")}}Copy the code
  • 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.
The sample

Let’s define a subclass initializer, combining sections 1 and 2 above. Otherwise the compilation will report an error

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 also very simple to write:

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

Necessary initializer

In front of the class initializerrequireModifier to indicate that all subclasses of the class must implement the initializerCompile an error if the subclass does not provide one.

Class life cycle

Compilation flowchart

iOSDevelop the language regardlessOCorSwiftThe back end is all throughLLVMCompiled, as shown below: OCthroughclangCompiler, compiles toIR, and then regenerate it into an executable file.o(This is our machine code),SwiftbySwiftThe compiler compiles toIR, and then generate the executable file. SwiftCompilation process:

  • SwiftThe code bydump parseParse into an abstract syntax tree
  • Generated by semantic analysisSwiftThe middle codeSIL(SIL includes both native and optimized)
  • byLLVMgenerateIR
  • It is then compiled into machine code from the back-end code (x86, ARM64…)

Swift compile command

Swift-dump-parse # analyze and check the type of output AST swiftc main. Swift-dump-ast # Generate intermediate language (SIL), Swiftc Main. Swift - EMIT - Silgen # Generate Intermediate Language (SIL) Swiftc main. Swift - EMIT -ir # Generate LLVM intermediate language (.bc file) swiftc Swift -emit-assembly # Compile to generate executable. Out file swiftc-o main.o main.swiftCopy the code

SIL code analysis

Start by defining a simple Swift class, and then generate the intermediate SIL code as follows:

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

Generate intermediate code SIL commands:

Swiftc-emit - SIL main.swift >./main.silCopy the code

Open the generated main.sil file and intercept some of the code

class ATTeacher { @_hasStorage @_hasInitialValue var age: Int { get set } @_hasStorage @_hasInitialValue var name: String {get set} @objc deinit init()} // main entry function // 0%~9% : registers that can be understood as SIL (virtual), and eventually run on the device will be mapped to the real register sil@main: $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 { bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>): // alloc_global: // s4main1tAA9ATTeacherCvp: Alloc_global @$s4main1tAA9ATTeacherCvp // id: alloc_global @$s4main1tAA9ATTeacherCvp %3 = global_addr @$s4main1tAA9ATTeacherCvp: $*ATTeacher: %7 %4 = metatype $@thick ATTeacher.Type // user: // function_ref atteacher.__allocating_init () // get the __allocating_init() pointer address %5 = function_ref @$s4main9ATTeacherCACycfC : $@convention(method) (@thick ATTeacher.Type) -> @owned ATTeacher // user: %6 // Use a 5% function pointer with metatype and return the result to 6% %6 = apply %5(%4) : $@convention(method) (@thick ATTeacher.Type) -> @owned ATTeacher // user: Store %6 to %3: $*ATTeacher // id: Int32, 0; // user: %9 = struct $Int32 (%8: struct $Int32) $Builtin.Int32) // user: %10 return %9 : $Int32 // id: %10 } // end sil function 'main'Copy the code

Detailed instructions on Swift SIL can be found in the official documentation.

There is a pointer to function_ref corresponding to s4main9ATTeacherCACycfC in the main function above. A search in the SIL file will locate the following functions:

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

To viewalloc_refOfficial documentation of the explanation alloc_refIn fact, it is to request memory space from the heap, if marked asobjcThe will toObjective-CThe initialization method of.

The sample analysis

In the initializationATTeacherMake a break point,debugOpened in assembly mode, you can see that the initialization isswift_allocObject Then put theATTeacherIntegration fromNSObjectRun it again and you can see that the initialization mode passesObjective-CThe way.

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

Swift source code analysis

Through the topSILThe code runs in conjunction with the instance breakpointSwiftWe saw that when we initialized the functionswift_allocObject, next downloadSwiftThe source code, through the global search can be locatedHeapObject.cppThe file has the following methods: swift_allocObjectCall the_swift_allocObject_._swift_allocObject_Call againswift_slowAlloc, and then global searchswift_slowAllocYou can see its function definition:You can see the call heremallocBasically the same asObjective-CSimilar to the initialization of themallocTo open up memory space.

The following points can be made from the above analysis:

  • _allocating_init~ >swift_allocObject~ >_swift_allocObject_~ >swift_slowAlloc~ >malloc
  • SwiftObject memory structureHeapObject(OC objc_object), has two properties: one isMetadata, one isRefCount, which occupies 16 bytes by default.

Through the source to locate the HeapObject, HeapMetadata found in the HeapObject structure, according to the namespace and find TargetHeapMetadata;

using HeapMetadata = TargetHeapMetadata<InProcess>;
Copy the code

Find the following definition from TargetHeapMetadata:

template <typename Runtime>
struct TargetHeapMetadata : TargetMetadata<Runtime> {
  using HeaderType = TargetHeapMetadataHeader<Runtime>;

  TargetHeapMetadata() = default;
  constexpr TargetHeapMetadata(MetadataKind kind)
    : TargetMetadata<Runtime>(kind) {}
#if SWIFT_OBJC_INTEROP
  constexpr TargetHeapMetadata(TargetAnyClassMetadata<Runtime> *isa)
    : TargetMetadata<Runtime>(isa) {}
#endif
};
Copy the code

According to the function definition, if it’s a pure Swift class it’s Metadata, if it’s interacting with objC it’s ISA, it’s pure Swift code and there’s objC code that generates different data types. The definition of kind in the source code is shown in the following table:

Name Value
Class 0x0
Struct 0x200
Enum 0x201
Optional 0x202
ForeignClass 0x203
Opaque 0x300
Tuple 0x301
Function 0x302
Existential 0x303
Metatype 0x304
ObjCClassWrapper 0x305
ExistentialMetatype 0x306
HeapLocalVariable 0x400
HeapGenericLocalVariable 0x500
ErrorObject 0x501
LastEnumerated 0x7FF

According to thekindWe found it.getTypeContextDescriptorFunction definition of:ifgetKindType isClass, will be strong intoTargetClassMetadataType, according toTargetClassMetadataWe can guess at the definition ofSwiftThe data structure of the class would look something like this:

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

The above is the result of the source code, the following example to verify that is not the case

The sample analysis

We can bind a pointer to a class to a custom HeapObject structure as follows:

struct HeapObject { var metadata: UnsafeRawPointer var refcounted1: UInt32 var refcounted2: UInt32 } class ATTeacher { var age: Int = 18 var name: String = "Atom"} var t = ATTeacher() let objcRawPtr = Unmanaged. PassUnretained (t as AnyObject).toopaque () let  objcPtr = objcRawPtr.bindMemory(to: HeapObject.self, capacity: 1) print(objcPtr.pointee)Copy the code

Then run:Metadata (HeapObject); metadata (HeapObject); metadata (HeapObject);

struct HeapObject { var metadata: UnsafeRawPointer var refcounted1: UInt32 var refcounted2: UInt32 } 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 } class ATTeacher { var age: Int = 18 var name: String = "Atom"} var t = ATTeacher() let objcRawPtr = Unmanaged. PassUnretained (t as AnyObject).toopaque () // Let objcPtr = objcRawPtr. BindMemory (to: heapObject. self, capacity: HeapObject. 1) print(objcptr.pointee) // Bind the pointer to objcptr.pointee. Metadata to the metadata structure type. // MemoryLayout objcPtr.pointee.metadata.bindMemory(to: Metadata.self, capacity: MemoryLayout<Metadata>.stride).pointee print(metadata)Copy the code

Run againThe code examples also verify that the data structure of Swift class is the Metadata obtained from the above source code analysis.

conclusion

You learned about classes and structures, class initialization, and class life cycles in Swift.

  • Similarities and differences in classes and structures
  • Concepts and usage of various initializers for class
  • The compilation process of the class is analyzed through SIL
  • Through source code analysis of the structure of the class and through the example to verify