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