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:
iOS
pointsOC
andSwift
Both languages, but their backends are passedLLVM
Compiled, as shown below:
OC
Is through theClang
The compiler compiles toIR
, and then regenerate it into an executable file.o
Swift
Is through theswiftc
The 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:
-
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
-
Semantic analysis
: Semantic analysis (inIn the lib/Sema implementation) is responsible for getting the parsedAST
And convert it to a well-formed, fully type-checkedAST
Form to warn or error about semantic problems in source code. Semantic analysis includes type inference, which, if successful, indicates that the generated, type-checkedAST
The generated code is safe
-
Clang importer(Clang importer)
:Clang
The importer (inIn the lib/ClangImporter implementation) the importClang
And I’m going to export themC
orObjective-C API
Map to the correspondingSwift API
. Generated importAST
References can be made through semantic analysis
-
SIL Generation
:Swift
Intermediate language(SIL)
Is a kind of advanced, specific toSwift
Intermediate language for further analysis and optimizationSwift
The code.SIL
Generation phase (inIn the lib/SILGen implementation) will be type checkedAST
Reduced to what’s called “primitive.”SIL
.SIL
The design of theIn docs/ sil.rstdescribe
-
SIL guaranteed transformations
:SIL
Ensure 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
.
-
SIL Optimizations
:SIL
Optimization (inlib/Analysis,lib/ARC,lib/LoopTransformsandIn the lib/TransformsPerform additional high-level, specific to the programSwift
Optimizations, including (for example) automatic reference counting optimization, de-virtualization, and generic specialization
-
LLVM IR Generation(LLVM IR Generation)
:IR
Generated (inIn the lib/IRGen implementation) will beSIL
Down toLLVM IRAt this time,LLVM
You can continue to optimize it and generate machine code.
-
- It is mainly divided into the following processes:
SIL is the intermediate code during Swift compilation, located between AST and LLVM IR
Generate the SIL
- The following will
swift
Code generationSIL
File:// main.swift class WSPerson { var age: Int = 18 var name: String = "wushuang" } var ws = WSPerson(a)Copy the code
swift
The front-end compiler used during compilation isswiftc
, you can use the commandswiftc -emit-sil main.swift >> ./main.sil
willswift
Files are generatedSIL
File:
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
@main
The function ismain.swift
File entry function,% % 0 and 1
isSIL
The register, which does not change after assignment, can be understood as a constant, is a virtual register, and is used when viewing assemblyregister read
Different registers inmain
The main function is to do some object creation work, there are mainly the following steps-
- Create a global variable
ws
And the assignment% 2
- Create a global variable
-
- Get global variables
ws
And assign a value to% 3
- Get global variables
-
- To obtain
WSPerson
Metadata type and assign a value to% 4
- To obtain
-
- will
WSPerson.__allocating_init()
The function assigns a value to% 5
- will
-
- Called based on the metadata type
__allocating_init
The function creates an object and assigns the result to% 6
- Called based on the metadata type
-
- Object to be created
% 6
Store global variables% 3
Address, that is, to the global variablews
For the assignment
- Object to be created
-
return
The end of themain
function
-
Create an object
- In the above analysis, we know that the core of creating an object is calling
__allocating_init
Function, 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:
-
- Created on the heap
WSPerson
Type and assign a value toThe % 1
- Created on the heap
-
- To obtain
WSPerson.init()
Function and assign to% 2
- To obtain
-
- object
The % 1
callinit
Method and returns the current object
- 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:
-
- call
swift_slowAlloc
The function calculates the memory size
- call
-
- Based on memory and metadata type
The heap area
Create an object
- Based on memory and metadata type
swift_slowAlloc
Computational 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
alignMask
It’s memory alignedmask
In theA 64 - bit
is16 bytes
Alignment, and32 -
is8 bytes
Align. See how you alignIOS low-level – Memory alignment- As for the
MALLOC_ALIGN_MASK
Why the alignment number minus1
Because when you calculate alignment, you have to&
on~MALLOC_ALIGN_MASK
, e.g.7
.~ 7
is8
Multiples.
-
-
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 above
HeapObject
The constructor takes two argumentsmetadata
isHeapMetadata
Type of pointer, accounting8 bytes
.- By looking for
refCounts
Did you get itInlineRefCounts
Type, occupation8 bytes
- At this point the conclusion can be drawn,
Swift object
Essential forHeapObject
, to take up16 bytes
.
- In the above
Memory allocation
-
Class WSPerson () {class_getInstanceSize ();
- Result-occupied
40 bytes
Why is that? To useMemoryLayout < type >. Stride
To print separatelyInt
andString
Memory 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
- Result-occupied
conclusion
-
- Object memory allocation process:
__allocating_init
->swift_allocObject_
->swift_slowAlloc
->malloc
- Object memory allocation process:
-
Swift
In theInstance objects
Take up16 bytes
, thanOC
In the moreRefCounted (reference count size)
class
-
Metadata is a pointer to HeapMetadata.
template <typename Target> struct TargetHeapMetadata; using HeapMetadata = TargetHeapMetadata<InProcess>; Copy the code
HeapMetadata
isTargetHeapMetadata
Alias 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 code
TargetHeapMetadata
inheritanceTargetMetadata
The core of the method is based on parametersKind (incoming InProcess)
callTargetMetadata
The construction method of. Let’s continue with the code
- In 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 in
InProcess
In structureuintptr_t
, continue to check the type to seeunsigned long
Type:
struct InProcess {.using StoredPointer = uintptr_t; . }typedef unsigned long uintptr_t; Copy the code
- so
kind
isunsigned long
Type, which mainly distinguishes the current type of data
- It’s actually passing in
-
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 the
getClassObject
The function is based onkind
To obtainobject
Type:
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
- One of the
-
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 and
OC
In theobjc_class
The structure is the same as that ofisa
There is a parent class, there iscacheData
.Data
Similar to theobjc_class
In thebits
- The structure here and
-
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 in
SIL
The 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
- in
Sil
Constant store properties in files can only be usedgetter
Method, variable store properties cansetter
andgetter
methods
- The value of a constant store property cannot be modified, while the value of a variable store property can be modified in
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 @_hasInitialValue
identifier
- You can see that the evaluated property does not evaluate all of the properties
-
-
Print the memory usage of these two classes:
- The results of
WSName
The occupied memory is48 byte
.familyName
Take up16 bytes
.givenName
Take up16 bytes
.metadata
Take up8 bytes
.refCount
Take up8 bytes
It adds up just right48 byte
.fullName
No memory usage. Square
In thewidth
Take up8 bytes
.metadata
Take up8 bytes
.refCount
Take up8 bytes
It adds up just right24 bytes
.area
No memory usage- Therefore, it is concluded that calculating attributes takes no memory
- The results of
Attribute observer (willSet/didSet)
-
The property observer can be understood as KVO in OC, when the property calls setter methods:
-
- Called before the new value is stored
willSet
Method to get a new valuenewValue
.
- Called before the new value is stored
-
- Called after the new value is stored
didSet
Method to get the old valueoldValue
- Called after the new value is stored
- As shown in the following case:
- in
SIL
File viewing:
- in
name.setter
The following operations are performed in the function:-
- Gets the old value of name and stores it in a register
% 6
- Gets the old value of name and stores it in a register
-
- call
name.willSet
Function and pass in the new value% 0
- call
-
- Store the new value
name
And releases the old value
- Store the new value
-
- call
name.didSet
Function and pass in the old value% 6
- call
-
-
-
Question 1: Does changing the value of an attribute in the init method trigger an observer?
- Add in the case
init
Run again
- It didn’t trigger
willSet
anddidSet
Methods,SIL
According to the analysis of the documents,init
To 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 triggeredsetter
Method, which does not trigger the property observer
- Add in the case
-
Question 2: Where can I add attribute observations
- You can add observers in three places:
-
- Class to define a storage property
-
- Store attributes inherited by a class
-
- Attributes are evaluated by class inheritance
-
- You can add observers in three places:
-
Problem 3: Both subclasses and superclasses have computed attributes
DidSet, willSet
When, 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 time
DidSet, willSet
When 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 - after
didSet
: superclass, subclass
- First,
-
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’s
init
, alreadyInitialize the
And theInitialize the
processensure
theAll attributes have values
, soThe observation property can be triggered
.
Lazy properties
-
- The initial value of the deferred storage property is not accessible until it is first used, using the keyword
lazy
To identify a delay property, the delay property must beSet an initial value
, the code is as follows:
- The initial value of the deferred storage property is not accessible until it is first used, using the keyword
-
- The following in
Sil
Available 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
- in
Sil
The delay attribute is an optional type, continue to analyzegetter
methods
- The analysis is in the interview
getter
When, will be rightage
The assignment
- The following in
-
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 view
Sil
File to observe its changes:
- use
static
After modifying a variable, inSil
A global variable is generated in the file, andname
Is thread unsafe - in
name
theglobal_init
As you can see,name
Initialize only once:
- You can break points in code, then look at assembly, and then
To enter (step into)
functionWSPerson.name.unsafeMutableAddressor
You can see it called inswift_once
swift_once
Last called in the source codedispatch_once_f
, that is, singletons
- To view
-
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
-
- Storage properties: there are constant storage properties and variable storage properties, both of which take up memory
-
- Calculation properties: No memory usage
-
- Attribute Observer:
-
- Attribute observations can be added in
Class storage properties
,Inherited storage properties
,Inherited computed properties
In the
- Attribute observations can be added in
-
- The parent class is calling
init
To change the attribute valueNot trigger
Property observation, the subclass calls the parent classinit
willThe trigger
Attribute to observe
- The parent class is calling
-
- The uniform attribute adds observations to both parent and subclass classes when the observation is triggered:
willSet
Method, subclass first, then superclassdidSet
Method, superclass and subclass
-
- The lazy attributeThe: delay attribute must have an initial value, only when
After the visit
The value is in memory, the delay property is internal,
- The lazy attributeThe: delay attribute must have an initial value, only when
-
- 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 🙏