One, variation method

1.1 Mutating keyword

As we learned in the last article, both classes and structs in Swift can define methods. One difference, however, is that by default, a value type attribute cannot be modified by its own instance method.

  • You can see that the prompt says self can’t be modified, because x and y belong to self, and to modify them is to modify self, modify yourself in your own methods.

The mutating keyword is used to modify the method

Struct Point {var x = 0, y = 0 mutating func moveBy() {x = 10 y = 20}}Copy the code

1.2 sil analysis

Add the following test code to main.swift:

Struct Point {var x = 0.0, y = 0.0 func test() {let TMP = self.x} mutating func moveBy() {x = 10 y = 20}}Copy the code

Generate the main.sil file using the following command

swiftc -emit-sil main.swift > ./main.sil && open main.sil
Copy the code

Get the main. Sil file and compare it with sil. What is the essential difference between not adding mutating and adding mutating

// test function: sil hidden @$s4main5PointV4testyyF: $@convention(method) (Point) -> () {debug_value %0: $Point, let, name "self", argno 1 // id: %1 ... }Copy the code
// moveBy function: sil hidden @$s4main5PointV6moveByyyF: $@convention(method) (@inout Point) -> () {debug_value_addr %0: $*Point, var, name "self", argno 1 // id: %1 ... }Copy the code
  • When comparing the two functions, you can see that the hidden argument of test is Point, and the hidden argument of moveBy is @inout Point@inoutThe official explanation:

    An @inout parameter is indirect. The address must be of An initialized object.

  • The assignment process of the test function can be expressed as:
    let self = Point
    Copy the code
  • The assignment of the moveBy function can be expressed as:
    var self = &Point
    Copy the code
  • The @inout modifier accepts an address and can be modified, otherwise it accepts a value and cannot be modified.

An example can be used:

var point = Point() let p1 = point var p2 = withUnsafePointer(to: &point){$0} point.x = 10 print(p1.x) print(p2.pointe.xCopy the code
  • According to the result, p1 cannot be changed but P2 can be changed.

1.3 Input and output Parameter Inout

Input/output parameters: If we want a function to be able to modify the value of a formal parameter, and we want those changes to persist after the function ends, we need to define formal parameters as input/output formal parameters. Adding an inout keyword at the beginning of a formal parameter definition defines an input/output formal parameter, as illustrated in the following example.

If the parameter is not modified by inout, modifying the parameter will cause an error:

To modify the parameter, decorate it with inout and pass in the address type:

Func changeAge(_ age: inout Int) {age += 10} changeAge(&age) print(age) 0.0 10.0 15Copy the code

Second, function table scheduling

2.1 Compilation Exploration

class LGTeacher { func teach() { print("teach") } func teach1() { print("teach") } func teach2() { print("teach") } } class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() let t = LGTeacher() t.teach() t.teach1() t.teach2() }}Copy the code

  • In the figure abovex8,x9,x9Represent theteach(),teach1(),teach2()
  • readx8, to verify:
    (lldb) register read x8
    x8 = 0x00000001023f22f4  SSLTwoTest`SSLTwoTest.LGTeacher.teach() -> ()
    Copy the code
  • We can also see that function calls are preceded by offset operations[x8, #0x50],[x9, #0x58],[x9, #0x60]

Find Metadata, determine the address of the function (Metadata + offset), and execute the function.

2.2 sil verification

Generate the SIL file using the following command

Swiftc-ema-silgen -onone-target x86_64-apple-ios14.2-simulator -sdk $(xcrun -- show-sdK-path -- SDK iphonesimulator) ViewController.swift > ./ViewController.sil && open ViewController.silCopy the code

In the SIL file, you can see the vtable function table, which lists all the functions in the class

sil_vtable LGTeacher { #LGTeacher.teach: (LGTeacher) -> () -> () : @$s14ViewController9LGTeacherC5teachyyF // LGTeacher.teach() #LGTeacher.teach1: (LGTeacher) -> () -> () : @$s14ViewController9LGTeacherC6teach1yyF // LGTeacher.teach1() #LGTeacher.teach2: (LGTeacher) -> () -> () : @$s14ViewController9LGTeacherC6teach2yyF // LGTeacher.teach2() #LGTeacher.init! allocator: (LGTeacher.Type) -> () -> LGTeacher : @$s14ViewController9LGTeacherCACycfC // LGTeacher.__allocating_init() #LGTeacher.deinit! deallocator: @$s14ViewController9LGTeacherCfD // LGTeacher.__deallocating_deinit }Copy the code

2.3 Source code analysis search V-table

We talked about Metdata’s data structure in the last article, so where is v-table stored? Let’s start with a review of current data structures

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

There’s one thing that we need to focus on here, whether it’s Class, Struct, Enum, has its own Descriptor, which is a detailed description of the Class.

Open the source code and find Description in metadata.h:

TargetSignedPointer<Runtime, TargetClassDescriptor> Description;

using ClassDescriptor = TargetClassDescriptor<InProcess>;
Copy the code

One of its aliases is ClassDescriptor, global search, find the following in genmeta.cpp:

class TypeContextDescriptorBuilderBase
: public ContextDescriptorBuilderBase<Impl> 
{
    ...
    void layout() {
      asImpl().computeIdentity();

      super::layout();
      asImpl().addName();
      asImpl().addAccessFunction();
      asImpl().addReflectionFieldDescriptor();
      asImpl().addLayoutInfo();
      asImpl().addGenericSignature();
      asImpl().maybeAddResilientSuperclass();
      asImpl().maybeAddMetadataInitialization();
    }
}

class ClassContextDescriptorBuilder
    : public TypeContextDescriptorBuilderBase<ClassContextDescriptorBuilder,
                                              ClassDecl>,
      public SILVTableVisitor<ClassContextDescriptorBuilder>
{
      ...
      void layout() {
      assert(!getType()->isForeignReferenceType());
      super::layout();
      addVTable();
      addOverrideTable();
      addObjCResilientClassStubInfo();
      maybeAddCanonicalMetadataPrespecializations();
    }
}
Copy the code

So this code up here is doing some assignment in the creation descriptor, and we also see addVTable(), so let’s see:

void addVTable() { if (IGM.hasResilientMetadata(getType(), ResilienceExpansion::Minimal) && (HasNonoverriddenMethods || ! VTableEntries.empty())) IGM.emitMethodLookupFunction(getType()); If (vtableentries.empty ()) // array 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
  • B is just descriptor, traversal is addedA function pointerAnd the delta functionThe number of.

The TargetClassDescriptor structure is restored as follows:

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

2.4 What is Mach-O

Macho: 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.o,.a. Dylib Framework, dyld.dsym.

Macho file format:

  • The first is the file header, which indicates that the file is in Mach-O format, specifies the target schema, and some other file attribute information that affects subsequent file structure arrangements
  • Load Commands is a table that contains many things. The content includes the location of the region, symbol table, dynamic symbol table, etc
  • The Data section is mainly responsible for code and Data logging. Mach-o organizes data in segments. A Segment can contain zero or more sections. Depending on which Load Command the Segment is mapped to, sections in a Segment can be interpreted as code, constants, or some other data type. When loaded in memory, memory mapping is done based on Segment.

2.5 Mach-O Analyzing V-tables

Section64(_TEXT,__swift5_types) stores Descriptor:

Memory address of Descriptor in Mach-O:

FFFFFBA8 + 0000BB7C = 0x10000B724
Copy the code

0x10000 is the start of the virtual address, and B724 is the offset in The Mach -o Descriptor. The location is as follows:

In the figure above, the red circle is the first address of a Descriptor, followed by the contents of the Descriptor structure. In a Descriptor, there are 13 UInt32, that is, 13 4-bytes.

Move 13 4 bytes to the following location:

B758 is the offset of Teach () in the Mach-O file, B758 + ASLR is the address of Teach (),

Obtain the base address 0x00000001023ec000 of the ASLR program by using the image list command:

So teach() starts with 0x00000001023ec000 + B758 = 0x1023F7758

Find the following structure in the source code, which is the Swift method:

struct TargetMethodDescriptor { /// Flags describing the method. MethodDescriptorFlags Flags; / / / / / 4 bytes The method implementation. / / offset TargetRelativeDirectPointer < Runtime, void > Impl; };Copy the code
  • Calculate Impl address:0x1023F7758 + 4 + FFFFAB98 = 0x2023F22F4
  • So teach() addresses:0x2023F22F40x1000 = 0x1023F22F4

Read the address of teach() :

You can see that the address is 0x1023F22F4 which proves what we said above, that the V-table is after the Descriptor structure.

2.6 Why is the fetch function offset

static void initClassVTable(ClassMetadata *self) { const auto *description = self->getDescription(); auto *classWords = reinterpret_cast<void **>(self); if (description->hasVTable()) { auto *vtable = description->getVTableDescriptor(); auto vtableOffset = vtable->getVTableOffset(description); auto descriptors = description->getMethodDescriptors(); for (unsigned i = 0, e = vtable->VTableSize; i < e; ++i) { auto &methodDescription = descriptors[i]; swift_ptrauth_init_code_or_data( &classWords[vtableOffset + i], methodDescription.Impl.get(), methodDescription.Flags.getExtraDiscriminator(), ! methodDescription.Flags.isAsync()); }}... }Copy the code

As you can see, vtableOffset is offset when the function is stored in VTable, so it needs to be offset when it is fetched.

Other function scheduling methods

3.1 Struct function scheduling

Replace class with struct and debug assembly again:

  • You can see the struct function call, which is the direct address call, which isStatic distributed.

3.2 Struct extension function scheduling

Add extension to SSLTeacher:

extension SSLTeacher {
    func teach3() {
        print("teach3")
    }
}
Copy the code

Assembly debugging:

  • Teach3 is also a direct address call, as is Struct extensionStatic distributed.

3.3 Class extension function scheduling

Change struct back to class:

class SSLTeacher {
    func teach() {
        print("teach")
    }
    func teach1() {
        print("teach1")
    }
    func teach2() {
        print("teach2")
    }
}
Copy the code

Assembly debugging:

  • As you can see, class extension is the sameStatic distributed.

3.4 Summary of method scheduling methods

4. The influence of keywords on distribution methods

4.1 the final

Functions that add the final keyword cannot be overridden, are statically distributed, do not appear in the Vtable, and are not visible to the OBJC runtime.

class SSLTeacher {
    final  func teach() {
        print("teach")
    }
    ...
}
Copy the code

4.2 the 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.

class SSLTeacher { dynamic func teach() { print("teach") } } extension SSLTeacher { @_dynamicReplacement(for: Teach) func teach3() {print("teach3")}} let t = SSLTeacher() t.each (Copy the code

4.3 @ objc

This keyword exposes the Swift function to the Objc runtime, which is still distributed from the function table.

class SSLTeacher: NSObject @objc func teach() {print("teach")} func teach1() {print("teach1")} func teach2() { print("teach2") } } extension SSLTeacher { @objc func teach3() { print("teach3") } }Copy the code

To view:

4.4 @ objc + dynamic

With the @objc + dynamic modifier, we can use the Runtime API

@objc dynamic func teach() {print("teach")} func teach1() {print("teach1")} func teach2() { print("teach2") } } extension SSLTeacher { @objc dynamic func teach3() { print("teach3") } }Copy the code

5. Function inlining

Function inlining is a compiler optimization technique that optimizes performance by calling a method directly using its content substitution.

5.1 OC Project Optimization Example

To create an OC project, add the following code:

int sum(int a, int b) {
    return a + b;
}

int main(int argc, char * argv[]) {
    
    int a = sum(1, 2);
    NSLog(@"%d",a);
    return 0;
}
Copy the code

No optimization

You can configure the optimization level as follows:

Assembly debugging:

  • You can see that without optimization, the values of 1 and 2 are first stored in w0 and w1, respectively
  • Sum is then called.

Fastest minimum optimization

Next select the fastest minimum optimization:

Assembly debugging again:

You can see the optimized code, which stores the result 3 directly into register W8 and then calls the NSLog function.

5.2 Inlining in Swift

Optimized Settings:

Inline operations

  • Inline functions in Swift are the default behavior and we don’t need to do anything. The Swift compiler may automatically inline functions as optimizations.
    Print func test() {print("test"); }Copy the code
  • Always – Will ensure that functions are always inlined. This behavior is implemented by prefixing the function with @inline(__always)
  • Never – Will ensure that functions are never inlined. This can be done by prefixing the function with @inline(never).
  • If the function is long and you want to avoid increasing the code segment size, use @inline(never).

5.3 Optimization of private

If the object is visible only in the declared file, you can decorate it with private or Fileprivate. The compiler checks the private or Fileprivate objects to make sure that there are no other inheritance relationships, and then automatically marks them as final, so that the objects get statically distributed features. (Fileprivate: only accessible in defined source files, private: Definition of access in the declaration)

Add code:

class SSLPerson { private var sex: Bool func unpdateSex() { self.sex = ! self.sex } init(sex innerSex: Bool) { self.sex = innerSex } func test() { self.unpdateSex() } } let t = SSLPerson(sex: true) t.test()Copy the code

You can see that the call to unpdateSex is a direct address call, without using a function table.