“This is the sixth day of my participation in the First Challenge 2022. For details: First Challenge 2022.”
This article focuses on class and struct methods, as well as the underlying source structure for restoring methods.
methods
Variation method
Both classes and structs in Swift can define methods. The only difference is that the attributes of the struct value type cannot be modified by their own instance methods.
Here’s an example:
struct Color {
var red = 0, green = 0, blue = 0
func buildColor(red paraRed: Int.green paraGreen: Int.blue paraBlue: Int) {
red = 255
green = 255
blue = 255}}Copy the code
Report this error after writing this
Cannot assign to property: 'self' is immutable
Copy the code
Struct to modify the properties of a value type in an instance method, you need to load the keyword mutating in front of the method, which is equivalent to passing the initialized address. Let’s look at the difference in the method of adding or not adding the keyword by analyzing Sil.
Add another general method
struct Color {
var red = 0, green = 0, blue = 0
func test(a){
let r = self.red
}
mutating func buildColor(red paraRed: Int.green paraGreen: Int.blue paraBlue: Int) {
red = 255
green = 255
blue = 255}}Copy the code
Click File ->New -> Target and select Other -> Aggregate. After creating the sil File, follow this step
swiftc -emit-sil ${SRCROOT}/SwiftDemo/main.swift > ./main.sil && open main.sil
Copy the code
Then select the new build Target and run the project to generate the SIL file. By analyzing the SIL files, you can see the differences between the two methods
Not mutating is passing a value, but mutating is passing an address. As we all know, the default method passes self. The difference we find is that the mutating method marks self as an inout argument. This keyword indicates that the current parameter type is indirect, passing the already initialized address, so we can change the value.
The nature of mutant methods: For mutant methods, self passed in is marked as an inout parameter. Whatever happens inside the mutating method affects everything about the external dependency type.
Input and output parameters: If we want a function to be able to change the value of a formal parameter, and we want those changes to persist after the function ends, we need to define the formal parameters as input and output parameters. Prefixes the type defined by the formal parameter with the inout keyword.
var red = 0
func buildColor(_ red: inout Int) {
red = 255
}
buildColor(&red)
Copy the code
Method dispatch
Calling a method in OC is actually sending objC_msgsend. How do you schedule a method in Swift?
class Person {
func test(a){
print("test")}}var p = Person()
p.test()
Copy the code
Add the metadata address to the offset as the address, and assign the memory address to register X8, where X8 is the address of test.
Common register instructions:
Mov: copies the value of a register to another register (used only between registers or between registers and constants, not memory addresses), for example: mov x1, x0 Copies the value of register x0 to register x1 Ӿ LDR: reads the value from memory to a register, as in: LDR x0, [x1, x2] add the values of registers X1 and x2 as the address, take the value of the memory address and store it in register X0 bl: (branch) Jump to an address (no return) BLR: jump to an address (return)Copy the code
Run to view assembly code:
Test1: Find Metadata, determine the address of the function (Metadata + offset), and schedule the function based on the function table.
This is generating the SIL file, and viewing the file again confirms the scheduling based on the function table
Metadata is adata structure that was restored in the previous article
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
So what we’re going to do here is focus on the typeDescriptor, whether it’s class, struct, enum, they all have their own Descriptor, which is a detailed description of the class. We can look at this Descriptor by analyzing the swift source code, and we call the following method when we generate the classDescriptor
So we can follow this addVTable() method
In fact, this B is the TargetClassDescriptor, and at that point we can complete the structure of the TargetClassDescriptor.
struct TargetClassDescriptor{
var flags: UInt32
var parent: UInt32
var name: Int32
var accessFunctionPointer: Int32
var fieldDescriptor: Int32
var superClassType: Int32
var metadataNegativeSizeInWords: UInt32
var metadataPositiveSizeInWords: UInt32
var numImmediateMembers: UInt32
var numFields: UInt32
var fieldOffsetVectorOffset: UInt32
var Offset: UInt32
var size: UInt32
//V-Table
}
Copy the code
Mach-O is the executable file format. The common. O,. A,. Dylib, Framework, dyld,. Dsym are all executables.
Here’s an example of a class instance method call:
class Person {
func test1(a){
print("test1")}func test2(a){
print("test2")}func test3(a){
print("test3")}}class ViewController: UIViewController {
override func viewDidLoad(a) {
super.viewDidLoad()
var p = Person()
p.test1()
p.test2()
p.test3()
}
}
Copy the code
The information for our Swift class is stored here, and the address of the first four bytes is the address of the current class
You can use this to calculate the memory address of the current class in the Mach-O file
The Data area is mainly responsible for code and Data records, where the specific information of classes is stored. We calculate the memory address of classes in the Mach-O file by using the Data structure of Descriptor, and then calculate the VTable according to the Data structureThe first address
Image List prints the base address at which the program runs
Start address + Mach-o = the address of the function in memory + the base address of the program to run = the address of the function in the program to run
The address of the Impl can be found by adding Flags and Offset to the VTable data structure
This is then verified by running the assembly code to see if the memory address of Register Read X8 is the same as the impL address obtained above.
Calculate the impL address of the function:
0x00000001040c4000+B7A4 = 0x1040CF7A0+4+FFFFC1FC = 0x2040CB9A4
From assembly, read X8 register address:
Find the Metadata, determine the address of the function (Metadata + offset), schedule the function based on the function table, find the impL of the function, and execute it.
Struct instance calls the method:
Change the Person class above to a structure and look at the assembly
An address that executes directly is found, which is static dispatch.
Class instances that inherit the NSObject method call methods:
class Person: NSObject {
func test1(a){
print("test1")}func test2(a){
print("test2")}func test3(a){
print("test3")}}class ViewController: UIViewController {
override func viewDidLoad(a) {
super.viewDidLoad()
var p = Person()
p.test1()
p.test2()
p.test3()
}
}
Copy the code
You see that the call is the same as it was when you didn’t inherit NSObject, which is how the function table is distributed.
Method Scheduling method summary:
Affects the distribution mode of functions
- Final: Functions with the final keyword cannot be overridden, do not support inheritance, use static distribution, do not appear in the VTable, and are not visible to the OBJC runtime.
- Dynamic: The dynamic keyword can be added to all functions to add dynamics to non-objC class and value type functions, but the distribution mode is still function table distribution.
- Objc: this keyword exposes the Swift function to the objC runtime, which is still distributed from the function table.
- @objc + dynamic: the way messages are sent, inherited from NSObject, to expose methods to OC calls
As can be seen from the above, OC and Swift call functions in completely different ways. All method calls in OC are ultimately converted into a C language message distribution function in Runtime.
An inline function
If compiler optimization is turned on, the compiler automatically makes some functions inline (expanding function calls into function bodies). In fact, optimization is enabled by default in Release mode, and it is optimized according to speed.
For example, if we have a function, once the test() function is called, the system will allocate stack space for this function, and allocate local variables in the stack space. After the function is finished, the stack space will be reclaimed. If the test() function only does one simple thing, isn’t that a waste of performance? Such as:
func test(a){
print("end")
}
test()
Copy the code
What an inline function does, however, is expand the function call into the function body code, which reduces the overhead of the function call and eliminates the need to create stack space to recycle the function.
Debug -> Debug Workflow -> Always Show Disassembly
This callq calls the test method
Turn on compiler optimization and see what happens at run time
It turns out that the breakpoint is not broken at all, and then it prints. At this point, the breakpoint is hit at the output function
If we look at the assembly, we can see that the print(“end”) code is placed directly in the main function, so the compiler does the inlining for us.
The function will not be inlined:
1. Functions with long function body will not be inlined (if the function body is long and the function is called many times, the assembly code generated by inlining operation will be very much, that is, the machine code will be more, and the final code volume will be larger and the installation package will be larger)
2. Recursive calls are not inline
3. Functions that contain dynamic dispatch (similar to OC dynamic binding) are not inlined
The above three articles cover and classes and structures and the differences between them.