In the previous article structure and class, we learned the essential difference between structure and class, and through reading Swift source code and verification, we learned that the essence of Swift class is the structure pointer of HeapObject.

When referring to an attribute of the current instance in a structure or a method inside a class, it is generally not necessary to prefix the attribute with the self keyword, such as self.age. I’m going to omit it. I’m going to write age. Let’s take a closer look at Swift’s approach.

Function-related modifiers

1. mutating

Constructs and enumerations are value types. By default, the properties of value types cannot be modified by their own instance methods. Mutating before the func keyword allows this modification behavior.

2. inout

Inout – In-out Parameter. You can define an input and output parameter with inout, and you can modify the value of the external argument inside the function. Inout needs to pay attention to the following points:

  • A variable parameter cannot be marked asinout.
  • inoutParameters cannot have default values.
  • inoutArguments can only be passed in values that can be assigned more than once.
  • inoutThe essence of a parameter is address passing (reference passing).

3. @discardableResult

Use @discardableresult in front of func to eliminate the warning that a function call returns an unused value ⚠.

4. final

  • befinalModified class that is prohibited from being inherited.
  • befinalModifier methods, subscripts, and attributes are not allowed to be overridden. And functions that add the final keyword cannot be overridden, using static distribution, and not invtableAppears in, andobjcNot visible at runtime.

5. dynamic

The dynamic keyword can be added to all functions to give dynamics to functions of non-objC classes and value types, but the distribution is still from the function table.

6.@objc

This keyword exposes the Swift function to the OBJC runtime, which interacts with the OC, but still dispatches the function table. So, @objc + Dynamic becomes the mode of message dispatch – that is, message passing in OC.

Two, the structure of the method

Next there is a SHPoint structure with x and y attributes. When I change the values of x and y in moveBy, the compiler returns the following error:

This modification behavior is allowed when we use mutating.

Here we consider a question: why can mutating be modified after adding it? What is the nature of it. The code is as follows:

struct SHPoint {
    var x = 5.0, y = 10.0

    func sum(a) {
        let result = x + y
        print(result)
    }

    mutating func moveBy(deltaX: Double.deltaY: Double) {
        x + = deltaX
        y + = deltaY
        print(x,y)
    }
}

var p = SHPoint()
p.sum()
p.moveBy(deltaX: 10, deltaY: 20)
Copy the code

We compiled the current Swift code into SIL code, and there was a pit I had to fill in before compiling.

1. Precautions for generating.sil files

I introduced SIL in my last article, Structures and Classes, by using the swiftc main. swift-emit – SIL command to generate.sil files and output SIL code on the terminal. Then I opened the.sil file and found nothing in it. Then I looked at the file size:

Boy, there was the output SIL code on the terminal, but the generated file was nothing. At this point, I was using a Mac with an Intel chip. When compiling.sil files with M1 computers, I could not see the.sil files.

Swiftc main. swift-emit -sil simply compiles the code of the main.swift file in the current directory into sil code and outputs it on the terminal. So what? I looked up some information and got the following commands for generating.sil and.ll files.

// Compile the main.swift file from the current directory into the main.sil file and save it to the current directory.
swiftc -emit-sil main.swift >> main.sil
// Compile to sil with translation
swiftc -emit-sil main.swift | xcrun swift-demangle >> main.sil
// compile to ir with translation
swiftc -emit-ir main.swift | xcrun swift-demangle >> main.ll
// Compile UIKit to sil
swiftc -emit-sil -target x86_64-apple-ios13.5-simulator -sdk $(xcrun --show-sdk-path --sdk iphonesimulator) ViewController.swift > ViewController.sil
Copy the code

To view more SwifTC commands, enter swiftc-h on the TERMINAL.

To open the.sil file for easy reading, I used swiftc-ema-sil main.swift >> main.sil to generate the main.sil file.

2. Main. Sil analysis

For easy reading, drag the main.sil file to the project’s directory file, the same directory as main.swift, as follows:

Specific content we can go to see their own generation, I only for the important part of the code posted to do explain. The first paragraph reads as follows:

// We define the SHPoint structure
struct SHPoint {
    / / x attributes
    @_hasStorage @_hasInitialValue var x: Double { get set }
    / / y properties
    @_hasStorage @_hasInitialValue var y: Double { get set }
    / / sum method
    func sum(a)
    / / moveBy method
    mutating func moveBy(deltaX: Double.deltaY: Double)
    // Initialize method
    init(a)
    init(x: Double = 5.0.y: Double = 10.0)
}

// Create the p variable
@_hasStorage @_hasInitialValue var p: SHPoint { get set }
Copy the code

The definition of the SHPoint structure is compiled into SIL code as above, but it does not seem to show the essential difference between SUM and moveBy. Let’s go to the main function and see how the method is called.

2.1. call to the SIL method

// function_ref SHPoint.sum()
// Note that s4main7SHPointV3sumyyF is the name of the sum mixed with SIL,
%11 = function_ref @$s4main7SHPointV3sumyyF : $@convention(method) (SHPoint) - > ()// user: %12
%12 = apply %11(%9) : $@convention(method) (SHPoint) - > ()%13 = float_literal $Builtin.FPIEEE64.0x4024000000000000 // 10 // user: %14
%14 = struct $Double (%13 : $Builtin.FPIEEE64) / /user: % 15 = 19%float_literal $Builtin.FPIEEE64. 0x4034000000000000/ / / / 20user: % 16 = 16%struct $Double (%15 : $Builtin.FPIEEE64) / /user19: %Copy the code

S4main7SHPointV3sumyyF is the name of sum mixed with SIL.

xcrun swift-demangle <The name of the mix-up>
Copy the code

With this command, the terminal prints the sum method in SHPoint. Moving on to the %11 line, notice that the sum method does not take any arguments in the Swift code, but the underlying SIL implementation does, by default, take an argument -shpoint, which is actually an instance of SHPoint -self, That’s why we can call self. The reason why.

2.2. Call to SIL of moveBy method

%17 = begin_access [modify] [dynamic] %3 : $*SHPoint // users: %20, %19
// function_ref SHPoint.moveBy(deltaX:deltaY:)
%18 = function_ref @$s4main7SHPointV6moveBy6deltaX0E1YySd_SdtF : $@convention(method) (Double.Double.@inout SHPoint) - > ()// user: %19
%19 = apply %18(%14.%16.%17) : $@convention(method) (Double.Double.@inout SHPoint) -> ()
end_access %17 : $*SHPoint                      // id: %20
Copy the code

Looking at the line %18, our moveBy method takes three arguments in the SIL implementation, deltaX, deltaY, and SHPoint(self). Attention! Unlike sum, SHPoint passed by moveBy has an inout in front of it.

In a structure, if a method is preceded by a mutating modifier, the SHPoint argument is preceded by inout, essentially passing the address of the SHPoint instance. This is when the structure can be modified by its own instance methods.

2.3. Implementation of SUM method SIL

To reduce the SIL code generated by print in the method, I regenerated the print comment into the main.sil file.

Sum = s4main7SHPointV3sumyyF sum = s4main7SHPointV3sumyyF

// SHPoint.sum()
sil hidden @$s4main7SHPointV3sumyyF : $@convention(method) (SHPoint) - > () {// %0 "self" // users: %3, %2, %1
    bb0(%0 : $SHPoint):
    debug_value %0 : $SHPoint.let, name "self", argno 1 // id: %1
    %2 = struct_extract %0 : $SHPoint#,SHPoint.x   // user: %4
    %3 = struct_extract %0 : $SHPoint#,SHPoint.y   // user: %5
    %4 = struct_extract %2 : $Double#,Double._value // user: %6
    %5 = struct_extract %3 : $Double#,Double._value // user: %6
    %6 = builtin "fadd_FPIEEE64"(%4 : $Builtin.FPIEEE64.%5 : $Builtin.FPIEEE64) : $Builtin.FPIEEE64 // user: %7
    %7 = struct $Double (%6 : $Builtin.FPIEEE64) / /user8: %debug_value %7 : $Double.let.name "result"    // id: % 9 = 8%tuple(a) / /user: % 10return %9 : $(a) / /id: % 10} / /end sil function '$s4main7SHPointV3sumyyF'
Copy the code

Take the values of SHPoint x and y, add them, and assign them to a constant named result.

2.4. Implementation of moveBy method SIL

The search process for moveBy is the same as sum, and its SIL implementation is as follows:

// SHPoint.moveBy(deltaX:deltaY:)
sil hidden @$s4main7SHPointV6moveBy6deltaX0E1YySd_SdtF : $@convention(method) (Double.Double.@inout SHPoint) - > () {// %0 "deltaX" // users: %10, %3
    // %1 "deltaY" // users: %20, %4
    // %2 "self" // users: %16, %6, %5
    bb0(%0 : $Double.%1 : $Double.%2 : $*SHPoint):
    debug_value %0 : $Double.let, name "deltaX", argno 1 // id: %3
    debug_value %1 : $Double.let, name "deltaY", argno 2 // id: %4
    debug_value_addr %2 : $*SHPoint.var, name "self", argno 3 // id: %5
    %6 = begin_access [modify] [static] %2 : $*SHPoint // users: %15, %7
    %7 = struct_element_addr %6 : $*SHPoint#,SHPoint.x // users: %13, %8
    %8 = struct_element_addr %7 : $*Double#,Double._value // user: %9
    %9 = load %8 : $*Builtin.FPIEEE64               // user: %11
    %10 = struct_extract %0 : $Double#,Double._value // user: %11
    %11 = builtin "fadd_FPIEEE64"(%9 : $Builtin.FPIEEE64.%10 : $Builtin.FPIEEE64) : $Builtin.FPIEEE64 // user: %12
    %12 = struct $Double (%11 : $Builtin.FPIEEE64) / /user: % 13store% 12to %7 : $*Double                      // id: % 14 = 13%tuple(a)end_access %6 : $*SHPoint                       // id: % 16 = 15%begin_access [modify] [static] %2 : $*SHPoint // users: %25, %17
    %17 = struct_element_addr %16 : $*SHPoint#,SHPoint.y // users: %23, %18
    %18 = struct_element_addr %17 : $*Double#,Double._value // user19 = : % 19%load %18 : $*Builtin.FPIEEE64             // user: % 20 = 21%struct_extractThe % 1:$Double#,Double._value // user: 21% % = 21builtin "fadd_FPIEEE64"(%19 : $Builtin.FPIEEE64, %20 : $Builtin.FPIEEE64) : $Builtin.FPIEEE64 // user: % 22 = 22%struct $Double (%21 : $Builtin.FPIEEE64) / /user: % 23store% 22to %17 : $*Double                     // id: % 24 = 23%tuple(a)end_access %16 : $*SHPoint                      // id: % 25% = 26tuple(a) / /user: % 27return %26 : $(a) / /id: % 27} / /end sil function '$s4main7SHPointV6moveBy6deltaX0E1YySd_SdtF'
Copy the code

It’s a little long, but notice begin_access and end_access, these two come in pairs, the first pair is the code for x += deltaX, so the second pair is the code for y += deltaY.

The sum method is $SHPoint and the sum method is $SHPoint. This further proves that after using mutating modification, is to take the address, for the address of the memory operation.

3. The nature of calling struct methods

In OC, the essence of calling a method is messaging, with the underlying objc_msgSend function to find the method and call it. Swift is a static language with no runtime mechanism, so how do native Swift methods get called?

Let’s start with assembly by looking at how the underlying structure is called when a method on a structure is called. The code is as follows:

We call the breakpoint and enter the assembly code to view the current call:

It can be found that in Swift, the method to call a structure is to directly call the function address, including the initialization method, which is not as complex as OC.

It is important to note that struct class method calls are called directly from the address of the function, just like instance method calls. Declaring a class method in Swift requires the static keyword before func.

Swift is a static language, many things can be determined at run time, so you can directly get the address of the function to call, the form of this call can also be called static dispatch.

Class methods

Now that we know about method calls to the Swift structure, what about Swift classes? Same as struct one, but no matter it’s different from struct one, it’s definitely different from OC’s class.

1. Class method assembly calls

We are building a new Swift project. It should be noted that we must use a real computer to run it, because all our iOS programs will be installed on mobile phones, and the current architecture of mobile phones is basically ARM64 architecture. Define a SHPerson type, call the method, and set a breakpoint as follows:

Set a breakpoint to see how the Swift class method is called in assembly.

class SHPerson {
    func setName1(a) {}
    func setName2(a) {}
    func setName3(a){}}Copy the code

As mentioned earlier, the BLR instruction jumps to an address (no return), that is, the x8, x9 registers in this process are the address of the function.

There’s one more point. Watch it! The instruction that calls the initialization method is BL, which means that there is a return value, which is the instance object of SHPerson. So in general, x0 is going to store the return value of this function.

Look at line 19.

0x1024cfd5c <+68>:  ldr    x8, [x0]
Copy the code

So what this code is saying is, take the address of X0, store it in x8, and notice, this is the beginning of the address of x0, and we’re going to do 8 bytes. The address of x8 is the address of metadata.

Set the breakpoint at line 20, retry, and read the value of x8:

X8 is the address of the metadata. And then you go to line 23 and hit the down button at the control point and jump in. Line 29,33, see if it corresponds to setName1, setName2, setName3.

It really is setName1, setName2, setName3. Although it is also called to get the address of the function, but obviously, the system is to get the SHPerson instance object, and get the address of the metadata, through the way of memory translation, get the address of the function to call again.

The addresses of functions are stored consecutively, unlike OC, which is stored in an unordered hash table. So, where is the address of the function stored?

2. Introduction of virtual function tables

Use this command to generate the viewController.sil file.

swiftc -emit-sil -target x86_64-apple-ios13.5-simulator -sdk $(xcrun --show-sdk-path --sdk iphonesimulator) ViewController.swift > ViewController.sil
Copy the code

After generating the viewController.sil file, we open the file and see the very bottom of the file.

sil_vtable SHPerson{#SHPerson.setName1: (SHPerson) -> () -> () : @$s14ViewController8SHPersonC8setName1yyF    // SHPerson.setName1()
    #SHPerson.setName2: (SHPerson) -> () -> () : @$s14ViewController8SHPersonC8setName2yyF    // SHPerson.setName2()
    #SHPerson.setName3: (SHPerson) -> () -> () : @$s14ViewController8SHPersonC8setName3yyF    // SHPerson.setName3()
    #SHPerson.init!allocator: (SHPerson.Type) - > () - >SHPerson : @$s14ViewController8SHPersonCACycfC    // SHPerson.__allocating_init()
    #SHPerson.deinit!deallocator: @$s14ViewController8SHPersonCfD    // SHPerson.__deallocating_deinit
}

sil_vtable ViewController{#ViewController.deinit!deallocator: @$s14ViewControllerAACfD    // ViewController.__deallocating_deinit
}
Copy the code

Vtable = vtable = vtable = vtable = vtable = vtable = vtable = vtable

3. Source lookup virtual function table

The Swift class contains a metadata, and the metadata contains a member variable. The member variable should look like this:

var typeDescriptor: UnsafeMutableRawPointer
Copy the code

This member contains a description of itself. Classes, structs, and enumerations all contain this member variable.

So let’s find the definition of typeDescriptor in the source code. Based on the structure and class in the previous article, the search process is as follows:

  • findHeapObject.
  • fromHeapObjectFound in theHeapMetadata.
  • Follow up,HeapMetadataTargetHeapMetadataThe alias.
  • findTargetHeapMetadataStructure.

If you find TargetHeapMetadata, it has a member variable that looks like this:

TargetSignedPointer<Runtime, const TargetClassDescriptor<Runtime> * __ptrauth_swift_type_descriptor> Description;
Copy the code

At this point, Description is a class that’s TargetClassDescriptor, and inherits a bunch of stuff.

You can compare the structure, and the result of that comparison, the TargetClassDescriptor looks something like this:

class 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

So the TargetClassDescriptor actually has a separate descriptor called ClassDescriptor, which is defined as follows:

using ClassDescriptor = TargetClassDescriptor<InProcess>;
Copy the code

So we searched globally for the ClassDescriptor, and we found the genmeta. CPP file, and from the name, we can guess that the code in the genmeta. CPP file is where the metadata was generated.

We enter to GenMeta. The CPP file, positioning to ClassContextDescriptorBuilder directly the description of this class – founder, this class is to create the metadata and class of Descriptor.

Find the Layout method in the class:

void layout() {
    super::layout();
    addVTable();
    addOverrideTable();
    addObjCResilientClassStubInfo();
    maybeAddCanonicalMetadataPrespecializations();
}
Copy the code

Super :: Layout () is called inside. Let’s look at the implementation of the parent class:

void layout() {
    asImpl().computeIdentity();

    super::layout();
    asImpl().addName();
    asImpl().addAccessFunction();
    asImpl().addReflectionFieldDescriptor();
    asImpl().addLayoutInfo();
    asImpl().addGenericSignature();
    asImpl().maybeAddResilientSuperclass();
    asImpl().maybeAddMetadataInitialization();
}
Copy the code
void layout() {
    asImpl().addFlags();
    asImpl().addParent();
 }
Copy the code

So at this point, it basically corresponds to the member variable of the TargetClassDescriptor class. This function adds the Swift class method to the virtual table as follows:

void addVTable() {
    LLVM_DEBUG(
    llvm::dbgs() << "VTable entries for " << getType()->getName() << ":\n";
    for (auto entry : VTableEntries) {
        llvm::dbgs() << "";
        entry.print(llvm::dbgs());
        llvm::dbgs() <<'\n'; });// Only emit a method lookup function if the class is resilient
    // and has a non-empty vtable, as well as no elided methods.
    if (IGM.hasResilientMetadata(getType(), ResilienceExpansion::Minimal)
    && (HasNonoverriddenMethods || !VTableEntries.empty()))
    IGM.emitMethodLookupFunction(getType());

    if (VTableEntries.empty())
    return;

    auto offset = MetadataLayout->hasResilientSuperclass()
       ? MetadataLayout->getRelativeVTableOffset()
       : MetadataLayout->getStaticVTableOffset();
    B.addInt32(offset / IGM.getPointerSize());
    B.addInt32(VTableEntries.size());

    for (auto fn : VTableEntries) { emitMethodDescriptor(fn); }}Copy the code

Forget about the previous code, but let’s focus on the last few lines. After calculating offset, we call the addInt32 function, which calculates the offset to add methods to the virtual function table. Finally, through the for loop, we add a pointer to the function.

This offset is the sum of all the memory sizes of the member variables in the TargetClassDescriptor structure, and at the end we get vtableentries.size ().

That means that the memory address of the virtual function table is the last member variable in the TargetClassDescriptor, and the form of the add method is appended to the end of the array. So the virtual function table is a sequential store of Pointers to the class’s methods.

4. Method storage of MachOView analysis class

4.1. The Mach – O files

Mach-O is short for Mach Object file format, which is a latticework for MAC and iOS executables. It is similar to the Portable PE Format on Windows and the ELF Format on Linux. Common examples are.o,.a. Dylib Framework, dyld.dsym.

  • The first is the file header, which indicates that the file is in Mach-O format, specifies the target schema, and some other information about the file attributes, which affects the subsequent structure of the file.
  • Load Commands is a table that contains many things. The content includes the location of the region, symbol table, dynamic symbol table, etc.
  • DataRegions are mainly responsible for code and data logging. Based on the Mach – OSegmentThis is a structure for organizing dataSegmentIt can contain zero or moreSection. Depending on which Segment is mappedLoad Command.SegmentSectionCan be interpreted as code, constants, or some other data class. When loaded in memory, also according toSegmentDo memory mapping.

withMachOViewThe tool opens the Mach-o file in something like this:

Here is a description of some of the fields in the Mach-O file.

LC_SEGMENT_64          // Map segments (32-bit or 64-bit) in a file to the process address space
LC_DYLD_INFO_ONLY      // Dynamically link related information
LC_SYMTAB              // symbolic address
LC_DYSYMTAB            // Dynamic symbol table address
LC_LOAD_DYLINKER       / / dyld load
LC_UUID                // UUID of the file
LC_VERSION_MIN_MACOSX  // Support the lowest operating system version
LC_SOURCE_VERSION      // Source code version
LC_MAIN                // Set the main thread entry address and stack size
LC_LOAD_DYLIB          // Path to dependent libraries, including tripartite libraries
LC_FUNCTION_STARTS     // Start address table of function
LC_CODE_SIGNATURE      // Code signature
Copy the code

4.2. Mach-o file lookup method memory structure address

After running the project, an executable file is generated and opened with the MachOView tool. Note, however, that the project above has Chinese names, and the executable generated by the project with Chinese names will flash back when opened with MachOView. So create a new project with no Chinese names.

At the time of new project, Xcode does not automatically generate Products file, you can refer to this article: blog.csdn.net/u012275628/…

Once resolved, we Show in Finder, display the package contents, and open the executable using the MachOView tool.

Swift5_types stores structures, enumerations, and class descriptors, so we can find the address information of class descriptors in swift5_types.

The first four bytes 90 FB FF FF is the SHPerson Descriptor information, so 90 FB FF FF plus the previous 0000BBDC is the memory address of the current Mach-O file Descriptor.

How do they add up? IOS is in little endian mode, so 90 FB FF FF is read from right to left. That is:

FFFFFB90 + 0000BBDC = 0x10000B76C
Copy the code

0x10000B76C is the value I calculated with my calculator, so 0x100000000 is the base address of virtual memory in the Mach-O file, as shown in the figure.

0x10000B76C – 0x100000000 = B76C is the memory address of SHPerson in the entire Data area. We find TEXT, const.

So as you can see, this is the start address of B76C, which means that the data after it is the data in the TargetClassDescriptor, so we can get the address of the SHPerson virtual function table here, the SHPerson method.

Calculate the size of the data in front of the VTable in TargetClassDescriptor, and get the offset. There are 12 4-byte (48-byte) member variables. 12 4-byte member variables plus size (4 bytes) give 52 bytes. The next 24 bytes are the structural addresses of the setName1, setName2, and setName3 methods (one function address is 8 bytes). As shown in the figure:

As shown in the figure, B7A0 is the address of the setName1 structure in the Mach-O file. So how do you find that address in your program.

4.3. Verify whether the function address analyzed by Mach-O is consistent with the program running

ASLR is a random offset address whose purpose is to give an application a random memory address.

We break a breakpoint, and when the program is running, type LLDB: image list. As shown in the figure:

Image list is a list of modules that the application runs on. We find the first one, whose memory address is 0x0000000100728000, so we can use this address as the base address of the application.

Next I found such a structure in the source code. The TargetMethodDescriptor is the in-memory structure of the Swift method, and the Impl is not really AN IMP, but a relative pointer offset.

/// An opaque descriptor describing a class or protocol method. References to
/// these descriptors appear in the method override table of a class context
/// descriptor, or a resilient witness table pattern, respectively.
///
/// Clients should not assume anything about the contents of this descriptor
/// other than it having 4 byte alignment.
struct TargetMethodDescriptor {
    /// Flags describing the method.
    // 4 bytes, Flags which method is used.
    MethodDescriptorFlags Flags;

    /// The method implementation.
    // this is not a real IMP, but a pointer to offset.
    TargetRelativeDirectPointer<Runtime, void> Impl;

    // TODO: add method types or anything else needed for reflection.
};
Copy the code

At this point, the address of the TargetMethodDescriptor structure can be determined, so to find the function address, you have to offset Flags + Impl, which is the address of the function. Combine the above logic to start calculating:

// Application base address: 0x0000000100728000, setName1 structure address: B7A0, Flags: 0x4, offset: 90 C5 FF FF
/ / attention! The small-endian mode is from right to left, so FFFFC590
0x0000000100728000 + B7A0 + 0x4 + FFFFC590 = 0x20072FD34

// Subtract the virtual address of the Mach-o file 0x100000000 to get the address of the function.
0x20072FD34 - 0x100000000 = 0x10072FD34
Copy the code

Open assembly debugging, read the address of setName1 in assembly, and verify that 0x10072FD34 is the address of setName1. As shown in the figure:

Perfect! 0x10072FD34 is the address of the setName1 function. This fully verifies that the Swift class’s methods are indeed stored in the vtable-virtual function table.

5. Method calls in extension

Now that we know where the Swift class’s methods are stored and how they are called, let’s take a look at how the class’s methods are called in Extension.

Add extension to the existing SHPerson and add the setName4 method.

extension SHPerson {
    func setName4(a){}}Copy the code

After the program runs, through the assembly check found that the Swift class extension method was not placed in the virtual function table, but directly address call. Let’s look at a picture:

Suppose you now have two classes, SHPerson and SHStudent, and SHStudent inherits from SHPerson. In Swift, each class has its own table of virtual functions, and if I add a method to SHPerson’s Extension at this point, SHStudent can call extension’s methods in addition to SHPerson’s.

At this point, method storage becomes a problem and it is impossible to append to the end of the virtual table, which would result in method storage confusion because SHStudent might have more methods than SHPerson. Considering the order of storage, it is necessary to record complex operations such as indexes, inserts, and lookups, which can be costly in performance.

Therefore, for optimization, it is better to make a static call to extension directly outside of the virtual function table. When the program is compiled, the address of the function is already known, so there is no need to worry about the complex operation of recording indexes, inserting, and finding methods.

Inline functions

An inline function is a compiler optimization technique that optimizes performance by calling a method directly using its content substitution.

If compiler optimization is enabled (which is enabled by default in Release mode), the compiler automatically makes some functions inline – expanding function calls into function bodies. The mode of manual modification is shown in the figure below:

  • always– Will ensure that functions are always inlined. By adding before the function@inline(__always)To implement this behavior.
  • never– Will ensure that functions are never inlined. This can be done by adding it before the function@inline(never)To implement.
// Will never be inlined (even if compiler optimization is enabled)
@inline(never) func test(a) {
print("test")}// When compiler optimization is enabled, even long code is inlined (except recursively called functions and dynamically distributed functions)
@inline(__always) func test(a) {
print("test")}Copy the code

In Release mode, the compiler already has optimization enabled and will automatically determine which functions need to be inline, so @inline is not necessary.

Which functions are not automatically inlined?

  • The body of the function is longer.
  • Contains recursive calls.
  • Includes dynamic dispatch.

Five, ARM assembly common instructions

  • Mov: to copy the value of a register to another register (used only to transfer values between registers or constants, not memory addresses), as in:
mov x1, x0     // Copy the value of register x0 into register x1
Copy the code
  • Add: To add the value of one register to the value of another register and save the result in another register, as:
add x0, x1, x2 // Add the values of registers X1 and x2 and save them in register x0
Copy the code
  • Sub: Subtract the value of one register from the value of another register and save the result in another register:
sub x0, x1, x2 // subtract the values of registers X1 and x2 and save them in register x0
Copy the code
  • And: to place the value of one register with the value of another register and save the result to another register, as:
and x0, x0, #0x1 // Save the value of register x0 and constant 1 in register x0 by bit
Copy the code
  • ORR: To place the value of one register with the value of another register or save the result to another register, as:
orr x0, x0, #0x1 // Save the value of register x0 and constant 1 to register x0 by bit or post
Copy the code
  • STR: Writes a value from a register to memory, as:
str x0, [x0, x8] // Save the value in register x0 to stack memory [x0, x8]
Copy the code
  • LDR: Reads a value from memory into a register, as in:
ldr x0, [x1, x2] // Add the value of register X1 and register x2 as the address, take the value of the memory address into register x0
Copy the code
  • CBZ: Compares with 0 and jumps if the result is zero (can only jump to subsequent instructions).
  • CBNZ: Compared to non-zero, if the result is non-zero, it is transferred (only to subsequent instructions).
  • CMP: Comparison instruction.
  • Bl: (branch) Indicates the branch to an address.
  • BLR: Indicates the switch to an address (no return is returned).
  • Ret: The subroutine (function call) returns the instruction, the return address has been saved in register LR (X30) by default.