First understanding of classes and structures

1.1 class and struct

In Swift, classes are modified with class and struct with struct, as shown below:

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

class SSLPerson {
    var age: Int
    var name: String
    
    init(age: Int, name: String) {
        self.age = age
        self.name = name
    }
    deinit {
        
    }
}
Copy the code

1.2 Overview of similarities and differences

The main similarities between a structure and a class are:

  • Defines attributes that store values
  • Define methods
  • Define subscripts to provide access to their values using subscript syntax
  • Define initializers
  • Use Extension to extend functionality
  • Follow a protocol to provide a function

The main differences are:

  • 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  classes have destructors to release their allocated resources
  • Reference counting allows multiple references to a class instance

1.3 Classes are reference types

The first thing we need to distinguish between classes and structures is:

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 stored.

Let’s look at the memory structure of the current variable with instructions

P: type of return value and reference name of command result x/8g: read value in memory (8g: output in 8-byte format)Copy the code

Take a look at the following example:

  • You can see that the addresses of t and T1 are the same, indicating that the class is a reference type.

It can be expressed as follows:

1.4 Structs are value types

Swift has reference types and value types, the most typical of which is Struct. The definition of structure is also very simple. In contrast, variables of class types store addresses, so value types store specific instances (or specific values).

Consider the following example:

  • As you can see, the structure stores values directly.

1.5 Storage Area Differences

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.

For those of you who are not familiar with memory areas, check out the five memory areas.

1.5.1 Structure memory allocation

Let’s use the following example for analysis

struct SSLTeacher {
    var age = 18
    var name = "ssl"
}

func test() {
    var t = SSLTeacher()
    print("end")
}   

test()
Copy the code

Next use the command

frame varibale -L xxx
Copy the code

  • T is stored on the stack, age and name are stored on the stack
  • The first address of t is directly the stored age variable, and age and name are also contiguous
  • When you performvar t = SSLTeacher()This code will open up the required memory space on the stack whentest()When the function completes, the memory space is reclaimed.

1.5.2 Class memory allocation

Again, use an example to analyze

  • On the stack, eight bytes of memory are allocated to store the address of SSLTeacher
  • On the heap, the appropriate memory area is found to open up memory and store values
  • At the end of the function, the pointer on the stack is destroyed, and the sample variable on the heap is found and reclaimed.

1.5.3 Time allocation of structures and classes

As you can see above, the memory allocation of the class is a bit tedious, and there is reference counting and so on.

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

After testing, we can find that the structure is significantly faster than the class.

1.5.4 Optimization cases

struct Attachment {
    let fileURL: URL
    let uuid: String
    let mineType: String
    ...
}
Copy the code

Attachment is a value type, but uUID and mineType are reference types, which will affect performance. We will optimize them and modify them with value types

struct Attachment {
    let fileURL: URL
    let uuid: UUID
    let mineType: MimeType
    ...
}

enum MimeType {
    case jpeg = "image/jpeg"
    ...
}
Copy the code

Class initializer

Instances of classes and structures in Swift must be created with an appropriate initial value for all storage properties.

2.1 Structure initializer

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 SSLTeacher {
    var age: Int
    var name: String
}
Copy the code

2.2 Class initializer

The class must provide a specified initializer, and we can also provide a convenient initializer for the current class (note: the convenient initializer must call another initializer from the same class).

class SSLPerson {
    var age: Int
    var name: String

    init(_ age: Int, _ name: String) {
        self.age = age
        self.name = name
    }
    
    convenience init(_ age: Int) {
        self.init(18, "ssl")
        self.age = age
    }
}
Copy the code
  • 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.

When we derive a subclass of SSLTeacher, look at its specified initializer written

class SSLTeacher: SSLPerson {
    var subjectName: String
    
    init(_ subjectName: String) {
        self.subjectName = subjectName
        super.init(18, "ss")
        self.age = 17
    }
}
Copy the code
  • The specified initializer must ensure that all properties introduced by its class are initialized before delegating up to the parent initializer, in this case subjectName
  • The specified initializer must delegate to the parent initializer before it can set new values for inherited properties. If this is not done, the new value assigned by the specified initializer will be overridden by the initializer in the parent class, in this case age.

2.3 Failable initializer

Failable initializer: This is easy to understand, meaning that the initialization failed due to 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 SSLPerson { var age: Int var name: String init? (_ age: Int, _ name: String) { if age < 18 {return nil} self.age = age self.name = name } convenience init?(_ age: Int) { self.init(18, "ssl") } }Copy the code
  • If the age is less than 18 and is not considered a legal adult, the creation fails.

2.4 Necessary initializers

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

  • If a subclass does not implement the required initializer, an error is reported.

Class life cycle

3.1 Swift compilation

Both OC and Swift backends of iOS development are compiled using 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.

Take a closer look at the Swift compilation process:

If you are interested in compiling the command, you can play it:

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.swiftCopy the code

3.2 SIL file analysis

Write the following code in main.swift:

class SSLPerson {
    var age: Int = 18
    var name: String = "ssl"
    }

var t = SSLPerson()
Copy the code

Run the following command

swiftc -emit-sil main.swift > ./main.sil && open main.sil
Copy the code

Use the command above to compile main.swift into the main.sil file, the file code is familiar, there are some related parsing below

class SSLPerson { @_hasStorage @_hasInitialValue var age: Int { get set } @_hasStorage @_hasInitialValue var name: String { get set } @objc deinit init() } // main sil @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 { bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>): alloc_global @$s4main1tAA9SSLPersonCvp // id: %2 %3 = global_addr @$s4main1tAA9SSLPersonCvp : $*SSLPerson // user: %7 %4 = metatype $@thick SSLPerson.Type // user: %6 // function_ref SSLPerson.__allocating_init() %5 = function_ref @$s4main9SSLPersonCACycfC : $@convention(method) (@thick SSLPerson.Type) -> @owned SSLPerson // user: %6 %6 = apply %5(%4) : $@convention(method) (@thick SSLPerson.Type) -> @owned SSLPerson // user: %7 store %6 to %3 : $*SSLPerson // id: %7 %8 = integer_literal $Builtin.Int32, 0 // user: %9 %9 = struct $Int32 (%8 : $Builtin.Int32) // user: %10 return %9 : $Int32 // id: %10 } // end sil function 'main' // SSLPerson.__allocating_init() sil hidden [exact_self_class] @$s4main9SSLPersonCACycfC : $@convention(method) (@thick SSLPerson.Type) -> @owned SSLPerson { // %0 "$metatype" bb0(%0 : $@thick SSLPerson.Type): %1 = alloc_ref $SSLPerson // user: // function_ref sslPerson.init () %2 = function_ref @$s4main9SSLPersonCACycfc: $@convention(method) (@owned SSLPerson) -> @owned SSLPerson // user: %3 %3 = apply %2(%1) : $@convention(method) (@owned SSLPerson) -> @owned SSLPerson // user: %4 return %3 : $SSLPerson // id: %4 } // end sil function '$s4main9SSLPersonCACycfC'Copy the code
  • @main: entry function
  • %0: register, virtual
  • Sil syntax rules: github.com/apple/swift…
  • This code basically parses the SSLPerson class, allocates memory in the heap, and calls key functions__allocating_init(), will be further analyzed through Swift source code.

3.3 Breakpoint assembly analysis

Break the code below to see the assembly

class SSLPerson {
    var age: Int = 18
    var name: String = "ssl"
    }

var t = SSLPerson()
Copy the code

class SSLPerson : NSObject {
    var age: Int = 18
    var name: String = "ssl"
}

var t = SSLPerson()
Copy the code

  • Find the key functionswift_allocObject, the following source code analysis.

3.4 Swift source code analysis

Swift source code: github.com/apple/swift

Search the heapObject. CPP file to find the swift_allocObject function:

HeapObject *swift::swift_allocObject(HeapMetadata const *metadata,
                                     size_t requiredSize,
                                     size_t requiredAlignmentMask) {
  CALL_IMPL(swift_allocObject, (metadata, requiredSize, requiredAlignmentMask));
}
Copy the code

Swift_allocObject calls the _swift_allocObject_ function:

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)); . }Copy the code

Swift_slowAlloc = swift_slowAlloc = swift_slowAlloc

void *swift::swift_slowAlloc(size_t size, size_t alignMask) { void *p; #if defined(__APPLE__) && SWIFT_STDLIB_HAS_DARWIN_LIBMALLOC p = malloc_zone_malloc(DEFAULT_ZONE(), size); #else p = malloc(size); . }Copy the code
  • You can see that the malloc function is also called in Swift.

From this we can get the memory allocation of the Swift object:

  • __allocating_init -> swift_allocObject -> _swift_allocObject_ -> swift_slowAlloc -> Malloc
  • The memory structure of a Swift objectHeapObject(OC objc_object), which has two properties: Metadata and RefCount, occupied by default16Size in bytes.
    struct HeapObject {
      HeapMetadata const * metadata;
      InlineRefCounts refCounts;
    }
    Copy the code

Objc_object has only one ISA, and HeapObject lacks two attributes. Next, let’s explore Metadata

4. Exploring the structure of classes

4.1 Metadata source analysis

Metadata is of type HeapMetadata.

struct InProcess;

template <typename Target> struct TargetHeapMetadata;
using HeapMetadata = TargetHeapMetadata<InProcess>;
Copy the code

This is an aliases like definition, so look at 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
  • TargetHeapMetadata inherits TargetMetadata
  • In the initialization method, passed in when it is a pure Swift classMetadataKindIf it interacts with OC, it passes oneisa

4.2 MetadataKind source code analysis

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"
  LastEnumerated = 0x7FF,
};
Copy the code

And the type association table in Swift is as follows:

4.3 class structure source code analysis

Look at TargetHeapMetadata’s parent TargetMetadata:

struct TargetMetadata {

using StoredPointer = typename Runtime::StoredPointer;

private:
  StoredPointer Kind;
}
Copy the code
  • You can see that TargetMetadata has a Kind member variable, moving on

We find ConstTargetMetadataPointer function:

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;
    }
  }
Copy the code
  • You can see that when kidn is Class, this is forcibly cast as TargetClassMetadata.

Check the TargetClassMetadata:

struct TargetClassMetadata : public TargetAnyClassMetadata<Runtime> { ... } struct TargetAnyClassMetadata : public TargetHeapMetadata<Runtime> { TargetSignedPointer<Runtime, const TargetClassMetadata<Runtime> *superclass; TargetPointer<Runtime, void> CacheData[2]; StoredSize Data; . }Copy the code

In TargetAnyClassMetadata we can find superclass, Data, CacheData and other member variables. After the above series of analysis, we can get the Data structure of swift class 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