一.异变方法
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寄存器中的地址

进来后发现,这里面就是方法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 Framework,dylib.dsym
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做内存映射的


-
__PAGEZERO主要是将低地址占用,防止用户访问。这里的VM Size就是Mach-o的基地址 - 程序在运行时,会加上
ASLR(地址空间布局随机化),来保证程序运行的安全


- OC的类存放在
Section64(_DATA_CONST,__objc_classlist) - Swift的类、结构体、枚举存放在
Section64(__TEXT,__swift5_types),每4字节作为一个区分,具体存放的是TargetClassDescriptor地址信息。
在这里第一个4字节就是我们今天要验证的LGTeacher的TargetClassDescriptor

这里的第一个4字节,小端模式,读取为0xFFFFFBA8
0xFFFFFBA8 + 0xBBCC = 0x10000B774(Descriptor在Mach-O文件的内存地址)
减去虚拟内存基地址(VM Address): 0x100000000
得出地址为:0xB774(Descriptor在Data数据区的内存地址)
找到0xB774,熟悉Mach-O的应该知道,这肯定在_TEXT_const中

- 这个地方就是
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字节

- size为0x00000004
- 0xB7A8就是
teach在Mach-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就是程序运行的基地址
-
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添加一个方法

类通过extension添加的方法也是静态派发
3.理解为什么类使用extension不写入函数表里面

这里Teacher有2个函数teach和teach1,LGPartTeacher有4个函数(继承了2个函数teach、teach1和自身的teach2、teach3)。假如这2个类在A.swift文件中,在B.swift文件中给LGTeacher添加teach4函数。当程序在编译A.swift时,编译了这2个类,此时vtable就生成了。此时程序再编译到B.swift时,LGTeacher中有一个extension,如果要在vtable添加的话,就应该在teach1下追加一个teach4。对于LGPartTeacher来说就应该加载teach1下面,但是LGPartTeacher已经确定了,再在中间插入一个MethodDescriptor,需要移动之前的teach2和teach3指针,还需要有一个下标来记录位移的index来保证extension添加的方法能够找到位置插入,此番操作代价太昂贵,因此编译器将extension添加的方法优化为静态调用。不会影响原有的vtable结构
3.方法调度的总结
| 类型 | 调用方式 | extension |
|---|---|---|
| 值类型 | 静态派发 | 静态派发 |
| Swift类 | 函数表派发 | 静态派发 |
| NSObject类 | 函数表派发 | 静态派发 |
4.影响函数派发方式
-
static、private、fileprivate添加后,会变成静态派发 -
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 Level为Optimize 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
}

0x102bfd4e4 <+188>: mov w8, #0x3 //将3复制到w8寄存器中
此时,在汇编里面已经没有关于sum函数调用了。编译器直接将1+2=3计算出来了
这样的行为就称为编译器优化技术
2.private、fileprivate对函数派发的影响
- 如果对象只在声明的文件中可见,可以用
private或fileprivate进行修饰。编译器会对private或fileprivate对象进行检查,确保没有其他继承关系的情形下,自动打上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()下一个断点

- 此时可以发现函数
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去掉,再次运行

- 函数
unpdateSex调度方式为函数表派发
