If you want to learn Swift in depth, you have to start from the most basic classes, objects, attributes in-depth analysis, this paper will use SIL files and Swift source code, to explore the underlying structure of classes, objects and attributes.

The compilation process

  • Before we look at SIL, let’s look at the compilation process for iOS:

    • iOSpointsOCandSwiftBoth languages, but their backends are passedLLVMCompiled, as shown below:



    • OCIs through theClangThe compiler compiles toIR, and then regenerate it into an executable file.o
    • SwiftIs through theswiftcThe compiler compiles and generatesIR, and then regenerate it into an executable file.o

Swift Compilation process

  • According to the description of the main components of Swift Compiler in Swift Compiler, the following figure can be obtained:



    • It is mainly divided into the following processes:
        1. Parsing (analysis)The: parser is a simple recursive descent parser (inIn the lib/Parse the implementation), with an integrated, hand-coded lexical analyzer. The parser is responsible for generating abstract syntax trees without any semantic or type information(AST)And issues warnings or errors for syntax problems in the input source
        1. Semantic analysis: Semantic analysis (inIn the lib/Sema implementation) is responsible for getting the parsedASTAnd convert it to a well-formed, fully type-checkedASTForm to warn or error about semantic problems in source code. Semantic analysis includes type inference, which, if successful, indicates that the generated, type-checkedASTThe generated code is safe
        1. Clang importer(Clang importer):ClangThe importer (inIn the lib/ClangImporter implementation) the importClangAnd I’m going to export themCorObjective-C APIMap to the correspondingSwift API. Generated importASTReferences can be made through semantic analysis
        1. SIL Generation:SwiftIntermediate language(SIL)Is a kind of advanced, specific toSwiftIntermediate language for further analysis and optimizationSwiftThe code.SILGeneration phase (inIn the lib/SILGen implementation) will be type checkedASTReduced to what’s called “primitive.”SIL.SILThe design of theIn docs/ sil.rstdescribe
        1. SIL guaranteed transformations:SILEnsure conversion (inIn the lib/SILOptimizer/Mandatory implementationPerform additional data flow diagnostics that affect program correctness (such as usingAn uninitialized variable). The end result of these transformations is “canonical”SIL.
        1. SIL Optimizations:SILOptimization (inlib/Analysis,lib/ARC,lib/LoopTransformsandIn the lib/TransformsPerform additional high-level, specific to the programSwiftOptimizations, including (for example) automatic reference counting optimization, de-virtualization, and generic specialization
        1. LLVM IR Generation(LLVM IR Generation):IRGenerated (inIn the lib/IRGen implementation) will beSILDown toLLVM IRAt this time,LLVMYou can continue to optimize it and generate machine code.

SIL is the intermediate code during Swift compilation, located between AST and LLVM IR

Generate the SIL

  • The following willswiftCode generationSILFile:
    // main.swift
    class WSPerson {
        var age: Int = 18
        var name: String = "wushuang"
    }
    var ws = WSPerson(a)Copy the code
    • swiftThe front-end compiler used during compilation isswiftc, you can use the commandswiftc -emit-sil main.swift >> ./main.silwillswiftFiles are generatedSILFile:



Preliminary analysis of SIL files

  • The form of classes and objects in SIL files:

    class WSPerson {
      // Store the attribute age
      @_hasStorage @_hasInitialValue var age: Int { get set }
      // Store attribute name
      @_hasStorage @_hasInitialValue var name: String { get set }
      // @objc tag deinit method
      @objc deinit
      // constructor init
      init(a)
    }
    // Store the attribute ws
    @_hasStorage @_hasInitialValue var ws: WSPerson { get set }
    // ws
    // s4main2wsAA8WSPersonCvp is after confusion, main.ws
    sil_global hidden @$s4main2wsAA8WSPersonCvp : $WSPerson
    Copy the code
    • S4main2wsAA8WSPersonCvp is the variable after confusion. You can use the xcrun swift-demangle s4main2wsAA8WSPersonCvp command to restore the variable. The result is as follows:

      $s4main2wsAA8WSPersonCvp ---> main.ws : main.WSPerson
      Copy the code
  • The main function:

    // @main entry function
    // @convention(c) stands for c function
    The main function takes two arguments of type Int32 and UnsafeMutablePointer and returns a value of type Int32
    sil @main : $@convention(c) (Int32.UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8- > > > >)Int32 {
    / / ` ` % 0, % 1 is the register in SIL, will not change after assignment can be understood as a constant, is the virtual register, and register to register in the read
    bb0(%0 : $Int32.%1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) :// 1. Create a global variable of type main.WSPerson and store it in %2
      alloc_global @$s4main2wsAA8WSPersonCvp          // id: %2
    
    // 2. Get the address of the global variable and assign the address to %3
      %3 = global_addr @$s4main2wsAA8WSPersonCvp : $*WSPerson // user: %7
    
    // 3. Get the WSPerson metadata type and assign it to %4
      %4 = metatype $@thick WSPerson.Type             // user: %6
    
    // 4. Assign the wsperson.__allocating_init () function to %5
      // function_ref WSPerson.__allocating_init()
      %5 = function_ref @$s4main8WSPersonCACycfC : $@convention(method) (@thick WSPerson.Type) - >@owned WSPerson // user: %6
    
    // 5. apply calls the function %5 __allocating_init and assigns the result to %6
      %6 = apply %5(%4) : $@convention(method) (@thick WSPerson.Type) - >@owned WSPerson // user: %7
    
    // 6. Store %6 to address %3,
      store %6 to %3 : $*WSPerson                     // id: %7
    
    // Create the integer variable 0 and return it
      %8 = integer_literal $Builtin.Int32.0          // user: %9
      %9 = struct $Int32 (%8 : $Builtin.Int32) / /user: % 10return %9 : $Int32                              // id: % 10} / /end sil function 'main'
    Copy the code
    • @mainThe function ismain.swiftFile entry function,% % 0 and 1isSILThe register, which does not change after assignment, can be understood as a constant, is a virtual register, and is used when viewing assemblyregister readDifferent registers in
    • mainThe main function is to do some object creation work, there are mainly the following steps
        1. Create a global variablewsAnd the assignment% 2
        1. Get global variableswsAnd assign a value to% 3
        1. To obtainWSPersonMetadata type and assign a value to% 4
        1. willWSPerson.__allocating_init()The function assigns a value to% 5
        1. Called based on the metadata type__allocating_initThe function creates an object and assigns the result to% 6
        1. Object to be created% 6Store global variables% 3Address, that is, to the global variablewsFor the assignment
        1. returnThe end of themainfunction

Create an object

  • In the above analysis, we know that the core of creating an object is calling__allocating_initFunction, whose code looks like this:

Source code analysis

// WSPerson.__allocating_init()
sil hidden [exact_self_class] @$s4main8WSPersonCACycfC : $@convention(method) (@thick WSPerson.Type) - >@owned WSPerson {
// %0 "$metatype"
bb0(%0 : $@thick WSPerson.Type) :// 1. Create an object of type WSPerson on the heap and assign it to %1
  %1 = alloc_ref $WSPerson                        // user: %3
  // 2. get the wsperson.init () function and assign it to %2
  // function_ref WSPerson.init()
  %2 = function_ref @$s4main8WSPersonCACycfc : $@convention(method) (@owned WSPerson) - >@owned WSPerson // user: %3
  // 3. Object calls init and returns the current object
  %3 = apply %2(%1) : $@convention(method) (@owned WSPerson) - >@owned WSPerson // user: %4
  return %3 : $WSPerson                           // id: %4
} // end sil function '$s4main8WSPersonCACycfC'
Copy the code
  • The core process of creating an object consists of three steps:
      1. Created on the heapWSPersonType and assign a value toThe % 1
      1. To obtainWSPerson.init()Function and assign to% 2
      1. objectThe % 1callinitMethod and returns the current object

Assembly analysis

  • Add the symbolic breakpoint __allocating_init to the Xcode code and parse the assembly:



  • __allocating_init calls swift_allocObject (swift_allocObject).

    static HeapObject *_swift_allocObject_(HeapMetadata const *metadata,
                                         size_t requiredSize,
                                         size_t requiredAlignmentMask) {
      assert(isAlignmentMask(requiredAlignmentMask));
      auto object = reinterpret_cast<HeapObject *>(
      // Calculate the memory size
          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.
      // Create an instance object based on the metadata type
      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
  • The _swift_allocObject_ function performs two main steps:

      1. callswift_slowAllocThe function calculates the memory size
      1. Based on memory and metadata typeThe heap areaCreate an object
    • swift_slowAllocComputational memory source code is as follows:
    // Linux malloc is 16-byte aligned on 64-bit, and 8-byte aligned on 32-bit.
    #  if defined(__LP64)
    #    define MALLOC_ALIGN_MASK 15
    #  else
    #    define MALLOC_ALIGN_MASK 7
    #  endif
    
    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
    • alignMaskIt’s memory alignedmaskIn theA 64 - bitis16 bytesAlignment, and32 -is8 bytesAlign. See how you alignIOS low-level – Memory alignment
    • As for theMALLOC_ALIGN_MASKWhy the alignment number minus1Because when you calculate alignment, you have to&on~MALLOC_ALIGN_MASK, e.g.7.~ 7is8Multiples.
  • The HeapObject method has the following source code:

    struct HeapObject {
      /// This is always a valid pointer to a metadata object.
      // Metadata refers to HeapMetadata
      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.
      // constructor
      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)
      { }
    
    #endif // __swift__
    };
    Copy the code
    • In the aboveHeapObjectThe constructor takes two arguments
      • metadataisHeapMetadataType of pointer, accounting8 bytes.
      • By looking forrefCountsDid you get itInlineRefCountsType, occupation8 bytes
    • At this point the conclusion can be drawn,Swift objectEssential forHeapObject, to take up16 bytes.

Memory allocation

  • Class WSPerson () {class_getInstanceSize ();



    • Result-occupied40 bytesWhy is that? To useMemoryLayout < type >. StrideTo print separatelyIntandStringMemory occupied:



    • Int and String are structs of 8 bytes and 16 bytes respectively.

      // InterTypes.swift
      @frozen
      public struct Int
        :FixedWidthInteger, SignedInteger, _ExpressibleByBuiltinIntegerLiteral {... }// String.swift
      @frozen
      public struct String {. }Copy the code
    • RefCount (8 bytes) + Int (8 bytes) + String (16 bytes

conclusion

    1. Object memory allocation process:__allocating_init -> swift_allocObject_ -> swift_slowAlloc -> malloc
    1. SwiftIn theInstance objectsTake up16 bytes, thanOCIn the moreRefCounted (reference count size)

class

  • Metadata is a pointer to HeapMetadata.

    template <typename Target> struct TargetHeapMetadata;
    using HeapMetadata = TargetHeapMetadata<InProcess>;
    Copy the code
    • HeapMetadataisTargetHeapMetadataAlias of the template function
  • In view of TargetHeapMetadata code:

    template <typename Runtime>
    struct TargetHeapMetadata : TargetMetadata<Runtime> {
      using HeaderType = TargetHeapMetadataHeader<Runtime>;
    
      TargetHeapMetadata() = default;
      // Initialize method
      constexpr TargetHeapMetadata(MetadataKind kind)
        : TargetMetadata<Runtime>(kind) {}
    #if SWIFT_OBJC_INTEROP
      constexpr TargetHeapMetadata(TargetAnyClassMetadata<Runtime> *isa)
        : TargetMetadata<Runtime>(isa) {}
    #endif
    };
    Copy the code
    • In the codeTargetHeapMetadatainheritanceTargetMetadataThe core of the method is based on parametersKind (incoming InProcess)callTargetMetadataThe construction method of. Let’s continue with the code
  • TargetMetadata code is mainly for some kind operations, kind types are as follows:

    struct TargetMetadata {.private: StoredPointer Kind; . }Copy the code
    • It’s actually passing inInProcessIn structureuintptr_t, continue to check the type to seeunsigned longType:
    struct InProcess {.using StoredPointer = uintptr_t; . }typedef unsigned long           uintptr_t;
    Copy the code
    • sokindisunsigned longType, which mainly distinguishes the current type of data
  • Enter the MetadataKind function in the TargetMetadata function, and then click on MetadataKind in #include “Metadatakind. def” to enter the metadatakind. def folder, you can see many types, The corresponding kind values are as follows:

    • class : 0x0
    • Struct : 0x200
    • Enum : 0x201
    • Optional : 0x202
    • ForeignClass : 0x203
    • Opaque : 0x300
    • Tuple : 0x301
    • Function : 0x302
    • Existential : 0x303
    • Metatype : 0x304
    • ObjCClassWrapper : 0x305
    • ExistentialMetatype : 0x306
    • HeapLocalVariable : 0x400
    • HeapGenericLocalVariable : 0x500
    • ErrorObject : 0x501
    • Task : 0x502
    • Job : 0x503
    • LastEnumerated : 0x7FF

Class structure analysis

  • There are methods to get the class in TargetMetadata

    const TargetClassMetadata<Runtime> *getClassObject(a) const;
    Copy the code
    • One of thegetClassObjectThe function is based onkindTo obtainobjectType:
    template<> inline const ClassMetadata *
    Metadata::getClassObject(a) const {
      switch (getKind()) {
      case MetadataKind::Class: {
        // Native Swift class metadata is also the class object.
        // If it is class, it is forcibly converted to ClassMetadata
        return static_cast<const ClassMetadata *>(this);
      }
      case MetadataKind::ObjCClassWrapper: {
        // Objective-C class objects are referenced by their Swift metadata wrapper.
        auto wrapper = static_cast<const ObjCClassWrapperMetadata *>(this);
        return wrapper->Class;
      }
      // Other kinds of types don't have class objects.
      default:
        return nullptr; }}Copy the code
    • If kind is of Class type, the current metadata is forcibly converted to ClassMetadata, which is the alias of TargetClassMetadata by type

      using ClassMetadata = TargetClassMetadata<InProcess>;
      Copy the code
  • Follow up with TargetClassMetadata viewing



    • TargetClassMetadata inherits TargetAnyClassMetadata and has its own constructor and associated fields. The swift_class_t structure is the same as the swift_class_t structure in objC4-818.2

      struct swift_class_t : objc_class {
          uint32_t flags;
          uint32_t instanceAddressOffset;
          uint32_t instanceSize;
          uint16_t instanceAlignMask;
          uint16_t reserved;
      
          uint32_t classSize;
          uint32_t classAddressOffset;
          void *description;
          // ...
      
          void *baseAddress(a) {
              return (void((*)uint8_t *)this- classAddressOffset); }};Copy the code
  • Continue looking at the code in TargetAnyClassMetadata:



    • The structure here andOCIn theobjc_classThe structure is the same as that ofisaThere is a parent class, there iscacheData.DataSimilar to theobjc_classIn thebits
  • When metadata kind is Class, it has the following inheritance relationship:



attribute

There are four attributes in SWIFT, namely: storage attribute, calculation attribute, delay attribute and type attribute. They will be explained in detail below

Storage properties

  • Storage properties are divided into two types: constant storage properties (LET modification) and variable storage properties (var modification), the specific code is as follows:

    class WSPerson {
        let age: Int = 18 // Constants store attributes
        var name: String = "wushuang" // The variable stores attributes
    }
    Copy the code
    • The value of a constant store property cannot be modified, while the value of a variable store property can be modified inSILThe document is even more straightforward:
    class WSPerson {
      @_hasStorage @_hasInitialValue final let age: Int { get }
      @_hasStorage @_hasInitialValue var name: String { get set }
      @objc deinit
      init(a)
    }
    Copy the code
    • inSilConstant store properties in files can only be usedgetterMethod, variable store properties cansetterandgettermethods

Calculate attribute

  • The computed property takes no memory, does not store the value itself, and accesses the value indirectly through the getter, as in the following two cases

    class WSName {
        var familyName: String = "Bryant"
        var givenName: String = "Kobe"
        var fullName: String {
            get {
                return givenName + familyName
            }
        }
    }
    
    class Square {
        var width: Double = 20.0
        var area: Double {
            get {
                return pow(width, 2)}set {
                width = sqrt(newValue)
            }
        }
    }
    Copy the code
    • View SIL files:



      • You can see that the evaluated property does not evaluate all of the properties_hasStorage @_hasInitialValueidentifier
  • Print the memory usage of these two classes:



    • The results ofWSNameThe occupied memory is48 byte.familyNameTake up16 bytes.givenNameTake up16 bytes.metadataTake up8 bytes.refCountTake up8 bytesIt adds up just right48 byte.fullNameNo memory usage.
    • SquareIn thewidthTake up8 bytes.metadataTake up8 bytes.refCountTake up8 bytesIt adds up just right24 bytes.areaNo memory usage
    • Therefore, it is concluded that calculating attributes takes no memory

Attribute observer (willSet/didSet)

  • The property observer can be understood as KVO in OC, when the property calls setter methods:

      1. Called before the new value is storedwillSetMethod to get a new valuenewValue.
      1. Called after the new value is storeddidSetMethod to get the old valueoldValue
    • As shown in the following case:



    • inSILFile viewing:



    • inname.setterThe following operations are performed in the function:
        1. Gets the old value of name and stores it in a register% 6
        1. callname.willSetFunction and pass in the new value% 0
        1. Store the new valuenameAnd releases the old value
        1. callname.didSetFunction and pass in the old value% 6
  • Question 1: Does changing the value of an attribute in the init method trigger an observer?

    • Add in the caseinitRun again



    • It didn’t triggerwillSetanddidSetMethods,SILAccording to the analysis of the documents,initTo change the value of the property is only to change the value of the property when it is initialized, because the object creation process has not been completed at this time, so it will not be triggeredsetterMethod, which does not trigger the property observer



  • Question 2: Where can I add attribute observations

    • You can add observers in three places:
        1. Class to define a storage property
        1. Store attributes inherited by a class
        1. Attributes are evaluated by class inheritance



  • Problem 3: Both subclasses and superclasses have computed attributesDidSet, willSetWhen, what is the call order?

    • Print using the following examples:



    • It is known from the case that the calculated attributes of the subclass parent exist at the same timeDidSet, willSetWhen the attribute value changes, the call order of the two methods of parent class and child class is as follows:
      • First,willSet: subclass first, then parent class
      • afterdidSet: superclass, subclass
  • Question 4: does a subclass call its parent’s init method and change the property value init trigger the observer?

    • The example code runs as follows:



    • The result can trigger the observer because the subclass calls the parent class’sinit, alreadyInitialize theAnd theInitialize theprocessensuretheAll attributes have values, soThe observation property can be triggered.

Lazy properties

    1. The initial value of the deferred storage property is not accessible until it is first used, using the keywordlazyTo identify a delay property, the delay property must beSet an initial value, the code is as follows:



    1. The following inSilAvailable types of analysis:
    class WSPerson {
      lazy var age: Int { get set }
      @_hasStorage @_hasInitialValue final var $__lazy_storage_$_age: Int? { get set }
      @objc deinit
      init(a)
    }
    Copy the code
    • inSilThe delay attribute is an optional type, continue to analyzegettermethods



    • The analysis is in the interviewgetterWhen, will be rightageThe assignment
  • Let’s look at the memory of the delay attribute:



    • It turns out that using the delay attribute results in memory enlargement

The type attribute

  • A type attribute belongs to the class itself. There is only one type attribute, no matter how many instances there are. We can use static to declare a type attribute:

    class WSPerson {
        static let name: String = "wushuang"
    }
    / / access
    let name = WSPerson.name
    Copy the code
    • To viewSilFile to observe its changes:



    • usestaticAfter modifying a variable, inSilA global variable is generated in the file, andnameIs thread unsafe
    • innametheglobal_initAs you can see,nameInitialize only once:



    • You can break points in code, then look at assembly, and thenTo enter (step into)functionWSPerson.name.unsafeMutableAddressorYou can see it called inswift_once



    • swift_onceLast called in the source codedispatch_once_f, that is, singletons



  • We can use static to create singletons

    class WSPerson {
        var name: String = "wushuang"
        var age: Int = 18
        static let share = WSPerson(a)private init(a){}}// Call the method
    let ws = WSPerson.share
    Copy the code

conclusion

    1. Storage properties: there are constant storage properties and variable storage properties, both of which take up memory
    1. Calculation properties: No memory usage
    1. Attribute Observer:
      1. Attribute observations can be added inClass storage properties,Inherited storage properties,Inherited computed propertiesIn the
      1. The parent class is callinginitTo change the attribute valueNot triggerProperty observation, the subclass calls the parent classinitwillThe triggerAttribute to observe
      1. The uniform attribute adds observations to both parent and subclass classes when the observation is triggered:
      • willSetMethod, subclass first, then superclass
      • didSetMethod, superclass and subclass
    1. The lazy attributeThe: delay attribute must have an initial value, only whenAfter the visitThe value is in memory, the delay property is internal,
    1. Type attributes: Type attributes must have initial values, memory is allocated only once, are thread-safe and can be used for singletons

Write in the last

Because the downloaded swift-source 5.5.1 did not compile successfully in xcode13.1, so it led to a lot of debugging is not convenient, if there is an incorrect place welcome to correct 🙏