IOS swift structure and class

1, Structure

In Swift's standard library, the vast majority of exposed types are structures, while enumerations and classes account for only a small part. For example, Bool, Int, Double, String, Array, Dictionary and other common types are structures.

We now define a structure.

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

let p = SHPerson(age: 18, name: "Coder_Zhang San")

(slide to show more)

All structures have an initializer (initializer, initialization method, constructor, constructor) automatically generated by the compiler. In the above code, all member values can be passed in to initialize all members (Stored Property).

1. Initializer of structure

Depending on the situation, the compiler may generate multiple initializers for the structure. The premise is to ensure that all members have initial values.

2. Custom initializer

Once the initializer is customized when defining a structure, the compiler will not automatically generate other initializers for you.

struct SHPerson {
var age: Int
var name: String

init(age: Int) {
self.age = age
self.name = "Coder_Zhang San"
    }
}

let p = SHPerson(age: 0)

(slide to show more)

When initializing a structure, we must ensure that all members of the structure have values. Therefore, when we set the initial value of a member variable of the structure, the generated initializer can not pass the parameter assignment of the member variable.

3. Memory structure of structure

Let's look at the following structure. SHPerson has three members: age, weight and sex.

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

(slide to show more)

Print out the number of memory alignment bytes and the memory occupied. In a 64 bit system, Int accounts for 8 bytes and Bool accounts for one byte in the structure, so SHPerson accounts for 17 bytes in total. However, because the memory alignment principle (8 bytes) should be observed, the system will allocate 24 bytes to store SHPerson.

2, Class

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

1. Class initializer

Specify initializer

When a member of a class has no initial value, you must customize the 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")

(slide to show more)

If all members of a class specify initial values when they are defined, the compiler will generate a parameterless initializer for the class, and the initialization of members is completed in this initializer.

class SHPerson {
var age: Int = 18
var name: String = "Coder_Zhang San"
}

let p = SHPerson()

(slide to show more)

Failable initializer

When the initialized value does not meet a certain condition, we need to return a nil to the initialization method, so we can add an option after init to modify it.

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))")

(slide to show more)

Print results:
p1 - nil
p2 - Optional(_1_Structure and class.SHPerson)

(slide to show more)

For example, when SHPerson is under the age of 18, he returns nil and belongs to a minor.

Necessary initializer

The necessary initializer needs to be decorated with required before init.

class SHPerson {
var age: Int
var name: String
// The parent class defines the necessary implementation initializers
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")
    }

// The child class must implement the necessary initializers of the parent class
required init(age: Int, name: String) {
fatalError("init(age:name:) has not been implemented")
    }
}

(slide to show more)

As shown in the code, when required is modified before init, all subclasses of this class must implement the initializer.

Convenient initializer

We can provide a convenient initializer for classes. The convenient initializer needs to be modified with convenience before init.

class SHPerson {
var age: Int
var name: String

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

convenience init() {
self.init(age: 18, name: "Coder_ Zhang San")
    }
}

(slide to show more)

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

2. 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 (in fact, it can also be understood as a pointer type). The most intuitive difference between them is that the storage locations are different: generally, 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() {
let point = SHPoint()
let person = SHPerson()
}

(slide to show more)

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

3, Value type

Assigning a value type to var, let or passing parameters to a function directly copies all the contents. It is similar to the operation of copy and paste on files, which produces a new copy of files. It belongs to deep copy.

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

var p1 = SHPoint()
var p2 = p1;

p2.x = 6

print("p1 - \(p1)")
print("p2 - \(p2)")
Print results:
p1 - SHPoint(x: 4, y: 8)
p2 - SHPoint(x: 6, y: 8)

We can see that after modifying the x of p2, it has no effect on p1, which belongs to deep copy. Let's look at the print results of the array.

var a1 = [1, 2, 3]
var a2 = a1
a2.append(4)
a1[0] = 2
print(a1)
print(a2)
Print results:
[2, 2, 3]
[1, 2, 3, 4]

In the Swift standard library, in order to improve performance, String, Array, Dictionary and Set adopt the Copy On Write technology. For example, the copy operation can only be performed when there is a "write" operation.

For the assignment operation of standard library value type, Swift can ensure the best performance, so it is not necessary to avoid assignment in order to ensure the best performance.

Suggestion: if it does not need to be modified, try to define it as let.

4, Reference type

To assign a reference to var, let, or pass a parameter to a function is to copy the memory address. It is similar to making a double for a file (shortcut, link), pointing to the same file. It belongs to shallow copy.

class SHPerson {
var age: Int = 18
var name: String = "Coder_Zhang San"
}

let p1 = SHPerson()
let p2 = p1

print("p1-age: \(p1.age)")
p2.age = 20
print("p1-age: \(p1.age)")
print("p2-age: \(p2.age)")

(slide to show more)

Print results:
p1-age: 18
p1-age: 20
p2-age: 20

Object's heap space application process

In Swift, to create an instance object of a class and apply for memory from the heap space, the general 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 in Mac and iOS always allocates a multiple of 16.

  • class_getInstanceSize: returns the size of the class instance.
  • malloc_size: the size of memory allocated by the system.
class CGPoint  {
var x = 11
var y = 22
var test = true
}
var p = CGPoint()
print(class_getInstanceSize(CGPoint.self))
print(malloc_size(unsafeBitCast(p, to: UnsafeRawPointer.self)))

(slide to show more)

Print results:
40
48
  • Through printing, it is known that the size of CGPoint is 40 bytes, and the memory size allocated by the system is 48 bytes.
  • In CGPoint, x accounts for 8 bytes, y for 8 bytes and test for 1 byte, so we see 17 bytes at present. However, because the class is stored in the heap space, there will be 8 bytes in front of it to store type information, 8 bytes to store reference count, plus the above, a total of 33 bytes. According to the memory alignment principle (8 bytes), the size of CGPoint is 40 bytes.
  • Because the malloc function in Mac and iOS always allocates a multiple of 16, the system will eventually allocate 48 bytes of memory for CGPoint.

5, Structure and class selection

The use of structs and classes is very similar. 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 a structure, such as Model. If the defined data structure is complex, it is recommended to use classes, such as when polymorphism is needed.

  • The memory of the structure is allocated in the stack space. When the structure runs out, the memory will be automatically released without additional processing.
  • Class memory is allocated in heap space. The system needs to allocate and destruct the memory size of the class. Compared with the structure, the performance will be consumed.

StructVsClassPerformance demo test

We can intuitively test the time allocation of the current structure and class through the demo of StructVsClassPerformance on github.

The specific code will not be posted. Let's take a look at the calling method and printing results:

Tests.runTests()
Print results:
Running tests

class (1 field)
9.566281178005738

struct (1 field)
6.391943185008131

class (10 fields)
10.430800677015213

struct (10 fields)
6.610909776005428

It can be seen intuitively from the printing results that the time allocation of structure comparison class is nearly twice as fast.

6, Swift principle extension

1. Swift code compilation process

The back-end of iOS development language, whether OC or Swift, is compiled through LLVM, as shown in the following figure:

OC compiles into IR through the clang compiler, and then generates executable files O (here is our machine code). Swift is compiled into IR by swift compiler, and then generates executable files.

  1. First, Swift Code is parsed into Parse (abstract syntax tree) through - dump Parse semantic analysis.
  2. Parse performs semantic analysis through - dump AST to analyze whether the syntax is correct and safe.
  3. Seam will then downgrade the Swift Code to SILGen (Swift intermediate code), which is divided into Raw SIL and optimized SIL (OPT canonical SIL).
  4. The optimized SIL will be downgraded from LLVM to IR, and then compiled into machine code by back-end code.

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

Analysis output AST:

// Analysis output AST
swiftc main.swift -dump-parse

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

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

// Generated 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 executable out file
swiftc -o main.o main.swift

(slide to show more)

Compile the following code into sil Code:

import Foundation

class SHPerson {
var age = 18
var name = "Coder_Zhang San"
}

let p = SHPerson()

Terminal cd to project main Swift directory, enter swift c main Swift - emit SIL and press enter to generate a main SIL file, and the SIL code will be output at the terminal. SIL codes are as follows:

In fact, there are corresponding documents for the syntax description of SIL. The address where the document description is pasted here: SIL reference document

2. Initialization process of assembly exploration class

Next, we view the class initialization process through assembly. We make a breakpoint as follows:

Next, open assembly debugging

Through assembly view, SHPerson will be called at the bottom during initialization__ allocating_init function, then__ allocating_ What did init do? Follow in and have a look.

Let the breakpoint go to__ allocating_init line of code, hold down the control key and click the down button.

As you can see, enter__ allocating_ After the internal implementation of init, it is found that it will call a swift_ The allocobject function is lost when the assembly continues.

Next, let's look at the source code. The source code can go to the download address of - swift source code under Apple's official website. Open the downloaded swift source code with VSCode and search swift globally_ Allocobject this function.

In heapobject Swift found in cpp file_ Implementation of allocobject function, and in swift_ Above the implementation of the allocobject function, there is a_ swift_allocObject_ Function implementation.

// The first parameter is metadata.
// The second parameter is the size of the allocated memory
// The third parameter, memory alignment, is generally 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;
}

(slide to show more)

A swift is called inside the function_ Slowalloc function, let's take a look at swift_ Internal implementation of 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;
}

(slide to show more)

swift_ The interior of the slowalloc function is to allocate memory, such as malloc. Therefore, it confirms the fourth point: the process of applying for heap space by reference type - > object.

3. Source code structure of swift class

1. Differentiated call between OC and Swift

In call_ swift_allocObject_ Function has a parameter, HeapMetadata named metadata. The following is the code process followed by HeapMetadata:

//  HeapMetadata is the alias of TargetHeapMetadata, and InProcess is generic.
using HeapMetadata = TargetHeapMetadata<InProcess>;

(slide to show more)

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
};

(slide to show more)

Here is the compatibility between OC and Swift. When calling the TargetHeapMetadata function of, if it is an OC class, the parameter is an isa pointer, otherwise it is a MetadataKind type. MetadataKind is a uint32_ Type of T.

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,
};

(slide to show more)

The types of MetadataKind are 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

(slide to show more)

2. The underlying source code structure of swift class

Next, we find the inheritance TargetMetadata of TargetHeapMetadata (structure inheritance is allowed in C + +). The getTypeContextDescriptor function is found in the TargetMetadata structure. The code is 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:
return nullptr;
    }
  }

(slide to show more)

You can see that when kind is a Class, you will get a pointer named TargetClassMetadata. Let's take a look at the implementation of TargetClassMetadata:

Finally, we see something familiar. We are looking at its inheritance TargetAnyClassMetadata structure. We can see superclass, isa, etc.

3. The underlying source code structure of swift class

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
}

(slide to show more)

Next, let's do a test to view the memory structure of Swift class through lldb. Since it is at the bottom of Swift_ swift_allocObject_ The function returns the pointer type of HeapObject. Let's take a look at the structure of 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() = 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__
 };

(slide to show more)

After knowing the source code structure of HeapObject, we also imitate the source code in a false way. We define a HeapObject ourselves. refcounted1 and refcounted2 can be ignored first. Regardless, we mainly look at metadata.

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

Next, we convert the SHPerson class into a HeapObject structure and print it through lldb to view its memory structure.

class SHPerson {
var age = 18
var name = "Coder_Zhang San"
}

let p = SHPerson()

// Convert SHPerson to HeapObject pointer
let p_raw_ptr = Unmanaged.passUnretained(p as AnyObject).toOpaque()
let p_ptr = p_raw_ptr.bindMemory(to: HeapObject.self, capacity: 1)
// Will p_ptr pointer is converted to the pointer type of HeapObject and the memory structure of HeapObject is printed
print(p_ptr.pointee)

(slide to show more)

Print results:
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)

(slide to show more)

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

Next, I need to print out the memory structure of metadata in HeapObject 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()

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 the HeapObject to the metadata type and convert it to the metadata pointer type. Then the size of the data type can be measured by MemoryLayout.
let metadata = p_ptr.pointee.metadata.bindMemory(to: Metadata.self, capacity: MemoryLayout<Metadata>.stride).pointee
print(metadata)

(slide to show more)

Print results:
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)

(slide to show more)

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

Original link: https://juejin.cn/post/7046043638781968421

- END -

Added by R_P on Fri, 14 Jan 2022 15:13:40 +0200