swift-类与结构体(下)

264 阅读7分钟

写在前面: swift-类与结构体(上) 为关于初始化、类型上的区别。 本次讲解方法上的区别。

一、异变方法

  • 默认值类型的属性不能被自身的实例方法修改。

image.png 如图示代码报错left side of mutating operator isn't mutable: 'self' is immutable,说明self是不可更改的。 在moveBy修改x、y时,self是结构体,值类型,存放两个属性的值。 self指代x和y。

image.png 如图示,内部函数的修改影响外部,所以:默认值类型的属性不能被自身的实例方法修改。

  • 如果需要被自身修改,在方法添加mutaing关键字。

image.png

通过函数是否用mutaing修饰,生成SIL语句,对比区别。
如图为定义的区别 image.png

继续找到test函数

  • @$s4main5PointV4testyyF:命名重整之后的名称
  • Point类型的默认参数,即self

image.png

找到moveBy函数

  • s4main5PointV6moveBy1x1yySd_SdtF:命名重整之后的名称
  • 参数x
  • 参数y
  • Point 即self(多了个inout关键字)

image.png

sil hidden @$s4main5PointV4testyyF : $@convention(method) (Point) -> () {
debug_value %0 : $Point, let, name "self", argno 1 // id: %1
sil hidden @$s4main5PointV6moveBy1x1yySd_SdtF : $@convention(method) (Double, Double, @inout Point) -> () {
@inout Point
debug_value_addr %2 : $*Point, var, name "self", argno 3 // id: %5

var self = &Point //取地址  添加mutaing
let self = Point //当前的值 没添加mutaing

@inout 解释:当前参数类型时间接的,传递的是已经初始化过的地址。

  • 异变方法的本质: 对于变异方法, 传入的 self 被标记为 inout 参数。无论在 mutating 方法 内部发生什么,都会影响外部依赖类型的一切。

二、方法调度

oc通过objc-mgsend消息机制来调度方法。

  • swift机制简介
class LGTeacher { 
    func teach() {
        print("teach")
    }
}
class ViewController: UIViewController{
    override func viewDidLoad() {
        t.teach1()
    }
}

再通过Debug->Debug Workfolw->Always Show Disassembly开启 来查看代码对应的汇编代码。\

  • 常见指令的总结:

常见指令.png 常见指令-2(拖移项目).png

image.png

再执行单步执行,进入到teach函数调用页面。

image.png

接下来,把函数扩充,分别执行,再次查看汇编代码查看bl和blr。

image.png

分别对应方法1、方法2、方法3

  • 其中x8含义是寄存器 ,其值是 通过mov指令,把x0的值赋值到x8寄存器里。
  • 函数的返回值放在x0寄存器里,x0存放实例对象;
  • 取x0实例对象的前八个字节,放在x8寄存器里;
  • x0的第一个8字节:metdata,通过命令行读取得到
    register read x8
    x8 = 0x0000000000000003
  • metadataAdress + 0x50 赋值给x8寄存器

-- 总结 --
teach函数的调用过程:

  1. 找到 Metadata
  2. 确定函数地址(metadata + 偏移量)
  3. 执行函数

image.png

第一种调度方式:基于函数表的调度
  • 编译成SIL文件

image.png 如图示 sil_vtable为函数表,罗列了当前类所有的函数。 回顾metadata的数据结构:

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
}

需要关注 typeDescriptor ,不管是 ClassStruct , Enum 都有自己 的 Descriptor ,就是对类的一个详细描述:

回到swift-source 打开 metadata.h 文件,

image.png

image.png 点击进去 ,通过继承关系,把结构体还原出来:

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
}

-> 全区文件搜:TargetClassDescriptor(ClassDescriptor) -> 找到相关文件。

image.png

  • 点击进入GenMeta.cpp,找到类ClassContextDescriptorBulder image.png
  • 找到layout 布局方法

image.png

image.png

  • 相比类似 ,说明layout在创建descriptor
    -> 有addVTable,进入方法:

image.png

  • 判断为空-> 优化判断 -> 计算偏移 -> 添加到B -> add vtable的size-> 遍历数组 -> 添加当前数组(函数)的指针
  • 即offset完成之后 -> size -> method
引入mach-O文件

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

  • 验证数据结构形式 通过macho软件打开: image.png

  • 首先是文件头,表明该文件是 Mach-O格式,指定目标架构,还有一些其他的文件属性信息,文件头信息影响后续的文件结构安排包含二进制信息: image.png

  • Load commands是一张包含很多内容的表。内容包括区域的位置、符号表、动态符号表 等。 image.png

  • Data 区: 主要就是负责代码和数据记录的。Mach-O 是以 Segment 这种结构来组织数据 的,一个 Segment 可以包含 0 个或多个 Section。根据 Segment 是映射的哪一个 Load Command,Segment 中 section 就可以被解读为是是代码,常量或者一些其他的数据类 型。在装载在内存中时,也是根据 Segment 做内存映射的。

image.png

  • Mach-O 分析V-Table Section64(_TEXT,__swift5_types) 中存放的就是 Descriptor地址信息;\

image.png 如上图,验证计算 Descriptor 在 Mach-O 的内存地址:
0xFFFFFBF4 + 0xBC68 = 0x10000B85C 0x10000开头,在mach-o文件开头叫做虚拟内存的地址。 在虚拟内存的地址,位置如下图:

image.png B85C的位置在如下红箭头所指: image.png 之后的数据结构和targetClassDescriptor结构体一一对应。 验证:往后数12个4字节,如下图所示,最下方两个红框,右侧为tech()的内容,左测位teach()在mach-o文件里的地址。 加上当前的aslr,随机偏移地址: image.png

再通过 image list 命令得到 ASLR 程序运行的基地址、起始地址: 0x00000001000cc000image.png

其中B890是在mach-o文件里的偏移量,偏移量需要加上程序运行及地址,是teach()的函数的内存的地址。 0x00000001000cc000+0xB890=0x1000D7890,指向B890后面的结构。

swift的method在内存中的数据结构:

struct TargetMethodDescriptor {
// Flags 为 4 字节
MethodDescriptorFlags Flags;
// 这里存储的是相对指针:offset
TargetRelativeDirectPointer<Runtime, void> Impl;
};

0x1000D7890+0x4(4字节)+0xFFC220(offset)-0000000100000000(虚拟内存的起始地址) 再通过断点register read x8调试,与计算结果一致。

其他函数调度方式

将class改成struct结构体,发现调度方式变成静态派发:

image.png 如果struct、class添加一个extension 函数也是静态派发。

方法调度总结:

image.png

影响函数的派发方式

  • final 添加了final的关键字无法被充血,使用静态派发,不会再vtable中出现,且对objc运行时不可见。在属性、方法中,不需要被重载,添加final关键字。
class LeeTeacher {
    final func teach() {
        print("teach") 
        } ... 
}
  • dynamic 函数均可添加 dynamic 关键字,为非objc类和值类型的函数赋予动态性,但派发 方式还是函数表派发。
class LeeTeacher {
    dynamic func teach() {
        print("teach")
    }
}
extension LeeTeacher {
    @_dynamicReplacement(for: teach)
    func teach3() {
        print("teach3")
    }
}
let t = LeeTeacher()
t.teach()
打印结果:
teach3
  • @objc @objc关键字可以将Swift函数暴露给Objc运行时,依旧是函数表派发。
  • @objc + dynamic 消息派发的方式 ,objc_msgSend,那就意味着此时此刻可以使用method swizzling,也就是runtime的api。
  • static 类中static修饰的方法属于静态派发

函数内联

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

  • OC

image.png 示例代码,定义简单加减法函数:

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; 
}
  • 当没有优化时: 0x1存储到w8里,0x2存储到w1里,接下来bl sum函数。
  • 当有优化时: 编译器做了优化,计算完成,0x3直接存储到w8,在进行输出函数。

-swift

image.png swift默认函数内联行为,不需额外操作。swift可能回自动内联函数作为优化。 编译器会认为 test 没有太多意义,会省略test的符号调用,直接调print

func test() { 
    print("test"); 
}
  • always - 将确保始终内联函数。通过在函数前添加@inline(__always) 来实现此行为

  • never - 将确保永远不会内联函数。这可以通过在函数前添加 @inline(never) 来实现。

  • 如果函数很⻓并且想避免增加代码段大小,请使用@inline(never)(使用@inline(never))

  • 如果对象只在声明的文件中可⻅,可以用 private 或 fileprivate 进行修饰。编译器会对 private 或 fileprivate 对象进行检查,确保没有其他继承关系的情形下,自动打上 final 标记,进而使得对象获得静态派发的特性(fileprivate: 只允许在定义的源文件中访问,private : 定义的声明中访问)

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

可以看到 unpdateSex 的调用是直接的地址调用,并没有使用函数表。