First, structure

In Swift’s standard library, the vast majority of exposed types are structures, while enumerations and classes make up only a small portion. Common types such as Bool, Int, Double, String, Array, and Dictionary are all structures.

We now define a structure.

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

let p = SHPerson(age: 18, name: "Coder_ zhang SAN")
Copy the code

All constructs have an initializer (initializer, initializer, constructor, constructor) automatically generated by the compiler. In the code above, you can pass in all member values to initialize all members (Stored properties, Stored properties).

1. Initializer for structure

The compiler may generate more than one initializer for a structure, depending on the situation. The premise is to ensure that all members have initial values.

2. Customize initializers

Once you customize an initializer when defining a structure, the compiler does not help automatically generate other initializers.

struct SHPerson {
    var age: Int
    var name: String

    init(age: Int) {
        self.age = age
        self.name = "Coder_ zhang SAN"}}let p = SHPerson(age: 0)
Copy the code

When we initialize a structure, we must ensure that all the members of the structure are valued, so when we set the initial value of a member variable of the structure, the generated initializer can be assigned without passing the parameter of the member variable.

3. The memory structure of the structure

SHPerson has age, weight, and sex members.

struct SHPerson {
    var age: Int
    var weight: Int
    var sex: Bool
}

print(MemoryLayout<SHPerson>.size)         / / 17
print(MemoryLayout<SHPerson>.stride)       / / 24
print(MemoryLayout<SHPerson>.alignment)    / / 8
Copy the code

It prints the number of aligned bytes and the amount of memory it occupies. On 64-bit systems, Int is 8 bytes and Bool is 1 byte, so SHPerson is 17 bytes, but since memory alignment rules are followed (8 bytes), So the system allocates 24 bytes to store the SHPerson.

Second, the class

The definition of a class is similar to that of a structure, but the compiler does not automatically generate initializers for classes that can pass in member values.

1. Class initializer

1.1 Specifying an initializer

When a member of a class has no initial value, you must define a custom initializer to initialize the member value.

class SHPerson {
    var age: Int
    var name: String

    init(age: Int.name: String) {
        self.age = age;
        self.name =name; }}let p = SHPerson(age: 18, name: "Coder_ zhang SAN")
Copy the code

If all members of a class have specified initializers when they are defined, the compiler generates an initializer for the class with no arguments, and members are initialized in this initializer.

class SHPerson {
    var age: Int = 18
    var name: String = "Coder_ zhang SAN"
}

let p = SHPerson(a)Copy the code

1.2 Failable initializer

We need to return nil to the initializer if it doesn’t meet a condition, so we can add an option to init.

class SHPerson {
    var age: Int
    var name: String

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

let p1 = SHPerson(age: 16, name: "Coder_ zhang SAN")
let p2 = SHPerson(age: 18, name: "Coder_ li si")
print("p1 - \(String(describing: p1))")
print("p2 - \(String(describing: p2))")
Copy the code
Print result: P1- nil
p2 - Optional_1_ struct and class.SHPerson)
Copy the code

For example, SHPerson returns nil when it is less than 18 years old, belonging to a minor.

1.3 Necessary initializers

Required initializers need to be decorated with required before init.

class SHPerson {
    var age: Int
    var name: String
    // The parent class defines the initializer that must be implemented
    required init(age: Int.name: String) {
        self.age = age
        self.name = name
    }
}

class SHStudent: SHPerson {
    var height: Int

    init(height: Int) {
    self.height = height
        super.init(age: 18, name: "Coder_ zhang SAN")}// Subclasses must implement the necessary initializers for their parent class
    required init(age: Int.name: String) {
        fatalError("init(age:name:) has not been implemented")}}Copy the code

As the code shows, when you decorate required before init, all subclasses of the class must implement the initializer.

1.4 Convenient initializer

We can provide a convenience initializer for a class that needs to be modified by convenience before init.

class SHPerson {
    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: "Coder_ zhang SAN")}}Copy the code

As the code shows, the convenience initializer must call another initializer from the same class, and must eventually call a designated initializer.

The essential difference between structure and class

The essential difference between a structure and a class is that a structure is a value type and a class is a reference type. One of the most obvious differences between them is where they are stored: typically, value types are stored on the stack and reference types are stored on the heap.

class SHPerson {
    var age = 18
    var height = 180
}

struct SHPoint {
    var x = 0;
    var y = 0;
}

func test(a) {
    let point = SHPoint(a)let person = SHPerson()}Copy the code

After SHPoint is initialized and assigned to point, SHPoint’s memory data is placed directly on the stack. After SHPerson is initialized and assigned to person, person is just a reference address. The memory data stored in this address is SHPerson’s memory address, which is placed in the heap space.

Value types

When a value type is assigned to a var, let, or function, it copies everything directly. This operation is similar to copying or paste a file to create a new copy of the file. It is a deep copy.

struct SHPoint {
    var x = 4;
    var y = 8;
}

var p1 = SHPoint(a)var p2 = p1;

p2.x = 6

print("p1 - \(p1)")
print("p2 - \(p2)")
Copy the code
Print result: P1- SHPoint(x: 4, y: 8)
p2 - SHPoint(x: 6, y: 8)
Copy the code

We can see that after modifying x of P2, there is no effect on P1, which is a deep copy. Let’s look at the print of the array.

var a1 = [1.2.3]
var a2 = a1
a2.append(4)
a1[0] = 2
print(a1)
print(a2)
Copy the code
Print result: [2.2.3]
[1.2.3.4]
Copy the code

In the Swift standard library, in order to improve performance, String, Array, Dictionary, and Set adopt the technology of Copy On Write, for example, only when there is a “Write” operation, the actual Copy operation will be performed.

Swift ensures optimal performance for assignment operations of library value types, so there is no need to avoid assignment in order to ensure optimal performance.

Suggestion: If you do not need to modify it, try to define it as let.

5. Reference types

When a reference is assigned to a var, let, or function, it makes a copy of the memory address. Similar to making a double of a file (shortcut, link) that points to the same file. It is a shallow copy.

class SHPerson {
    var age: Int = 18
    var name: String = "Coder_ zhang SAN"
}

let p1 = SHPerson(a)let p2 = p1

print("p1-age: \(p1.age)")
p2.age = 20
print("p1-age: \(p1.age)")
print("p2-age: \(p2.age)")
Copy the code
Print result: P1-age: 18
p1-age: 20
p2-age: 20
Copy the code

Object heap space application process:

In Swift, to create an instance object of class, to allocate memory to the heap space, the process is as follows:

  1. Class.__allocating_init()
  2. libswiftCore.dylib: swift_allocObject
  3. libswiftCore.dylib: swift_slowAlloc
  4. libsystem_malloc.dylib: malloc

The malloc function on Mac and iOS always allocates a multiple of 16.

  • class_getInstanceSize: Returns the size of the class instance.
  • malloc_size: Specifies the memory size allocated by the system.
class CGPoint  {
var x = 11
var y = 22
var test = true
}
var p = CGPoint(a)print(class_getInstanceSize(CGPoint.self))
print(malloc_size(unsafeBitCast(p, to: UnsafeRawPointer.self)))
Copy the code
Print result:40
48
Copy the code
  • According to the print, the size of CGPoint is 40 bytes, and the memory size of CGPoint allocated by the system is 48 bytes.

  • In CGPoint, x is 8 bytes, Y is 8 bytes, and test is 1 byte, so we are currently looking at 17 bytes. But because the class is stored in heap space, it is preceded by 8 bytes for the type information, 8 bytes for reference counting, and the faces, which adds up to 33 bytes. According to memory alignment rules (8 bytes), cgPoints are 40 bytes in size.

  • Since the malloc function in Mac and iOS always allocates a multiple of 16, the system eventually allocates 48 bytes of CGPoint memory.

Structure and class selection

Structs and classes are used in a similar way, so is it better to use structs or classes in normal development? In this case, if the defined data structure is relatively simple, it is recommended to use the structure, such as Model. Classes are recommended for more complex data structures, such as polymorphisms.

  • The structure’s memory is allocated in stack space, and when the structure runs out, the memory is automatically freed without any additional processing.
  • Class memory allocation in the heap space, the system needs to allocate the memory size of the class and other operations such as destruction, relative to the structure, the performance of the cost.

StructVsClassPerformance Demo test:

We can visually test the time allocation of current structures and classes with the demo StructVsClassPerformance on Github.

The specific code will not be posted, let’s see how to call and print the result:

Tests.runTests()
Copy the code
Print result:Running tests

class (1 field)
9.566281178005738

struct (1 field)
6.391943185008131

class (10 fields)
10.430800677015213

struct (10 fields)
6.610909776005428
Copy the code

As can be seen intuitively from the printed results, the structure is nearly twice as fast as the time allocation of the class.

Vii. Swift Principle expansion

1. Swift code compilation process

Both OC and Swift are developed for iOS, and the backend is compiled by LLVM, as shown below:

OC is compiled by the CLang compiler into IR, which is then regenerated into the executable.o(in this case our machine code). Swift is compiled into IR by Swift compiler and then generates executable files.

  1. First of all,Swift Codeafter-dump-parseCarry out semantic analysis and parse intoParse(Abstract syntax tree).
  2. Parseafter-dump-astPerform semantic analysis to check whether the syntax is correct and safe.
  3. SeamAfter theSwift CodeWill degrade toSILGen(Swift intermediate code) forSILGenSubdivided into native (Raw SIL) and optimized (SIL Opt Canonical SIL).
  4. optimizedSILWill be downgraded from LLVM toIRThe downgrade intoIRThe backend code is then compiled into machine code.

The above is the Swift compilation process, and the following is the command for the compilation process.

Analysis output AST:

// Analyze the output AST
swiftc main.swift -dump-parse

// Analyze and check the type output AST
swiftc main.swift -dump-ast

// Generate intermediate language (SIL), not optimized
swiftc main.swift -emit-silgen

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

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

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

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

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

Compile the following code into SIL code:

import Foundation

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

let p = SHPerson(a)Copy the code

Go to the main. Swift directory of the project and type swiftc main. swift-emit -sil and press Enter. A main. The SIL code is as follows:

The syntax of SIL is also documented. Here is the address of the documentation: SIL reference document

2. Compile the initialization process of the exploration class

Next, we can look at the initialization process of the class through assembly. We can break the following:

Next, turn on assembly debugging.

When SHPerson is initialized, the underlying function __allocating_init is called. What does __allocating_init do?

Let the breakpoint go to the line __allocating_init, hold down the Control key, and click the down button.

As you can see, the internal implementation of __allocating_init calls a swift_allocObject function and gets lost while continuing with assembly.

Let’s take a look at the source code. Source code can go to apple’s official website -swift source download address. Use VSCode to open the downloaded Swift source code and search swift_allocObject globally.

Find the implementation of the swift_allocObject function in the heapObject. CPP file, and above the implementation of the swift_allocObject function, there is an implementation of the _swift_allocObject_ function.

// The first argument, metadata.
// The size of the allocated memory
// The third argument, memory alignment, is usually 7 because 8-byte alignment is observed
static HeapObject *_swift_allocObject_(HeapMetadata const *metadata,
                                    size_t requiredSize,
                                    size_t requiredAlignmentMask) {
assert(isAlignmentMask(requiredAlignmentMask));
auto object = reinterpret_cast<HeapObject * >(
   swift_slowAlloc(requiredSize, requiredAlignmentMask));

// NOTE: this relies on the C++17 guaranteed semantics of no null-pointer
// check on the placement new allocator which we have observed on Windows,
// Linux, and macOS.
new (object) HeapObject(metadata);

// If leak tracking is enabled, start tracking this object.
SWIFT_LEAKS_START_TRACKING_OBJECT(object);

SWIFT_RT_TRACK_INVOCATION(object, swift_allocObject);

return object;
}
Copy the code

A swift_slowAlloc function is called inside the swift_slowAlloc function.

void *swift::swift_slowAlloc(size_t size, size_t alignMask) {
void *p;
// This check also forces "default" alignment to use AlignedAlloc.
if (alignMask < = MALLOC_ALIGN_MASK) {
#if defined(__APPLE__)
 p = malloc_zone_malloc(DEFAULT_ZONE(), size);
#else
 p = malloc(size);
#endif
} else {
 size_t alignment = (alignMask = = ~(size_t(0)))
                        ? _swift_MinAllocationAlignment
                        : alignMask + 1;
 p = AlignedAlloc(size, alignment);
}
if (!p) swift::crash("Could not allocate memory.");
return p;
}
Copy the code

Inside the swift_slowAlloc function is to do some memory allocation operations, such as malloc. Therefore, the fourth reference type -> object request heap space process.

3. Swift class source structure

3.1. Differentiated calls between OC and Swift

The _swift_allocObject_ function is called with one argument, HeapMetadata named metadata. Here is the code process HeapMetadata follows:

// HeapMetadata is an alias of TargetHeapMetadata, and InProcess is generic.
using HeapMetadata = TargetHeapMetadata<InProcess>;
Copy the code
template <typename Runtime>
struct TargetHeapMetadata : TargetMetadata<Runtime> {
  using HeaderType = TargetHeapMetadataHeader<Runtime>;

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

There is compatibility for OC and Swift here. The TargetHeapMetadata function is called with an ISA pointer if it is an OC class, or a MetadataKind type if it is not. MetadataKind is a uint32_t type.

enum class MetadataKind : uint32_t {
#define METADATAKIND(name, value) name = value,
#define ABSTRACTMETADATAKIND(name, start, end)                                 \
  name##_Start = start, name##_End = end,
#include "MetadataKind.def"
  
  /// The largest possible non-isa-pointer metadata kind value.
  ///
  /// This is included in the enumeration to prevent against attempts to
  /// exhaustively match metadata kinds. Future Swift runtimes or compilers
  /// may introduce new metadata kinds, so for forward compatibility, the
  /// runtime must tolerate metadata with unknown kinds.
  /// This specific value is not mapped to a valid metadata kind at this time,
  /// however.
  LastEnumerated = 0x7FF};Copy the code

So the MetadataKind category is as follows:

name                       Value

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

3.2. Swift class underlying source code structure

Next we find an inheritance of TargetHeapMetadata, TargetMetadata (structs are allowed to inherit in C++). The getTypeContextDescriptor function is found in the TargetMetadata structure as follows:

ConstTargetMetadataPointer<Runtime.TargetTypeContextDescriptor>
  getTypeContextDescriptor() const {
    switch (getKind()) {
    case MetadataKind: :Class: {
      const auto cls = static_cast<const TargetClassMetadata<Runtime> * >(this);
      if (!cls->isTypeMetadata())
        return nullptr;
      if (cls->isArtificialSubclass())
        return nullptr;
      return cls->getDescription();
    }
    case MetadataKind: :Struct:
    case MetadataKind: :Enum:
    case MetadataKind: :Optional:
      return static_cast<const TargetValueMetadata<Runtime> * >(this)
          ->Description;
    case MetadataKind: :ForeignClass:
      return static_cast<const TargetForeignClassMetadata<Runtime> * >(this)
          ->Description;
    default:
      returnnullptr; }}Copy the code

When kind is a Class, we get a pointer named TargetClassMetadata. If kind is a Class, we get a pointer named TargetClassMetadata.

Finally, we see something familiar, we look at its successor TargetAnyClassMetadata structure, you can see superclass, ISA, etc.

3.3. Swift class underlying source code structure

Through the above analysis, we can conclude that the metadata data structure in Swift class is roughly 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

_swift_allocObject_ () returns the pointer type of the HeapObject.

 struct HeapObject {
   /// This is always a valid pointer to a metadata object.
   HeapMetadata const *__ptrauth_objc_isa_pointer metadata;

   SWIFT_HEAPOBJECT_NON_OBJC_MEMBERS;

 #ifndef __swift__
   HeapObject(a)= default;

   // Initialize a HeapObject header as appropriate for a newly-allocated object.
   constexpr HeapObject(HeapMetadata const *newMetadata)
     : metadata(newMetadata)
     , refCounts(InlineRefCounts::Initialized) {}// Initialize a HeapObject header for an immortal object
   constexpr HeapObject(HeapMetadata const *newMetadata,
                        InlineRefCounts::Immortal_t immortal)
   : metadata(newMetadata)
   , refCounts(InlineRefCounts::Immortal)
   { }

 #ifndef NDEBUG
   void dump() const SWIFT_USED;
 #endif

 #endif // __swift__
 };
Copy the code

After knowing the source structure of HeapObject, we also fake in fake gas imitation source code, define a HeapObject, the following refcounted1 and refcounted2 can be ignored first, regardless of the main look at metadata.

struct HeapObject {
    var metadata: UnsafeRawPointer
    var refcounted1: UInt32
    var refcounted2: UInt32
}
Copy the code

Next, we turn the SHPerson class into a HeapObject structure and print it through LLDB to see its memory structure.

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

let p = SHPerson(a)// Convert SHPerson to a HeapObject pointer
let p_raw_ptr = Unmanaged.passUnretained(p as AnyObject).toOpaque()
let p_ptr = p_raw_ptr.bindMemory(to: HeapObject.self, capacity: 1)
// Convert the p_ptr pointer to the HeapObject pointer type and print out the HeapObject memory structure
print(p_ptr.pointee)
Copy the code
Print result:HeapObject(metadata: 0x00000001000081a0, refcounted1: 3, refcounted2: 0)

(lldb) x/8g 0x00000001000081a0
0x1000081a0: 0x0000000100008168 0x00007fff806208f8
0x1000081b0: 0x00007fff20208aa0 0x0000803000000000
0x1000081c0: 0x00000001085040f2 0x0000000000000002
0x1000081d0: 0x0000000700000028 0x00000010000000a8

(lldb) x/8g 0x0000000100008168
0x100008168: 0x00007fff80620920 0x00007fff80620920
0x100008178: 0x00007fff20208aa0 0x0000a03100000000
0x100008188: 0x0000000108504090 0x00000001000032b0
0x100008198: 0x00007fff8152f3e0 0x0000000100008168
(lldb)
Copy the code

Through printing, we know that the essence of Swift class is the structure pointer of HeapObject, and we print out its memory layout in the form of X /8g.

Next, I need to print the memory structure of HeapObject metadata to try:

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
}

struct HeapObject {
    var metadata: UnsafeRawPointer
    var refcounted1: UInt32
    var refcounted2: UInt32
}

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

let p = SHPerson(a)let p_raw_ptr = Unmanaged.passUnretained(p as AnyObject).toOpaque()
let p_ptr = p_raw_ptr.bindMemory(to: HeapObject.self, capacity: 1)
// We bind the metadata in HeapObject to the metadata type and convert it to the pointer type of metadata. MemoryLayout can be used to measure the size of the data type.
let metadata = p_ptr.pointee.metadata.bindMemory(to: Metadata.self, capacity: MemoryLayout<Metadata>.stride).pointee
print(metadata)
Copy the code
Print result:Metadata(kind: 4295000432, 
         superClass: _TtCs12_SwiftObject, 
         cacheData: (140733732391584.140943646785536), 
         classFlags: 2, 
         instanceAddressPoint: 0, 
         instanceSize: 40, 
         instanceAlignmentMask: 7, 
         reserved: 0, 
         classSize: 168, 
         classAddressPoint: 16, 
         typeDescriptor: 0x0000000100003c6c, 
         iVarDestroyer: 0x0000000000000000)
(lldb)
Copy the code

We successfully print the values of member variables such as KIND, superClass, and cacheData.