This paper studies and practices classes and structs in Swift. Contains basic syntax, similarities and differences. Class initialization and life cycle. Verify the truth with Swift source code.

Similarities and differences between classes and structures

Define a class and struct respectively:

/ / parent class
class CPerson {
    func getWeight(a){}}/ / agreement
protocol PersonIntroProtocol {
    static func introSelf(a)
}

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

extension STeacher: PersonIntroProtocol {
    static func introSelf(a) {
        print("STeacher")}}class CTeacher: CPerson{
    var age: Int
    var name: String
    // class can be inherited
    override func getWeight(a){}init(age: Int.name: String) {
        self.age = age
        self.name = name
    }
    
    deinit{}}extension CTeacher: PersonIntroProtocol {
    // Both classes and structs can follow protocols
    static func introSelf(a) {
        print("CPerson")}}Copy the code
  • Memory alignment principles:

Attributes often have different data types, such as int and Boolean, which take up different bytes. The system allocates the same amount of memory to use.

The main similarities between a structure and a class
  • 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
Major differences
  • 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
  • A class has a destructor that frees its allocated resources. That’s the deinit method
  • Reference counting allows multiple references to a class instance. Struct is a value type that exists on the stack without reference.

Hence the distinction between a value type (struct) and a reference type (class).

Value type and reference type

First of all, we have a basic concept of memory area cognition: part of memory is used by the system itself, and the rest is what we can operate. This is also divided into: stack area (stack) : local variables and functions in the running context. Heap: Stores all objects. Which is usually generated using the new and malloc methods. Global: Stores Global variables; Constant; Code area;

The Mach-O file has multiple segments, each with a different function. Each Section is then divided into smaller sections. For example:

// format: segment.section: Const: initialized constant data. DATA: initialized variable (static/global) data. const: uninitialized constant data. BSS: Uninitialized (static/global) variable DATA.common: symbol declaration that is not initializedCopy the code
Viewing memory structure

In general, value types are stored on the stack and reference types are stored on the heap. Use the LLDB directive to check the memory distribution of strcut and class:

frame variable -L <variable_name>
Copy the code

Instantiation:

var t0 = STeacher(age: 18, name: "test name")
var t1 = CTeacher(age: 18, name: "test name")
Copy the code

Instruction output:

(lldb) frame variable -L t0
0x000000010000c4c8: (SwiftTest.STeacher) t0 = {
0x000000010000c4c8:   age = 18
0x000000010000c4d0:   name = "test name"
}
(lldb) frame variable -L t1
scalar: (SwiftTest.CTeacher) t1 = 0x0000000101a38a40 {
scalar:   SwiftTest.CPerson = {}
0x0000000101a38a50:   age = 18
0x0000000101a38a58:   name = "test name"
}
Copy the code

You can also view the memory structure of the current variable with the following instructions: Po: prints only the corresponding value P: the type of return value and the reference name of the command result x/8g: reads the value in memory (x: reads; 8: continuous printing 8 sections; G: 8-byte output)

// The console uses LLDB command output
(lldb) frame variable -L t0
0x000000010000c638: (SwiftTest.STeacher) t0 = {
0x000000010000c638:   age = 18
0x000000010000c640:   name = "name"
}
(lldb) x/8g 0x000000010000c638
// Count +1 for 1 byte; A byte is the smallest unit of storage
1 byte =8 binary =2 hexadecimal, so each segment is 8 bytes, total 16 bytes;
// The corresponding left address counts exactly 16 different per line. 0x0000000000000012 corresponds to age=18
// There is no value since the fourth paragraph, indicating that the string is 16 bytes long
0x10000c638: 0x0000000000000012 0x00000000656d616e
0x10000c648: 0xe400000000000000 0x0000000000000000
0x10000c658: 0x0000000000000000 0x0000000000000000
0x10000c668: 0x0000000000000000 0x0000000000000000
Copy the code

Class initializer

Specifies an initializer
  • Instances of classes and structures in Swift must be created with an appropriate initial value for all storage properties. So provide the corresponding designated initializer, which is the init() method at the beginning of the CTeacher.

Multiple specified initializers can be combined depending on the parameters.

  • 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).
Convenient initializer

Multiple specified initializers are logically independent. When you want each initializer to pass through a default initializer, the convenience initializer is involved.

    convenience init(name: String) {
        // a specified initializer must be called first,
        self.init(age: 18, name:"nil")
        // Add your own logic
        self.name = name
    }
Copy the code

Here we remember:

  • 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

In actual scenarios, initialization may fail because parameters are invalid or external conditions are not met. Init with “?” Number indicates failure

    init?(age: Int.name: String) {
        // Minors are not allowed to be teachers.
        if age < 18 { return nil}
        self.age = age
        self.name = name
    }
Copy the code
Necessary initializer

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

class CTeacher: CPerson{
    var age: Int
    var name: String
    
    required init(age: Int.name: String) {
        self.age = age
        self.name = name
    }
}

class CCTeacher: CTeacher{
    required init(age: Int.name: String) {
        super.init(age: 0, name: "")
        self.age = age
        self.name = name
    }
}
Copy the code

Class life cycle

Both OC and Swift backends are compiled through LLVM.

  • OC generates LLVM IR files through clang C Compiler.
  • Swift generates LLVM IR files through the SWIFT Compiler.
  • Finally, LLVM is used to generate the executable file.o(here is our machine code).
Swift detailed compilation process
  • Swift code

Swift code

  • Parse

The Parse parser is a simple recursive descent parser (implemented in Lib /Parse) with a fully hand-coded lexical parser. Use parse for lexical analysis

  • Sema

The Semantic Analysis phase (implemented in Lib /Sema) is responsible for taking parsed AST (abstract syntax tree) and converting it to a well-formed and type-checked AST, as well as warning or error in source code indicating Semantic problems. Semantic analysis includes type inference, which indicates that it is safe to generate code from the type-checked final AST at this time if the type can be successfully derived; The ClangImporter (ClangImporter), implemented in lib/ClangImporter, is responsible for importing the Clang module and mapping the exported C or Objective-C API to the appropriate Swift API. The final imported AST can be referenced by semantic analysis

  • SILGen
  • RAW SIL

SIL Generation: THE Swift Intermediate Language (SIL) is a high-level, swift-specific Intermediate Language suitable for further analysis and optimization of Swift code. The SIL generation phase (implemented in Lib /SILGen) weakens the type-checked AST to what is called a “raw” SIL. The SIL design is described in docs/ sil.rst. This process generates RAW SIL (native SIL, large amount of code, no type checking, code optimization)

  • SIL Opt Canonical SIL

SIL Optimizations: The SIL optimization phase (implemented in Lib /Analysis, lib/ARC, lib/LoopTransforms, and lib/Transforms) is responsible for performing additional high-level, Swift-specific optimizations on the program, These include (for example) automatic reference counting optimization, de-virtualization, and general specialization. An optimized SIL Opt Canonical SIL is generated using the -emit-sil command. This is also the SIL code we normally read;

  • IRGen
  • LLVM IR

LLVM IR Generation: The IR Generation phase (implemented in Lib /IRGen) weakens SIL to LLVM LR, at which point LLVM can continue to optimize and generate machine code. Generate IR through IRGen;

  • Machine code

Finally generate machine code to the machine for identification.

Each step compiles the command

// 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
Swift object memory allocation
  • Trace method calls through debug.

Xcode –>Debug–>Debug Workflow–>Always Show Disassembly — Debug breakpoint –>Debug Workflow–>Always Show Disassembly —

0x10000395c <+108>: callq  0x100004160 ; SwiftTest.CTeacher.__allocating_init(age: Swift.Int, name: wift.String) - >SwiftTest.CTeacher at main.swift:41
Copy the code

Then hold down the Ctrl key and click the Step Into button on the console to go inside the method. You’ll see the line that contains swift_allocObject. And so on and so on; I put the process together to show:

// SwiftTest`CTeacher.__allocating_init(age:name:):
0x10000418b <+43>: callq  0x1000075e2 ; symbol stub for: swift_allocObject
// libswiftCore.dylib`swift_allocObject:
0x7ff82a3aeed0 <+16>: leaq   0x2399(%rip), %rcx ; _swift_allocObject_
0x7ff82a3aeee2 <+34>: callq  0x7ff82a3aed50 ; swift_slowAlloc
// libswiftCore.dylib`swift_slowAlloc:
0x7ff82a3aed73 <+35>:  callq  0x7ff82a4311e6 ; symbol stub for: malloc_zone_malloc
// libsystem_malloc.dylib`_malloc_zone_malloc:
Copy the code
  • Tracing method calls through source code:

Swift official source code is open source: download address, use VSCode tool open, search swift_allocObject or heapObject. CPP file, find HeapObject;

HeapObject *swift::swift_allocObject(HeapMetadata const *metadata,
                                     size_t requiredSize,
                                     size_t requiredAlignmentMask) {
  CALL_IMPL(swift_allocObject, (metadata, requiredSize, requiredAlignmentMask));
}
// 
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;
}
// 
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__) && SWIFT_STDLIB_HAS_DARWIN_LIBMALLOC
    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

Tracing method calls until you finally find a few familiar malloc methods, the generic object is done. To sum up, the process is: __allocating_init —–> swift_allocObject —–> swift_allocObject —–> swift_slowAlloc —–> Malloc

The memory structure of a Swift object

HeapObject (objc_object in OC) has two properties: Metadata and RefCount, which occupy 16 bytes by default.

struct HeapObject {
  HeapMetadata const * metadata;
  InlineRefCounts refCounts;
}
Copy the code

OC, on the other hand, has only one lonely Isa

objc_object{ 
    isa 
}
Copy the code

Trace HeapMetadata from the source code input parameter:

namespace swift {

struct InProcess;

template <typename Target> struct TargetHeapMetadata;
using HeapMetadata = TargetHeapMetadata<InProcess>;
#else
typedef struct HeapMetadata HeapMetadata;
typedef struct HeapObject HeapObject;
#endif. }Copy the code

Looking at TargetHeapMetadata, where SWIFT_OBJC_INTEROP represents the initialization used by classes that inherit OC, you can see the familiar ISA pointer.

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

The MetadataKind parameter is the method used by Swift. Check the MetadataKind:

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

All types are included in the #include “metadatakind. def” file.

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

The TargetClassMetadata class is defined as TargetClassMetadata.

The structure of all class metadata. Structure of all class metadata

/// The structure of all class metadata. This structure is embedded
/// directly within the class's heap metadata structure and therefore
/// cannot be extended without an ABI break.
///
/// Note that the layout of this type is compatible with the layout of
/// an Objective-C class.
template <typename Runtime>
struct TargetClassMetadata : public TargetAnyClassMetadata<Runtime> {
  using StoredPointer = typename Runtime::StoredPointer;
  using StoredSize = typename Runtime::StoredSize;

  TargetClassMetadata() = default;
  constexpr TargetClassMetadata(const TargetAnyClassMetadata<Runtime> &base,
             ClassFlags flags,
             ClassIVarDestroyer *ivarDestroyer,
             StoredPointer size, StoredPointer addressPoint,
             StoredPointer alignMask,
             StoredPointer classSize, StoredPointer classAddressPoint)
    : TargetAnyClassMetadata<Runtime>(base),
      Flags(flags), InstanceAddressPoint(addressPoint),
      InstanceSize(size), InstanceAlignMask(alignMask),
      Reserved(0), ClassSize(classSize), ClassAddressPoint(classAddressPoint),
      Description(nullptr), IVarDestroyer(ivarDestroyer) {}

  // Ignore more code
};
using ClassMetadata = TargetClassMetadata<InProcess>;
Copy the code

The simplified structure is as follows:

struct Metadata{
    var kind: Int
    var superClass: Any.Type
    var cacheData: (Int.Int)
    var data: Int
    var flags: ClassFlags
    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