写在前面: swift-类与结构体(上) 为关于初始化、类型上的区别。 本次讲解方法上的区别。
一、异变方法
- 默认值类型的属性不能被自身的实例方法修改。
如图示代码报错
left side of mutating operator isn't mutable: 'self' is immutable,说明self是不可更改的。
在moveBy修改x、y时,self是结构体,值类型,存放两个属性的值。
self指代x和y。
如图示,内部函数的修改影响外部,所以:默认值类型的属性不能被自身的实例方法修改。
- 如果需要被自身修改,在方法添加
mutaing关键字。
通过函数是否用mutaing修饰,生成SIL语句,对比区别。
如图为定义的区别
继续找到test函数
- @$s4main5PointV4testyyF:命名重整之后的名称
- Point类型的默认参数,即self
找到moveBy函数
- s4main5PointV6moveBy1x1yySd_SdtF:命名重整之后的名称
- 参数x
- 参数y
- Point 即self(多了个inout关键字)
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开启 来查看代码对应的汇编代码。\
- 常见指令的总结:
再执行单步执行,进入到teach函数调用页面。
接下来,把函数扩充,分别执行,再次查看汇编代码查看bl和blr。
分别对应方法1、方法2、方法3
- 其中x8含义是寄存器 ,其值是 通过mov指令,把x0的值赋值到x8寄存器里。
- 函数的返回值放在x0寄存器里,x0存放实例对象;
- 取x0实例对象的前八个字节,放在x8寄存器里;
- x0的第一个8字节:metdata,通过命令行读取得到
register read x8
x8 = 0x0000000000000003 - metadataAdress + 0x50 赋值给x8寄存器
-- 总结 --
teach函数的调用过程:
- 找到
Metadata - 确定函数地址(metadata + 偏移量)
- 执行函数
第一种调度方式:基于函数表的调度
- 编译成SIL文件
如图示
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 ,不管是 Class , Struct , Enum 都有自己 的 Descriptor ,就是对类的一个详细描述:
回到swift-source 打开 metadata.h 文件,
点击进去 ,通过继承关系,把结构体还原出来:
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) -> 找到相关文件。
- 点击进入GenMeta.cpp,找到类ClassContextDescriptorBulder
- 找到layout 布局方法
- 相比类似 ,说明layout在创建descriptor
-> 有addVTable,进入方法:
- 判断为空-> 优化判断 -> 计算偏移 -> 添加到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软件打开:
-
首先是文件头,表明该文件是 Mach-O格式,指定目标架构,还有一些其他的文件属性信息,文件头信息影响后续的文件结构安排包含二进制信息:
-
Load commands是一张包含很多内容的表。内容包括区域的位置、符号表、动态符号表 等。
-
Data 区: 主要就是负责代码和数据记录的。Mach-O 是以 Segment 这种结构来组织数据 的,一个 Segment 可以包含 0 个或多个 Section。根据 Segment 是映射的哪一个 Load Command,Segment 中 section 就可以被解读为是是代码,常量或者一些其他的数据类 型。在装载在内存中时,也是根据 Segment 做内存映射的。
- Mach-O 分析V-Table
Section64(_TEXT,__swift5_types)中存放的就是Descriptor地址信息;\
如上图,验证计算 Descriptor 在 Mach-O 的内存地址:
0xFFFFFBF4 + 0xBC68 = 0x10000B85C
0x10000开头,在mach-o文件开头叫做虚拟内存的地址。
在虚拟内存的地址,位置如下图:
B85C的位置在如下红箭头所指:
之后的数据结构和targetClassDescriptor结构体一一对应。
验证:往后数12个4字节,如下图所示,最下方两个红框,右侧为tech()的内容,左测位teach()在mach-o文件里的地址。
加上当前的aslr,随机偏移地址:
再通过 image list 命令得到 ASLR 程序运行的基地址、起始地址: 0x00000001000cc000:
其中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结构体,发现调度方式变成静态派发:
如果struct、class添加一个extension 函数也是静态派发。
方法调度总结:
影响函数的派发方式
- 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
示例代码,定义简单加减法函数:
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
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 的调用是直接的地址调用,并没有使用函数表。