This article has participated in the activity of “New person creation Ceremony”, and started the road of digging gold creation together.

preface

In order to “go black” in the iOS development industry, I had to pick up the long-abandoned Swift and make a full iOS developer

Classes and structures in Swift

/ / class
class RectClass {
    var height = 0.0
    var width = 0.0
}
/ / structure
struct RectStruct {
    var height = 0.0
    var width = 0.0
}

var rectCls = RectClass(a)var rectStrct = RectStruct(a)Copy the code

The difference between classes and structures

First, we all know the basic differences between classes and structures in Swift, as follows

Thing in common:
  1. Properties that define stored values

  2. Can define methods

  3. Can define subscripts to provide access to them using subscript syntax

  4. Ability to define initializers

  5. You can use Extension to extend functionality

  6. Ability to follow protocol to provide a function

Difference:
  1. Classes have inherited attributes, while structs do not
  2. Conversions enable you to examine and interpret instance types of classes at run time
  3. A class has a destructor that frees its allocated resources
  4. Reference counting allows multiple references to a class instance

In Swift, structs, classes, and enums are interchangeable for common use. Should you use structs or classes in real development? It is therefore necessary to explore the logic behind it, and this question leads to the difference between value types and reference types in Swift

Struct in Swift is a value type and class is a reference type, so we take these two types as representatives to elaborate in detail. Then how to demonstrate?

  1. In Swift, struct, enum, and tuple are typically value types, while Int, Double, Float, String, Array, Dictionary, and Set are all value types implemented by structures.

  2. In Swift, classes and closures are reference types.

stack & heap

There are two areas in RAM, the stack and the heap. In Swift, variables of value type are stored in the heap, and variables of reference type are stored in the stack

Basic memory distribution diagram

Stack: The context in which local variables and functions are run

Heap: Stores all objects

Global: Stores Global variables. Constant; Code section

In the LLDB debugging environment, you can run the following command to view the distribution of variables in memory

frame varibale -L xxx

Reference Type

Reference types, in which all instance variables share a copy of the data, mean that variables of a class type do not store a specific instance object directly, but rather refer to the memory address where the specific instance is currently stored.

See the code:

class LPTeacher {
		var age = 0
		var name = ""
}

 var t = LPTeacher(a)var t2 = t
 t.age = 27
 print("t.age -> \(t.age)")
 print("t2.age -> \(t2.age)")

//t.age -> 27
//t2.age -> 27

Copy the code
Found 1:

You can see that the variable names of the new object and the source object are different, but when you manipulate the internal data of the source object with the new object, the internal data of the source object is also affected

Next we need to use the following two LLDB debugging instructions to see the memory structure of the current variable

Po: The difference between p and Po is that Po only prints the corresponding value, while p returns the type of the value and the reference name of the command result

X /8g: Reads the value in memory

Unmanaged.passretained (t).toopaque () : obtains the value of the reference type variable

WithUnsafePointer (to: &t){print($0)} : Gets the address of the variable

As shown in the figure, Po variable t and t2, respectively, obtain the values of the variables

In Swift, we can also get the memory address that the variable points to and the variable address using the following method, as shown in the following code

// Prints the value of the reference type variable
 print(Unmanaged.passRetained(t).toOpaque())
 print(Unmanaged.passRetained(t2).toOpaque())
//0x0000600003db4000
//0x0000600003db4000
// An 8-byte memory address is also found
Copy the code
Found 2

It turns out that the value of both is the same 8-byte address. Note The two points to the same memory address

Use the command frame varibale -l XXX to get the following figure

Found 3

You’ll see that references to type variables point to the heap

Summary of reference types

In Swift, the assignment of a Reference type is Shallow Copy. The Reference Semantics is that the variable name of the new object is different from that of the source object, but its Reference (to the memory address) is the same. Therefore, when the new object is used to manipulate its internal data, The internal data of the source object is also affected. And the values of variables of reference types are stored in the heap

Value type

The most typical type of value is a Struct, and the definition of a structure is very simple. A value type stores a concrete instance (or value), whereas a class type’s variable stores an address.

See the code

struct LPTeacher {
		var age = 0
		var name = ""
}

 var t = LPTeacher(a)var t2 = t
 t.age = 27
 print("t.age -> \(t.age)")
 print("t2.age -> \(t2.age)")

//t.age -> 27
//t2.age -> 0

// Prints the address of the variable
print(withUnsafePointer(to: &t){print($0)})
print(withUnsafePointer(to: &t2){print($0)})

//0x00007ffeed7a4fc0
//0x00007ffeed7a4fa0
Copy the code
Found 1

As you can see, the new object and the source object are independent. When the properties of the new object are changed, the source object is not affected. Print the memory address of the value type variable so that you can see that the memory address of the two variables is not the same.

As shown, Po variable t and t2 are respectively

Found 2

You can see that two variables store values, unlike reference types, which store addresses

Use the command frame varibale -l XXX to get the following figure

Found 3

You can see that the value of a variable is stored on the stack and the first address of the value is not the same as the value of a reference type variable is stored on the heap

Value type summary

In Swift, the assignment of Value type is Deep Copy, and Value Semantics means that the new object and the source object are independent. When the attributes of the new object are changed, the source object will not be affected, and vice versa.

Nesting of value types and reference types

In practice, value types and reference types are not isolated; sometimes there are reference type variables in value types, and vice versa. These four types of nesting are briefly described here.

Value type Nested value types

Value type When a value type is nested, a new variable is created when the assignment is made, and the two are independent. The nested value type variable also creates a new variable, which is also independent.

Value types nested reference types

When a value type nested a reference type, a new variable is created during assignment. The two are independent, but the nested reference type refers to the same block of memory. When the nested reference type variable value is changed (except for reinitialization), the property of other objects also changes.

Reference types nested value types

When a reference type nested a value type, a new variable is created during assignment, but the new variable points to the same block of memory as the source variable, so changing the internal value of the source variable affects the values of other variables.

Reference type Nested reference types

When a reference type is nested within a reference type, a new variable is created during assignment, but the new variable points to the same block of memory as the source variable, and the internal reference type variable points to the same block of memory address. Changing the value of a reference type nested within a reference type also affects the value of other variables.

How are classes and structures selected?

Of type class instance variable memory allocation, first in the stack area assign an address space size (i.e., 8 bytes), and then to find suitable memory heap area, and returns the address to stack of heap area, by the same token, the destruction of the operation will be first in the heap area, so relative to the value type on the stack, only to be more time consuming

The choice between classes and structures is easier to make from the perspective that they are distributed differently in memory, resulting in different speeds and times in use

Here we can also intuitively test the time allocation of current constructs and classes by StructVsClassPerformance on Github.

There are also two official cases

enum Color { case blue, green, gray }
enum Orientation { case left, right }
enum Tail { case none, tail, bubble }
var cache = [String : UIImage] ()func makeBalloon(_ balloon: Ballon) -> UIImage {
	if let image = cache[balloon] {
		return image
	}
.
}
struct Balloon: Hashable{
	var color: Color
	var orientation: Orientation
	var tail: Tail
}
Copy the code
struct Attachment {
let fileURL: URL
let uuid: UUID
let mineType: MimeType
init?(fileURL: URL.uuid: String.mimeType: String) {
guard mineType.isMineType
else { return nil }
self.fileURL = fileURL
self.uuid = uuid
self.mineType = mimeType
} }
enum MimeType: String{
case jpeg = "image/jpeg"
.
}
Copy the code
conclusion

Because structs are allocated on the stack, both allocation time and destruction time are better than classes, so structs are generally preferred.

Class initializer

Class members must have initial values

Because Swift is a type-safe language, classes and structures must be instantiated with appropriate initial values for all stored properties.

class LPTeacher{
    var age : Int = 0
    var name : String = ""
   // There is no initializer, this is provided by default, but the member must also be provided with an initial value
// init(){
//
/ /}
}
var t = LPTeacher(a)/ / -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- gorgeous line -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
class LPTeacher{
    var age : Int = 0
    var name : String = ""
    init(_ age:Int , _ name:String){
        self.age = age
        self.name = name
    }
}
 var t = LPTeacher(27."LP")

Copy the code

Current class compilers do not automatically provide member initializers by default, but they do provide default initializers for structures (if we don’t specify initializers ourselves)!

struct LPStudent{
    var age : Int
    var name : String
}
var st = LPStudent(age: 27, name: "LP")
Copy the code
Multiple initializers can be provided in a class
class LPTeacher{
    var age : Int = 0
    var name : String = ""
    init(_ age:Int , _ name:String){
        self.age = age
        self.name = name
    }
    init(_ age:Int){
        self.age = age
        self.name = ""
    }
    init (_ name:String){
        self.age = 0
        self.name = name
    }
}

var t = LPTeacher(27."LP")
var t2 = LPTeacher(27)
var t3 = LPTeacher("LP")


Copy the code
Convenient initializer

The class LGPerson must provide a specified initializer (if it doesn’t, it will default), 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 LPTeacher{
    var age : Int = 0
    var name : String = ""
    init(_ age:Int , _ name:String){
        self.age = age
        self.name = name
    }
    convenience init(a) {
        self.init(27."LP")}}var t2 = LPTeacher(a)Copy the code
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 attributes, or refer to self as a value until the first phase of initialization is complete.
Initializers can fail

The initialization fails because the parameters are invalid or external conditions are not met. Such Swift failable initializers write return nil statements to indicate under what circumstances the failable initializers trigger initialization failures.

class LPPersion {
    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
class LPPersion {
    var age : Int
    var name : String
    required init(_ age : Int , _ name : String) {
        self.age = age
        self.name = name
    }

}
class LPStudeng: LPPersion {
    var classV : String
    var sAge : Int
    init(_ classV : String , _ sAge : Int) {
        self.classV = classV
        self.sAge = sAge
        super.init(20."")}required init(_ age: Int._ name: String) {// Must be implemented
        fatalError("init(_:_:) has not been implemented")}}Copy the code

Class life cycle

Compile part

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

It’s just that the compiler is different. We probably know that OC is compiled to IR by the CLang compiler, and then generates the executable file.o(here is our machine code).

Swift, on the other hand, is a bit more complex, compiling to IR through the Swift compiler and then generating the executable. The process is as follows:

Where compile command line:

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

Swift intermediate language

Among them, **Swift Intermediate Language (SIL)** Intermediate Language is the main place for our analysis

First, Swift is a memory safe language by default

int8_t x = 100; int8_t y = x + 100; NSLog(@"%d",y); // Output: -56 typedef signed char int8_t; // It is a 1 byte char. The value ranges from -128 to 127Copy the code
let x = Int8(100)+100
//Wrong:'100+100'(on type 'Int8')result in an overflow
// Memory overflow is reported
Copy the code

Since OC is a runtime language, types are not checked during compilation, whereas SWIFT is a type-safe language, and due to SIL, an intermediate language, types are checked during compilation to ensure memory safety

Declare a global variable of type LPTeacher in the main file

import Foundation

class LPTeacher {
    var age = 0
    var name = "LP"
}

var t = LPTeacher(a)Copy the code

We compiled the optimized **.sil file and opened it with the VSCode** editor by adding a script to xcode

First, you can see the LPTeacher declaration, which contains two attributes and destructors and a default initializer (because no initializer is specified)

Next we look at @main, the function entry, the identifier in the **@**sil syntax

Where **%0 represents a register, which is of course dummy and understood as a constant ** that cannot be changed once assigned

S4main1tAA9LPTeacherCvp: Obfuscating the name, which can be restored using xcRun swift-demangle

Main. Sil file, Swift SIL syntax document

Analyze Swift intermediate language in the whole main and declare a global variable process

  1. Assign a global variable (alloc_global)

  2. Get the address of this global variable and assign it to register %3 (global_addr)

  3. Get the metatype of the LPTeacher class and assign it to register 4

  4. //function_ref lpteacher.__allocating_init (), which fetches the address of this function and assigns it to register 5

  5. Apply, passes the metatype argument just obtained to the function pointer and returns an instance object to register 6

  6. Store stores the address of the instance object in register 6 into the address of the global variable just fetched

  7. The last three lines of code build an INT32-bit integer with a value of 0, the same as return 0 in OC

By analyzing the flow of class instantiation, we find that steps 4 through 6 instantiate the object, where the key function ** lpteacher.__allocating_init ()** plays a decisive role. What does it do?

Global search

  1. (@thick lpteacher.type) This function requires such a meta-type, which can be understood as ISA
  2. alloc_ref $LPTeacher

  1. Alloc_ref $LPTeacher (SIL syntax, swift code compiled) : Alloc_ref allocates an object of type T. This object will be initialized with Retain Count 1; Otherwise its state will not be initialized. The optional objc property indicates that objects should be allocated using objective-c’s allocation method (+allocWithZone :).

From swift source, breakpoint to see the assembly analysis will find

If it is a pure Swift class, an instance object will be created with Swift_allocObject, and the instance member variable will be initialized with swift_init, and the reference count +1 will apply for a chunk of memory in the heap

If it is an inherited NSObject class, use the same method as oc to allocate memory with allocWithZone and call “init” with objc_msgSend.

  1. First, the breakpoint tells you to call **__allocating_init()** first

    I’m going to go in and call swift_allocObject

    Again, explore what Swift_allocObject does. By looking at the heapObject.cpp file in swift Resource

    You can see that **_swift_allocObject_** was called directly

    Analysis of the _swift_allocObject_ function reveals three parameters

    HeapMetadata: Explained later

    Size_t requireSize: Size required

    Size_t requireAlignmentMask: source code needed to align (should be 7 bytes)

    Swift_slowAlloc is called and a value of type HeapObject is returned

    Moving on to swift_slowAlloc, you’ll see that malloc is eventually called to allocate memory space

conclusion

Without the developer calling, Allocating_init —–> swift_allocObject —–> swift_allocObject —–>swift_slowAlloc —–> Malloc

The HeapObject (OC objc_object) memory structure of the Swift object has two properties: Metadata and RefCount. The default size is 16 bytes.

Overall idea: First of all, knowing that there is such a thing as an intermediate language, Swift native code –> intermediate language —-> get allocating_init () key function —-> the function implementation of —->alloc_ref() function —-> through the official documentation and assembly to prove that the function is from allocating_init function to swift_allo CObject. Then by viewing the official swift source code to get the specific process of class instantiation, and know the basic memory structure of the object

Analysis HeapMetadata
HeapMetadata constant *metadata
using HeapMetadata = TargetHeapMetadata<InProcess>;
-------
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
};
-------
struct TargetMetadata {
  using StoredPointer = typename Runtime::StoredPointer;

  /// The basic header type.
  typedef TargetTypeMetadataHeader<Runtime> HeaderType;

  constexpr TargetMetadata(a)
    : Kind(static_cast<StoredPointer>(MetadataKind::Class)) {}
  constexpr TargetMetadata(MetadataKind Kind)
    : Kind(static_cast<StoredPointer>(Kind)) {}

#if SWIFT_OBJC_INTEROP
protected:
  constexpr TargetMetadata(TargetAnyClassMetadata<Runtime> *isa)
    : Kind(reinterpret_cast<StoredPointer>(isa)) {}
#endif

private:
  /// The kind. Only valid for non-class metadata; getKind() must be used to get
  /// the kind value.
  StoredPointer Kind;
public:
  /// Get the metadata kind.
  MetadataKind getKind(a) const {
    returngetEnumeratedMetadataKind(Kind); } -- -- -- -- -- -- -- -- -- -- -- -- -- -// Find a StoredPointer Kind; We know the kind type. The base class
  // From the OC class structure: objc_class thinks that swift should have a similar structure
 
  ConstTargetMetadataPointer<Runtime, TargetTypeContextDescriptor>
  getTypeContextDescriptor(a) const {
    switch (getKind()) {
    case MetadataKind::Class: {// You can see the TargetClassMetadata final 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; }}Copy the code

TargetClassMetadata is finally found

Analysis of the source code:

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 type of MetadataKind