美文网首页
Swift -- 2.类与结构体(下)

Swift -- 2.类与结构体(下)

作者: MissStitch丶 | 来源:发表于2022-01-07 01:29 被阅读0次

一.异变方法

1.值类型方法

Swift中的class和struct都能定义方法。但是有一点区别的是默认情况下,值类型的属性不能被自身的实例方法修改。

struct Point {
    var x = 0.0, y = 0.0
    func moveBy(x deltaX: Double, y deltaY: Double) {
        x += deltaX
        y += deltaY
    }
}

代码会报错,因为在moveBy方法内修改x或y值相当于修改其本身(self)

要想使得值类型在实例方法里修改其属性,需要加上关键字mutating

struct Point {
    var x = 0.0, y = 0.0
    mutating func moveBy(x deltaX: Double, y deltaY: Double) {
        x += deltaX
        y += deltaY
    }
}

2.分析mutating关键字

这里给Point添加了一个普通方法test

struct Point {
    var x = 0.0, y = 0.0
    
    func test(){
        let tmp = self.x
    }
    
    mutating func moveBy(x deltaX: Double, y deltaY: Double) {
        x += deltaX
        y += deltaY
    }
}

生成SIL文档

// Point.test()
sil hidden @$s4main5PointV4testyyF : $@convention(method) (Point) -> () {
// %0 "self"                                      // users: %2, %1
bb0(%0 : $Point):
  debug_value %0 : $Point, let, name "self", argno 1 // id: %1
  %2 = struct_extract %0 : $Point, #Point.x       // user: %3
  debug_value %2 : $Double, let, name "tmp"       // id: %3
  %4 = tuple ()                                   // user: %5
  return %4 : $()                                 // id: %5
} // end sil function '$s4main5PointV4testyyF'
  • 函数默认参数类型为Point,也就是self。(OC中函数默认的参数,self、_cmd
  • debug_value %0 : $Point, let, name "self", argno 1,相当于let self = Point
// Point.moveBy(x:y:)
sil hidden @$s4main5PointV6moveBy1x1yySd_SdtF : $@convention(method) (Double, Double, @inout Point) -> () {
// %0 "deltaX"                                    // users: %10, %3
// %1 "deltaY"                                    // users: %20, %4
// %2 "self"                                      // users: %16, %6, %5
bb0(%0 : $Double, %1 : $Double, %2 : $*Point):
  //声明一个deltaX赋值给%0
  debug_value %0 : $Double, let, name "deltaX", argno 1 // id: %3
  //声明一个deltaY赋值给%1
  debug_value %1 : $Double, let, name "deltaY", argno 2 // id: %4
  debug_value_addr %2 : $*Point, var, name "self", argno 3 // id: %5
  debug_value_addr %2 : $*Point, var, name "self", argno 3 // id: %5
  ...
} // end sil function '$s4main5PointV6moveBy1x1yySd_SdtF'
  • 加上mutating默认传入的参数变为@inout Point
  • debug_value_addr %2 : $*Point, var, name "self", argno 3,相当于var self = &Point

SIL文档对@inout的解释
An @inout parameter is indirect. The address must be of an initialized object.(当前参数类型是间接的,传递的是已经初始化过的地址)

异变方法的本质:对于变异方法,传入的self被标记为inout参数。无论在mutating方法内部发生什么,都会影响外部依赖类型的一切。也就是说,mutating标记的方法,可以修改本身的值

输入输出参数:如果我们想函数能够修改一个形式参数的值,并且希望这些改变在函数结束之后依然生效,那么久需要将形式参数定义为输入输出形式参数。在形式参数定义开始的时候在前边添加一个inout关键字可以定义一个输入输出形式参数

var age = 10

//函数的形式参数都是let类型的
func modifyAge(_ age: inout Int) {
    age += 1
}

modifyAge(&age) //传入的是age地址

print(age) // 11

3.代码案例来理解mutating

struct Point {
    var x = 0.0, y = 0.0
    
    func test(){
        let tmp = self.x
    }
    
    mutating func moveBy(x deltaX: Double, y deltaY: Double) {
        x += deltaX
        y += deltaY
    }
}

var p = Point()

//相当于test方法传入的p, let self = Point
let x1 = p

//相当于moveBy方法传入的&p, var self = &Point
var x2 = withUnsafePointer(to: &p){return $0}

var x3 = p // 相当于值拷贝,2个独立的结构体实例

p.x = 30.0
//x2和x3的x值会发生变化吗?
//x2的会发生变化,x3的值不会发生变化
//x2和p是2个一模一样的实例,指向同一一块内存空间

print(x2.pointee.x) // 30.0
print(x3.x) // 0

对应SIL

debug_value_addr %0 : $*Int, var, name "age", argno 1 // id: %1
相当于 var age = &age

二.方法调度

对于Objective-C来说通过objc_msgSend进行方法调度
那么,在Swift中的方法调度是怎么样的呢

1.汇编分析

class LGTeacher{
    func teach(){
        print("teach")
        
    }
}

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        
        var t = LGTeacher()
        t.teach()
    }
}

创建项目,使用真机调试,查看汇编来分析方法调度
t.teach()打上断点,进入汇编调试模式

我们知道在汇编代码中blr代表着有返回值的跳转,将断点移到blr x8表示跳转到x8寄存器中的地址

LGTeacher.teach

进来后发现,这里面就是方法teach的调用
此时,虽然找到了teach的函数调用,那么这和方法调度有什么关系呢?我们继续往下

在Teacher中再添加2个teach方法

class LGTeacher{
    func teach(){
        print("teach")
    }
    
    func teach1(){
        print("teach1")
    }
    
    func teach2(){
        print("teach2")
    }
}

class ViewController: UIViewController {

    override func viewDidLoad() {
        var t = LGTeacher()
        t.teach()
        t.teach1()
        t.teach2()
    }
}

teach函数的调用过程:找到metadata,确定函数地址(metadata + 偏移量),执行函数。
teach、teach1、teach2相差的就是函数指针的大小,在内存地址上是连续的内存空间

一些汇编指令
bl:(branch)跳转到某地址(无返回)
blr:跳转到某地址(有返回)

mov:将某一寄存器的值复制到另一寄存器(只能用于寄存器与寄存器或者寄存器于常量之间传值,不能用于内存地址):
mov x1, x0  将寄存器x0的值复制到寄存器x1中

add: 将某一寄存器的值和另一寄存器的值 相加 并将结果保存在另一寄存器中:
add x0, x1, x2   将寄存器x1和x2的值相加后保存到寄存器x0中

sub: 将某一寄存器的值和另一寄存器的值 相减 并将结果保存在另一寄存器中:
sub x0, x1, x2   将寄存器x1和x2的值相减后保存到寄存器x0中

and:将某一寄存器的值和另一个寄存器的值 按位与 并将结果保存到另一寄存器中:
and x0, x0, #0x1   将寄存器x0的值和常量1按位与后保存到寄存器x0中
位与符号是&,真值表达式为: 1&1=1,1&0=0,0&1=0,0&0=0

orr:将某一寄存器的值和另一寄存器的值 按位或 并将结果保存到另一寄存器中:
orr x0, x0, #0x1   将寄存器x0的值和常量1按位或后保存到寄存器x0中
位或符号是|,真值表达式为: 1|1=1,1|0=1,0|1=1,0|0=0

str:将寄存器中的值写入到内存中:
str x0, [x0, x8]    将寄存器x0的值保存到栈内存[x0 + x8]处

ldr:将内存中的值读取到寄存器中:
ldr x0, [x1, x2]    将寄存器x1和寄存器x2的值相加作为地址,取该内存地址的值放入寄存器x0中

cbz: 和 0 比较,如果结果为零就转移(只能跳到后面的指令)

cbnz: 和非 0 比较,如果结果非零就转移(只能跳到后面的指令)

cmp: 比较指令

ret: 子程序(函数调用)返回指令,返回地址已默认保存在寄存器 lr (x30) 中

2.SIL上查看sil_vtable

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
}

3.源码找到vtable

类与结构体(上)总结的Metadata中有一个重要的字段typeDescriptor,类型描述

//TargetClassMetadata中
TargetSignedPointer<Runtime, const TargetClassDescriptor<Runtime> * __ptrauth_swift_type_descriptor> Description;
//进入TargetClassDescriptor,发现它有一个别名,全局搜索ClassDescriptor查找在哪里构建这个结构体
using ClassDescriptor = TargetClassDescriptor<InProcess>;
//进入GenMeta.cpp,找到ClassContentDescriptorBuilder
void layout() {
   super::layout();
   addVTable();
   addOverrideTable();
   addObjCResilientClassStubInfo();
   maybeAddCanonicalMetadataPrespecializations();
 }

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

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

//addVTable()
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());

  //VTableEntries可以理解为数组
  if (VTableEntries.empty()) 
    return;
  
  //计算了一个偏移量
  auto offset = MetadataLayout->hasResilientSuperclass()
                  ? MetadataLayout->getRelativeVTableOffset()
                  : MetadataLayout->getStaticVTableOffset();
  //B就是TargetClassDescriptor
  //添加偏移量
  B.addInt32(offset / IGM.getPointerSize());
  //添加vtable的size
  B.addInt32(VTableEntries.size());
  //遍历数组,添加函数指针
  for (auto fn : VTableEntries)
    emitMethodDescriptor(fn);
}
//通过TargetClassDescriptor、TargetTypeContextDescriptor、TargetContextDescriptor的源码总结出的TargetClassDescriptor
//TargetClassDescriptor继承自TargetTypeContextDescriptor
//TargetTypeContextDescriptor继承自TargetContextDescriptor

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

    //对应上面添加的size =》B.addInt32(VTableEntries.size());
    var size: UInt32

    //V-Table
}

4.Mach-O验证TargetClassDescriptor

Mach-O:其实是Mach Object文件格式的缩写,是mac以及iOS上可执行文件的格式,类似于Windows上的PE(Portable Executable)Linux上的elf格式(Executable and Linking Format)。常见的.o.a.dylib Frameworkdylib.dsym

Mach-O文件格式:


Mach-O
  • 首先是文件头,表明该文件是Mach-O格式,制定目标架构,还有一些其他的文件属性信息,文件头信息影响后续文件的文件结构
  • Load commands是一张包含很多内容的表。内容包括区域的位置、符号表、动态表等。
name info
LC_SEGMENT_64 将文件中(32位或64位)的段映射到进程地 址空间中
LC_DYLD_INFO_ONLY 动态链接相关信息
LC_SYMTAB 符号地址
LC_DYSYMTAB 动态符号表地址
LC_LOAD_DYLINKER dyld加载
LC_UUID 文件的UUID
LC_VERSION_MIN_MACOSX 支持最低的操作系统版本
LC_SOURCE_VERSION 源代码版本
LC_MAIN 设置程序主线程的入口地址和栈大小
LC_LOAD_DYLIB 依赖库的路径,包含三方库
LC_FUNCTION_STARTS 函数起始地址表
LC_CODE_SIGNATURE 代码签名
  • Data区主要就是负责代码和数据记录的。Mach-O是以Segment这种结构来组织数据的,一个Segment可以包含0个或多个Section。根据Segment是映射的哪一个Load Command,Segment中section就可以被解读为是代码、常量或者一些其他的数据类型。在装载在内存中时,也是根据Segment做内存映射的
Mach64 Header.png Load Commands
  • __PAGEZERO主要是将低地址占用,防止用户访问。这里的VM Size就是Mach-o的基地址
  • 程序在运行时,会加上ASLR(地址空间布局随机化),来保证程序运行的安全
Memory Data
  • OC的类存放在Section64(_DATA_CONST,__objc_classlist)
  • Swift的类、结构体、枚举存放在Section64(__TEXT,__swift5_types),每4字节作为一个区分,具体存放的是TargetClassDescriptor地址信息。

在这里第一个4字节就是我们今天要验证的LGTeacherTargetClassDescriptor

swift_types
这里的第一个4字节,小端模式,读取为0xFFFFFBA8
0xFFFFFBA8   +  0xBBCC  = 0x10000B774(Descriptor在Mach-O文件的内存地址)

减去虚拟内存基地址(VM Address): 0x100000000
得出地址为:0xB774(Descriptor在Data数据区的内存地址)

找到0xB774,熟悉Mach-O的应该知道,这肯定在_TEXT_const

0xB774
  • 这个地方就是Descriptor的内容,也就是起始位置
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

    //对应上面添加的size =》B.addInt32(VTableEntries.size());
    var size: UInt32

    //V-Table
}

这个是我们之前查看源码总结的TargetClassDescriptor数据结构,根据该数据结构,在0xB774进行偏移得到vtable的内存地址,偏移12个4字节

vtable起始位置
  • size为0x00000004
  • 0xB7A8就是teachMach-O中的信息

验证这个0xFFFFC04400000010就是我们的teach信息

1.使用image list找到ASLR(地址空间布局随机性,Address Space Layout Randomization),也就是程序在启动的时候随机偏移了一个地址,也称为程序的基地址。

(lldb) image list
[  0] 12C1648A-0E28-3BB8-A1F1-CCD13EDCBE38 0x0000000104a68000 /Users/zt/Library/Developer/Xcode/DerivedData/projectTest-bmemtiaawffbzbcslwyavvzqqwmh/Build/Products/Debug-iphoneos/projectTest.app/projectTest 
  • 0x0000000104a68000就是程序运行的基地址
  1. 0x0000000104a68000 + 0xB7A8(偏移量) = 0x104A737A8 就是函数在内存的地址
    此时0x104A737A8指向的就是Mach-O中的0xFFFFC04400000010,也就是上图中的vtable起始位置

3.源码查看swift函数在内存的数据结构

struct TargetMethodDescriptor {
  /// Flags describing the method.
  MethodDescriptorFlags Flags; //标识方法的类型,4字节

  /// The method implementation.
  TargetRelativeDirectPointer<Runtime, void> Impl; //imp的指针,存储的offset

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

此时算出的0x104DA37A8就是指向的teach(TargetMethodDescriptor)

偏移Flags(4字节),加上offset(0xFFFFC044)
0x104A737A8 + 0x4 + 0xFFFFC044  = 0x204A6F7F0
减去虚拟基地址0x100000000(VM Address)
得到0x104A6F7F0

4.汇编验证函数地址
在执行到第一个x8寄存器时,读取x8的值

(lldb) register read x8
      x8 = 0x0000000104a6f7f0  projectTest`projectTest.LGTeacher.teach() -> () at ViewController.swift:11

5.至此证明了TargetClassDescriptor数据结构

Swift代码

import UIKit

class LGTeacher{
    func teach(){
        print("teach")
    }
    
    func teach1(){
        print("teach1")
    }
    
    func teach2(){
        print("teach2")
    }
}

class ViewController: UIViewController {

    //ASLR:地址空间分布随机性(offset)
    
    //使用image list获取程序基地址(ASLR offset + __PAGEZERO) 0x0000000104a68000
    //0x0000000104a68000 + 0xB7A8 = 0x104A737A8
    
    //加上偏移0x4,加上offset, 减去程序的基地址0x100000000得到方法函数的内存地址
    //0x104A737A8 + 0x4 + 0xFFFFC044 - 0x100000000 = 0x104A6F7F0
    
    /*
     (lldb) register read x8
           x8 = 0x0000000104a6f7f0  projectTest`projectTest.LGTeacher.teach() -> () at ViewController.swift:11
     */
    
    override func viewDidLoad() {
        let t = LGTeacher()
        t.teach()
        t.teach1()
        t.teach2()
    }
}

三.影响函数派发方式

1.结构体派发

struct LGTeacher{
    func teach(){
        print("teach")
    }
    
    func teach1(){
        print("teach1")
    }
    
    func teach2(){
        print("teach2")
    }
}

class ViewController: UIViewController {

    override func viewDidLoad() {
        let t = LGTeacher()
        t.teach()
        t.teach1()
        t.teach2()
    }
}
  • 可以看到,这里的3个方法在汇编代码中是直接派发的。代码编译后,函数地址就确定了。

  • 为什么?
    值类型没有继承关系,因此也就不需要记录我们的函数,编译器就优化为了静态调用,类似于静态函数

  • 源码查看Struct是否存在vtable

进入GenMeta.cpp,找到StructContextDescriptorBuilder(与ClassContextDescriptorBuilder对应)

//laout
void layout() {
  super::layout();
  maybeAddCanonicalMetadataPrespecializations();
}

void addLayoutInfo() {
  // uint32_t NumFields;
  B.addInt32(getNumFields(getType()));

  // uint32_t FieldOffsetVectorOffset;
  B.addInt32(FieldVectorOffset / IGM.getPointerSize());
}

并没有vtable的相关方法

2.使用extension添加的方法

1.使用extension给Struct添加一个方法

extension LGTeacher {
    func teach3() {
        print("teach3")
    }
}

  Struct通过extension添加的方法是静态派发

2.使用extension给Class添加一个方法

image.png

  类通过extension添加的方法也是静态派发

3.理解为什么类使用extension不写入函数表里面

vtable示意图

  这里Teacher有2个函数teachteach1,LGPartTeacher有4个函数(继承了2个函数teachteach1和自身的teach2teach3)。假如这2个类在A.swift文件中,在B.swift文件中给LGTeacher添加teach4函数。当程序在编译A.swift时,编译了这2个类,此时vtable就生成了。此时程序再编译到B.swift时,LGTeacher中有一个extension,如果要在vtable添加的话,就应该在teach1下追加一个teach4。对于LGPartTeacher来说就应该加载teach1下面,但是LGPartTeacher已经确定了,再在中间插入一个MethodDescriptor,需要移动之前的teach2teach3指针,还需要有一个下标来记录位移的index来保证extension添加的方法能够找到位置插入,此番操作代价太昂贵,因此编译器将extension添加的方法优化为静态调用不会影响原有的vtable结构

3.方法调度的总结

类型 调用方式 extension
值类型 静态派发 静态派发
Swift类 函数表派发 静态派发
NSObject类 函数表派发 静态派发

4.影响函数派发方式

  • staticprivatefileprivate添加后,会变成静态派发
  • final添加了final关键字的函数无法被重写,使用静态派发,不会在vtable中出现,且对objc运行时不可见
//实际开发过程中属性、方法、类不需要被重载的时候,使用final
class LGTeacher{
    //写上了final关键字,静态派发
    final func teach(){
        print("teach")
    }
}

let t = LGTeacher()
t.teach()

->  0x102bf7bd8 <+56>:  bl     0x102bf771c               ; projectTest.LGTeacher.teach() -> () at ViewController.swift:12
  • dynamic函数均可添加dynamic关键字,为非objc类和值类型的函数赋予动态性,但派发方式还是函数表派发。也就是可以动态的替换,使用了该字段才能使用编译器字段@_dynamicReplecement(for:xx)来替换方法的imp。注意,@_dynamicReplecement只能在extension里使用
class LGTeacher{
    //依然还是函数表的调度
    dynamic func teach(){
        print("teach")
    }
}

extension LGTeacher {
    //将teach的imp改为了tech3
    //具体可以查看SIL代码。实际上是保留了2个代码的imp,一个orignal_entry,一个forward_to_replace
    @_dynamicReplacement(for: teach)
    func teach3() {
        print("teach3")
    }
}

let t = LGTeacher()
t.teach()

执行结果为teach3
  • @objc该关键字可以将Swift函数暴露给Objc运行时,依旧是函数表派发。

  • @objc + dynamic消息派发的方式,objc_msgSend。能够使用Runtime Api来动态修改。

class LGTeacher {

    //@objc,将方法暴露给OC,函数表派发
    //dynamic,方法具有动态性,函数表派发
    //二者结合过后变为,消息派发(objc_msgSend)

    //加上@objc + dynamic变为消息调度的机制,objc_msgSend
    //此时就可以使用Runtime Api,method-swizzling、...
    @objc dynamic func teach(){
        print("teach")
    }
  
}

此时此刻,对于这个teach函数OC来说,能调用得到吗?
答案肯定是调用不到的,因为它是一个纯粹的Swift类。我们也可以在Xcode中查看xxxx-swift.h(Swift暴露给OC的声明文件)中是否有这个类。

进入发现没有LGTeacher类的相关信息。但是可以在Swift代码中对@objc dynamic标记的函数使用Runtime Api

比如当前类使用method-swizzling

class LGTeacher {
    
    //加上@objc变为消息调度的机制,objc_msgSend
    //此时就可以使用Runtime Api,method-swizzling、...
    @objc dynamic func teach(){
        print("teach")
    }
    
    @objc dynamic func teach1(){
        print("teach1")
    }
    
    func teach2(){
        print("teach2")
    }
}

extension LGTeacher {
    //将teach的imp改为了tech3
    //具体可以查看SIL代码。实际上是保留了2个代码的imp,一个orignal_entry,一个forward_to_replace
    @_dynamicReplacement(for: teach)
    func teach3() {
        print("teach3")
    }
}

class ViewController: UIViewController {

    override func viewDidLoad() {
        
        let teachSel = #selector(LGTeacher.teach)
        let teach1Sel = #selector(LGTeacher.teach1)
        
//        let teachImp = class_getMethodImplementation(LGTeacher.self, teachSel)
//        let teach1Imp = class_getMethodImplementation(LGTeacher.self, teach1Sel)
//
//        //替换方法
//        class_replaceMethod(LGTeacher.self, teachSel, teach1Imp!, nil)
//        class_replaceMethod(LGTeacher.self, teach1Sel, teachImp!, nil)
        
        let teachMethod = class_getInstanceMethod(LGTeacher.self, teachSel)
        let teach1Method = class_getInstanceMethod(LGTeacher.self, teach1Sel)
        
        method_exchangeImplementations(teachMethod!, teach1Method!)
        
        let t = LGTeacher()
        t.teach()
        t.teach1()
        
//        执行结果
//        teach1
//        teach3
    }
}

如果想让OC使用到这个类,必须让这个类继承自NSObject

此时在xxx-swift.h中就有了相关的声明

SWIFT_CLASS("_TtC11projectTest9LGTeacher")
@interface LGTeacher : NSObject
- (void)teach;
- (void)teach1;
- (nonnull instancetype)init OBJC_DESIGNATED_INITIALIZER;
@end

四.函数内联

函数内联是一种编译器优化技术,它通过使用方法的内容替换直接调用该方法,从而优化性能。

  • 将确保有时内联函数。这是默认行为,我们无需执行任何操作,Swift编译器可能会自动内联函数作为优化
  • always将确保始终内联函数。通过在函数前添加@inline(__always)来实现此行为
  • never将确保永远不会内联函数。这可以通过在函数前添加@inline(never)来实现
  • 如果函数很长并且想避免增加代码段大小,请使用@inline(never)
//始终内联函数
@inline(__always) func test() {
    print("test")
}

//永远不内联函数
@inline(never) func test1() {
    print("test1")
}
工程中优化选项

1.分析代码在不同优化等级下汇编代码

class ViewController: UIViewController {

    override func viewDidLoad() {

        let a = sum(a: 1, b: 2)
    
    }
    
}

func sum(a:Int, b:Int) -> Int {
    return a + b
}
  • 默认Not Optimization
    Not Optimization
0x102ce3a9c <+428>: mov    w8, #0x1   将1复制到w8寄存器
0x102ce3aa4 <+436>: mov    w8, #0x2   将2复制到w8寄存器
0x102ce3aac <+444>: bl     0x102ce3af0               ; 
projectTest.sum(a: Swift.Int, b: Swift.Int) -> Swift.Int at ViewController.swift:90  执行sum函数
  • 修改Optimization LevelOptimize for Speed
    此时细心的你就会发现如果还是将断点断在let a = sum(a: 1, b: 2)下一行的话,此时不会进入断点。因为编译已经优化掉了,此时的let a = sum(a: 1, b: 2)其实就是3。因此,加上打印语句,断点下载print
class ViewController: UIViewController {

    override func viewDidLoad() {

        let a = sum(a: 1, b: 2)
        print(a)
    }
    
}

func sum(a:Int, b:Int) -> Int {
    return a + b
}

Optimize for speed
0x102bfd4e4 <+188>: mov    w8, #0x3   //将3复制到w8寄存器中
此时,在汇编里面已经没有关于sum函数调用了。编译器直接将1+2=3计算出来了

这样的行为就称为编译器优化技术

2.private、fileprivate对函数派发的影响

  • 如果对象只在声明的文件中可见,可以用privatefileprivate进行修饰。编译器会对privatefileprivate对象进行检查,确保没有其他继承关系的情形下,自动打上final标记,进而使得对象获得静态派发的特性(fileprivate只允许在定义的源文件中访问,private定义的声明 中访问)
class LGPerson{
    
    private var sex: Bool
    
    private func unpdateSex(){
        self.sex = !self.sex
    }
    
    init(sex innerSex: Bool) {
        self.sex = innerSex
    }
    
    func test() {
        self.unpdateSex()
    }
}

class ViewController: UIViewController {

    override func viewDidLoad() {

        let t = LGPerson(sex: true)
        t.test()
    }
    
}

在函数test中的self.unpdateSex()下一个断点

private_静态派发
  • 此时可以发现函数unpdateSex静态派发了,那是否vtable里没有这个函数了?
此时我们发现vtable里依然有unpdateSex,只是编译器针对private将函数调用方式优化了

sil_vtable LGPerson {
  #LGPerson.sex!getter: (LGPerson) -> () -> Bool : @$s14ViewController8LGPersonC3sex33_37ACD668159BB52851391EE68C0B8918LLSbvg   // LGPerson.sex.getter
  #LGPerson.sex!setter: (LGPerson) -> (Bool) -> () : @$s14ViewController8LGPersonC3sex33_37ACD668159BB52851391EE68C0B8918LLSbvs // LGPerson.sex.setter
  #LGPerson.sex!modify: (LGPerson) -> () -> () : @$s14ViewController8LGPersonC3sex33_37ACD668159BB52851391EE68C0B8918LLSbvM // LGPerson.sex.modify
  #LGPerson.unpdateSex: (LGPerson) -> () -> () : @$s14ViewController8LGPersonC10unpdateSex33_37ACD668159BB52851391EE68C0B8918LLyyF  // LGPerson.unpdateSex()
  #LGPerson.init!allocator: (LGPerson.Type) -> (Bool) -> LGPerson : @$s14ViewController8LGPersonC3sexACSb_tcfC  // LGPerson.__allocating_init(sex:)
  #LGPerson.test: (LGPerson) -> () -> () : @$s14ViewController8LGPersonC4testyyF    // LGPerson.test()
  #LGPerson.deinit!deallocator: @$s14ViewController8LGPersonCfD // LGPerson.__deallocating_deinit
}

此时将函数unpdateSex声明的private去掉,再次运行

去掉private_函数表派发
  • 函数unpdateSex调度方式为函数表派发

相关文章

  • Swift -- 2.类与结构体(下)

    一.异变方法 1.值类型方法 Swift中的class和struct都能定义方法。但是有一点区别的是默认情况下,值...

  • swift从零开始-10-类与结构体

    1. 类和结构体对比swift中类与结构体的共同处: 1.定义属性用于存储值 2.定义方法用于提供功能 3.定义下...

  • 第九章 类和结构体

    c++中,结构体是稍有不同的类,类能做的,结构体也可以; 而swift中,结构体与类有较大区别, 结构体与类的区别...

  • Swift - 学习

    1.类和结构体的区别 Swift中结构体和类的比较 2.写时拷贝机制 Swift Copy-On-Write 写时...

  • Swift类与结构体(下)

    一.异变方法 Swift中class和struct都能定义方法,但在默认情况下,值类型(struct)属性不...

  • Swift - 类与结构体(下)

    1. struct结构体方法调用探究 探究方法调用的过程,我们知道在 OC 中,调用一个方法的本质是消息传递,底层...

  • Swift基础5(结构体和类)

    类与结构体的对比 在 Swift 中类和结构体有很多共同之处 定义属性用来存储值; 定义方法用于提供功能; 定义下...

  • Swift结构体内存初探之写时复制

    Swift赋予了结构体很多余类相同的特性,以至于Swift推荐在程序中使用结构体来存储结构化数据(关于类与...

  • iOS知识点-8.类(class)和结构体(struct)有什么

    Swift Basics 类(class)和结构体(struct)有什么区别? Swift中,类是引用类型,结构体...

  • Swift初探(二)

    继Swift初探之后,我们来继续学习下Swift里的结构体,类,协议,闭包 结构体 两种调用结构体的方法1.调用结...

网友评论

      本文标题:Swift -- 2.类与结构体(下)

      本文链接:https://www.haomeiwen.com/subject/uhfhcrtx.html