写在前面,上一节我们讲了swift类和结构体,这两者都可以添加方法。 那么同样是方法,在类和结构体中有什么异同呢?方法的调度是不是不一样呢?结构体是值类型的,方法修改self的值能成功修改吗? 让我们带着问题来一一分析。
1、函数相关的修饰符
首先我们先来了解一下swift方法的关键字。
mutating
结构体和枚举是值类型,默认情况下,值类型的属性不能被自身的实例方法修改,使用 mutating 关键字修饰方法是为了能在该方法中修改 结构体 或是 枚举 的变量。
final
Swift中,final关键字可以在class、func和var前修饰,表示不允许对其修饰的内容进行继承或者重新操作。
discardableResult
想消除方法返回值未被使用的 警告
来说的话,该属性还是很有用的,只需要在对应方法前添加 @discardableResult
属性即可。
lazy
lazy关键词的作用:指定延迟加载(懒加载),懒加载存储属性只会在首次使用时才会计算初始值属性。 lazy修饰的属性非线程安全的。
inout
方法的参数默认是不可变(constants)类型。在方法的内部视图去改变参数的值是会导致编译错误的。
func swapTwoInts(_ a:Int, _ b:Int) {
let temporaryA = a
a = b //报错:Cannot assign to value: 'a' is a 'let' constant
b = temporaryA//报错:Cannot assign to value: 'b' is a 'let' constant
}
如果想要在方法的内部去改变参数的值,并且想要在方法执行结束后仍然保持这个改变,那么可以用一个 in-out 参数替代原来的参数。
func swapTwoInts(_ a: inout Int, _ b: inout Int) {
let temporaryA = a
a = b //不报错
b = temporaryA//不报错
}
2、异变方法
2.1 值类型属性的结构体
是默认情况下,值类型属性不能被自身的实例方法修改,以下代码是会报错的。
struct Point{
var x = 0.0
var y = 0.0
func moveBy(x deltaX: Double, y deltaY: Double) {
x += deltaX//报错:Left side of mutating operator isn't mutable: 'self' is immutable
y += deltaY//报错:Left side of mutating operator isn't mutable: 'self' is immutable
}
}
并且编译器会给出修复方法,建议添加mutating关键字,Mark method 'mutating' to make 'self' mutable。 修复后:
struct Point{
var x = 0.0
var y = 0.0
mutating func moveBy(x deltaX: Double, y deltaY: Double) {
x += deltaX//不报错
y += deltaY//不报错
}
}
2.2 main.sil分析
那么为什么加了mutating
关键字就不报错呢,下面我们通过SIL来查看一下
SIL:Swift编程语言是在LLVM上构建,并且使用LLVM IR和LLVM的后端去生成代码。但是Swift编译器还包含新的高级别的中间语言,称为SIL。SIL会对Swift进行较高级别的语义分析和优化。详情见第6节补充资料。
方法Test的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'
方法moveBy的SIL中间码
// 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):
debug_value %0 : $Double, let, name "deltaX", argno 1 // id: %3
debug_value %1 : $Double, let, name "deltaY", argno 2 // id: %4
debug_value_addr %2 : $*Point, var, name "self", argno 3 // id: %5
%6 = begin_access [modify] [static] %2 : $*Point // users: %15, %7
%7 = struct_element_addr %6 : $*Point, #Point.x // users: %13, %8
%8 = struct_element_addr %7 : $*Double, #Double._value // user: %9
%9 = load %8 : $*Builtin.FPIEEE64 // user: %11
%10 = struct_extract %0 : $Double, #Double._value // user: %11
%11 = builtin "fadd_FPIEEE64"(%9 : $Builtin.FPIEEE64, %10 : $Builtin.FPIEEE64) : $Builtin.FPIEEE64 // user: %12
%12 = struct $Double (%11 : $Builtin.FPIEEE64) // user: %13
store %12 to %7 : $*Double // id: %13
%14 = tuple ()
end_access %6 : $*Point // id: %15
%16 = begin_access [modify] [static] %2 : $*Point // users: %25, %17
%17 = struct_element_addr %16 : $*Point, #Point.y // users: %23, %18
%18 = struct_element_addr %17 : $*Double, #Double._value // user: %19
%19 = load %18 : $*Builtin.FPIEEE64 // user: %21
%20 = struct_extract %1 : $Double, #Double._value // user: %21
%21 = builtin "fadd_FPIEEE64"(%19 : $Builtin.FPIEEE64, %20 : $Builtin.FPIEEE64) : $Builtin.FPIEEE64 // user: %22
%22 = struct $Double (%21 : $Builtin.FPIEEE64) // user: %23
store %22 to %17 : $*Double // id: %23
%24 = tuple ()
end_access %16 : $*Point // id: %25
%26 = tuple () // user: %27
return %26 : $() // id: %27
} // end sil function '$s4main5PointV6moveBy1x1yySd_SdtF'
从代码中我们可以分析得出,test()的入参是Point类型,而moveBy的入参是@inout Point类型。
前面关键字部分我们说过,如果想要在方法的内部去改变参数的值,并且想要在方法执行结束后仍然保持这个改变,那么可以用一个 in-out 参数替代原来的参数,也就是原本是值类型的self改变成引用类型的self进行传递。
也就是说,通过mutating
关键字,SIL底层传递@inout Point
类型的self,传入的是该实例的地址,所以这个结构体才能被自身的实例方法修改自身的属性值。
2.3 结构体的方法调用
如果对OC有了解的同学,应该都知道Objective-C底层是通过objc_msgSend
函数以运行时runtime
动态来查找方法调用的,那么swift是通过什么来进行方法调用的。
我们新建iOS项目,并使用真机arm64编译,代码如下:
struct JFPerson{
func test1(){}
func test2(){}
func test3(){}
}
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
var p1 = JFPerson()
p1.test1()//断点
p1.test2()
p1.test3()
}
}
通过编译的结果如下:
0x104cd69e8 <+92>: bl 0x104cd6988; methodPhoneDemo.JFPerson.init() -> methodPhoneDemo.JFPerson at ViewController.swift:9:8
0x104cd69ec <+96>: bl 0x104cd697c(函数地址); methodPhoneDemo.JFPerson.test1() -> () at ViewController.swift:10:18
0x104cd69f0 <+100>: bl 0x104cd6980(函数地址); methodPhoneDemo.JFPerson.test2() -> () at ViewController.swift:11:18
0x104cd69f4 <+104>: bl 0x104cd6984(函数地址); methodPhoneDemo.JFPerson.test3() -> () at ViewController.swift:12:18
通过对编译过后的代码进行分析,我们看到是汇编是直接调用bl
方法,拿到函数的地址直接调用methodPhoneDemo.JFPerson.test1()
等方法。
swift作为一门编译型语言,结构体的方法调用是在编译期已经确定好了,后续调用是通过函数地址直接调用即可,这种也被称作静态派发。那么问题来了,类的方法调用也是静态派发吗?如果不是,这会影响类方法调用的速度吗?
3、类的方法调度
同样,我们写一段class的代码
class JFPerson{
func test1(){}
func test2(){}
func test3(){}
}
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
let p1 = JFPerson()
p1.test1()
p1.test2()
p1.test3()
print("end")
}
}
打开Always Show Disassembly
查看程序运行时的汇编代码。
[Xcode]工具栏->Debug->Debug workflow: Always Show Disassembly,选中时可以查看程序运行时的汇编代码
3.1 汇编分析
同样,运行在真机后,查看对应的汇编代码,如下图:
图片中31行init是JFPerson的初始化代码,49行是release方法,而其中的就是JFPersonclass内的实现,我们写了三个方法,大胆猜测其中37行、41行、45行的bl就是我们的方法调用,通过在37行打断点,也证明的确是test1的方法调用,如下图。
那么问题又来了,37行的寄存器的值是从哪里来的?往前看,31行是bl,有值返回,也就是JFPerson的创建函数,返回的是JFPerson的实例变量。 mov x20 x0 就是将JFPerson对象结构体放入x20寄存器中, 再看下面的ldr指令,他是将内存中的值读取到寄存器当中,那么这条ldr指令意思就是,将x20的值读到x8寄存器中,64位寄存器x8取x0前8个字节(64位),是什么呢,JFPerson的实例对象前8个字节是什么?MedaData 如下打印地址证实
而
0x50
的偏移量就是我们的函数地址的偏移量,通过内存平移的方式,拿到函数地址再进行调用。
经过上面的推断和证实,test函数的调用过程,先做一个总结:
- 找到 Metadata
- 确定函数地址 = (metaDataAddress + 偏移量(#0x50)),
- 执行函数
通过上面汇编代码的观察,发现以下的调用特点,以8个字节进行调度,是连续的内存空间,Metadata里面是不是有一个连续的函数表?V-Table来存储函数地址的偏移量呢?我们的方法调度,就是基于这个偏移量的表来调度呢?
3.2 SIL分析
我们通过sil文件的分析,的确能看到vtable中有我们上面所有的函数
sil_vtable JFPerson {
#JFPerson.test1: (JFPerson) -> () -> () : @$s14ViewController8JFPersonC5test1yyF // JFPerson.test1()
#JFPerson.test2: (JFPerson) -> () -> () : @$s14ViewController8JFPersonC5test2yyF // JFPerson.test2()
#JFPerson.test3: (JFPerson) -> () -> () : @$s14ViewController8JFPersonC5test3yyF // JFPerson.test3()
#JFPerson.init!allocator: (JFPerson.Type) -> () -> JFPerson : @$s14ViewController8JFPersonCACycfC // JFPerson.__allocating_init()
#JFPerson.deinit!deallocator: @$s14ViewController8JFPersonCfD // JFPerson.__deallocating_deinit
}
3.3 源码分析
上一篇文章讲到了 Metdata 的数据结构,那么 V-Table 是存放在什么地方那?我们先来回顾一下当前的数据结构
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 ,就是对类的一个详细描述。
打开源码,在 metadata.h 中找到 Description
:
TargetSignedPointer<Runtime, TargetClassDescriptor> Description;
using ClassDescriptor = TargetClassDescriptor<InProcess>;
ClassDescriptor 是它的一个别名,全局搜索,在 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();
}
}
上面的代码这就是在创建 descriptor ,做了一些赋值的操作,我们也看到了 addVTable()
,点击查看:
void addVTable() {
if (IGM.hasResilientMetadata(getType(), ResilienceExpansion::Minimal)
&& (HasNonoverriddenMethods || !VTableEntries.empty()))
IGM.emitMethodLookupFunction(getType());
if (VTableEntries.empty()) // 数组
return;
auto offset = MetadataLayout->hasResilientSuperclass()
? MetadataLayout->getRelativeVTableOffset()
: MetadataLayout->getStaticVTableOffset();
B.addInt32(offset / IGM.getPointerSize());
B.addInt32(VTableEntries.size());
for (auto fn : VTableEntries)
emitMethodDescriptor(fn);
}
- B 就是 descriptor,遍历添加了
函数指针
,和函数的数量
。
至此,还原出 TargetClassDescriptor 结构如下:
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
}
4、其他关键字对函数派发方式的影响
- final: 添加了 final 关键字的函数无法被重写,使用静态派发,不会在 vtable 中出现,且 对 objc 运行时不可见。
- dynamic: 函数均可添加 dynamic 关键字,为非objc类和值类型的函数赋予动态性,但派发 方式还是函数表派发。
- @objc: 该关键字可以将Swift函数暴露给Objc运行时,依旧是函数表派发。
- @objc + dynamic: 消息派发的方式
5、函数内联
函数内联是一种编译器优化技术,它通过使用方法的内容替换直接调用该方法,从而优化性能。
- Swift 中的内联函数是默认行为,我们无需执行任何操作. Swift 编译器可能会自动内联函数作为优化。
// 编译器会认为 test 没有太多意义,会省略test的符号调用,直接调用print func test() { print("test"); } 复制代码
- always - 将确保始终内联函数。通过在函数前添加 @inline(__always) 来实现此行为
- never - 将确保永远不会内联函数。这可以通过在函数前添加 @inline(never) 来实现。
- 如果函数很⻓并且想避免增加代码段大小,请使用@inline(never)(使用@inline(never))
6、补充资料
6.1、SIL
Swift作为一种高级语言,有些高级特性,比如基于protocol的泛型。而且也是一门安全的语言,确保变量在使用之前被初始化、检测不可执行的代码(unreachable code)。于是为Swift编译器增加了一层SIL来做这些事情。
SIL
- 能够完全保留程序的语义
- 专为代码生成和分析而设计
- 在编译器流程的hot path上
- 弥补源码和LLVM之间的巨大抽象
SIL简介
6.2 汇编常用指令
- MOV: 用于将一个寄存器或被移位寄存器或一个立即数移动到目的寄存器
- add:将某一寄存器的值和另一寄存器的值 相加 并将结果保存在另一寄存器中。
- ldr:将内存中的值读取到寄存器中。
- lb: 跳转到某地址(有返回)
- lbr: 跳转到某地址(无返回)
- stur: 把寄存器的值(32位)存到一个内存的虚地址内间(一般等同str)
引用
- Swift 的SIL语言是什么? www.jianshu.com/p/1bb7908f6…