一、 消息派发类型:
笼统的说法是分动态和静态,在Swift 中并不只有 2 种(静态和动态)派发方法,而是 4 种派发方法:
-
内联 inline(最快)
-
静态派发 Static Dispatch
-
动态虚拟派发 Virtual Dispatch
-
动态消息派发 Dynamic Dispatch(最慢)
由编译器来决定应该使用哪种派发方法,优先使用内联,然后再按需选择。
ps: 内联派发可以理解成不需要进行函数地址跳转,直接运行函数中的代码块
静态派发:
它的速度非常快,因为编译器能够在编译时确定指令所在的位置。因此,当函数被调用时,编译器直接跳转到函数的内存地址来执行操作。这将带来巨大的性能提升和某些编译器优化,比如内联。
动态派发:
-
函数表派发 (Table Dispatch)
这种方式是编译型语言最常见的派发方式,他既保证了动态性也兼顾了执行效率。函数所在的类会维护一个“函数表”,也就是我们熟知的虚函数表,Swift 里称为 “witness table”。该函数表存取了每个函数实现的指针。每个类的vtable在编译时就会被构建,所以与直接派发相比只多出了两个读取的工作: 读取该类的vtable和该函数的指针。理论上说,函数表派发也是一种高效的方式。不过和直接派发相比,编译器对某些含有副作用的函数却无法优化,也是导致函数表派发变慢的原因之一。
跳转原理:
以调用ChildClass的method2为例
源码:
class ParentClass {
func method1() {}
func method2() {}
}
class ChildClass: ParentClass {
override func method2() {}
func method3() {}
}
复制代码
跳转步骤大致如下:
-
读取0xB00 虚函数表(vtable)
-
读取method2函数指针0x222
-
跳转到地址0x222, 读取函数实现
-
消息机制派发 (Message Dispatch)
在Swfit使用的消息派发依旧是Objc的运行时系统。
消息机制是调用函数最动态的方式,也是 Cocoa 的基石。
所有的方法基本都是通过 objc_msgSend(或者super_msgSend)函数,将self和selector作为默认参数进行传递,函数通过isa指针抓取类的层次结构以确定调用哪个方法。
实际上运行时机制远比这个复杂多,大致分三个步骤:
-
消息发送
-
消息动态解析
-
消息转发
这真的很慢。所幸的是每个类都会有一块缓存,若是之后发送相同的消息,执行速率会很快,会把性能提高到和函数表派发一样快。
二、 消息派发的原则:
原则:
直接调用
函数表
消息机制
明确执行
final, static
—
dynamic(NSObject子类)
值类型
所有方法
—
—
协议
拓展中的方法
定义的方法
—
类
拓展中的方法
定义的方法
带有 @objc 的扩展
这也就是Swift类中拓展的方法是无法重写,除非使用OC运行时机制;
这里拓展中的方法是可以被遵循者重写的,但是拓展中的方法还是直接调用的,遵循者实现了某个方法是不能再调用协议中的该方法,具体遵循者真么派发看遵循者怎么定义如果是直接调用那就是直接调用,如果可以被继承那就是虚函数表调度方式。
修饰符:
static:确定静态派发
fianl:确定是静态派发
dynamic: 确定使用消息机制,必须是最终继承NSObject
@objc: 对OC可用,内部不一定使用消息机制
@objc dynamic : 对OC可用, 确定使用消息机制
@inlinable:顾名思义是想告诉编译器将此函数直接派发,但将其转换成SIL代码后,依旧是vtable派发
class: 类方法函数表派发,如果fianl class的class方法可能就会本优化成静态派发
详细说明查阅 修饰符官方文档
补充:
-
Swift会尽可能的去优化函数派发方式,例如一个私有函数时,该函数很可能会被优化为直接派发
-
不同版本Swift消息派发机制有一定的差异性,4.0版本以后整体派发原则差不多,只是一些优化,版本差异性不细说4.0以下没有研究的意义。
三、 如何验证消息派发机制
代码编译过程:
LLVM 架构:
LLVM是一种编译架构,主要优化了多种语言源码转不同架构的机器码,当新增一种语言时只需新增一个前端编译器(转成LLVM IR代码),就可以实现转不同的机器码;相反当新增一个当新增一个LLVM IR代码转机器码的后端编译器。
LLVM 结构图:
OC编译
OC前端代码使用了clang编译器
OC 源码 -> 预处理成C++代码 -> IR前端代码
生成指令:
$: clang -x objective-c -rewrite-objc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOS.platform/Developer/SDKs/MacOS.sdk xxx.m
复制代码
xxx.m 为需要转化的OC源文件
如果嫌麻烦在~/.bash_profile中设置“宏” ,例如:
alias ft_rwobjc='clang -x objective-c -rewrite-objc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOS.platform/Developer/SDKs/MacOS.sdk'
复制代码
生成指令简化成:
$: ft_rwobjc xxx.m
复制代码
Swift前端编译
Swift使用了swiftc 编译器
Swift 源码 -> 语法树AST -> 中间代码SIL -> IR前端代码
SIL有两种形式,raw SIL(原始SIL) 和 canonical SIL(规范SIL),刚刚从SILGen中出来的未经优化的SIL称为raw SIL
Swift源码转SIL命令
生成 raw SIL:
swiftc -emit-silgen Source.swift -o Source.sil
生成 canonical SIL:
swiftc Source.swift -emit-sil > Source-canonical.sil
SIL语法:
截取了函数约定部分说明
这里简单翻译一下
函数的约定,由@convention(convention)属性表示:
-
@convention(thin): 表示轻量级方法,表示没有特殊的“self”或“context”参数
-
@convention(thick):表示重量级方法,它使用 Swift 调用约定并携带一个引用计数的上下文对象,用于表示函数所需的捕获或其他状态。此属性由@callee_owned或隐含@callee_guaranteed
-
@convention(block):表示 Objective-C 兼容的block引用。函数值表示为对block对象的引用,block对象是一个id兼容的 Objective-C 对象,将其调用函数嵌入到对象中。调用函数使用 C 调用约定
-
@convention(c)表示 C 函数引用。函数值不携带上下文并使用 C 调用约定
-
@convention(objc_method)表示一个 Objective-C 方法实现。该函数使用 C 调用约定,将 SIL 级self 参数(通过 SIL 约定映射到最终形参)映射到实现的self和_cmd参数
-
@convention(method): 表示 Swift 实例方法实现。该函数使用 Swift 调用约定,使用特殊self 参数
-
@convention(witness_method):表示 Swift 协议方法实现。函数的多态约定以这样一种方式发出,以保证它在协议的所有可能实现者中都是多态的
SIL优化:
一般优化通过 SIL 捕获特定于语言的类型信息,从而可以执行在 LLVM IR 上难以执行的高级优化。
-
泛型专门化分析对泛型函数的专门调用并生成函数的新专门版本。然后它将泛型的所有专用用法重写为对适当专用函数的直接调用。
-
给定类型的witness和 VTable 去虚拟化从类的虚拟表或类型见证表中查找关联的方法,并将间接虚拟调用替换为对映射函数的调用。
-
性能内联
-
引用计数优化
-
内存提升/优化
基于这些优化Swift在4.0之后不再支持对类进行dynamic修饰了,正因为Swift优化太多了如果再在Swift使用dynamic修饰类相当于写OC代码,这将没有意义,我觉得这也就是OC代码不能继承自Swift类原因。
前端中间代码剖析:
OC:
OC源码
@interface Student : NSObject
@property (nonatomic, copy) NSString *name;
@end
@implementation Student
@end
int main() {
Student *stu = [[Student alloc] init];
stu.name = @"Good Student";
NSLog(@"stu = %@", stu);
return 0;
}
复制代码
转化的C++
实现部分代码
extern "C" unsigned long OBJC_IVAR_$_Student$_name;
struct Student_IMPL {
struct NSObject_IMPL NSObject_IVARS;
NSString *_name;
};
// @property (nonatomic, copy) NSString *name;
/* @end */
// @implementation Student
static NSString * _I_Student_name(Student * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_Student$_name)); }
extern "C" __declspec(dllimport) void objc_setProperty (id, SEL, long, id, bool, bool);
static void _I_Student_setName_(Student * self, SEL _cmd, NSString *name) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct Student, _name), (id)name, 0, 1); }
// @end
复制代码
消息发送部分代码:
int main() {
Student *stu = ((Student *(*)(id, SEL))(void *)objc_msgSend)((id)((Student *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Student"), sel_registerName("alloc")), sel_registerName("init"));
((void (*)(id, SEL, NSString *))(void *)objc_msgSend)((id)stu, sel_registerName("setName:"), (NSString *)&__NSConstantStringImpl__var_folders_h__dw86ldt90kb41f_vmvkpss700000gn_T_oc_main_986940_mi_0);
NSLog((NSString *)&__NSConstantStringImpl__var_folders_h__dw86ldt90kb41f_vmvkpss700000gn_T_oc_main_986940_mi_1, stu);
return 0;
}
复制代码
可以看出这些对象方法(包括类方法)都是基于 objc_msgSend (或者super_msgSend) 实现的。
Swift:
首先 定义一个Student类和其子类
class Student: NSObject {
/// 属性
var name: String = ""
/// 直接调用 final
final func finalMethod() {
}
/// 虚函数调用
func vtableMethod() {
}
/// 运行时方法
@objc dynamic func runTimeMethod() {
}
}
/// 子类方法
class GoodStuden: Student {
override func vtableMethod() {
}
}
复制代码
函数实现部分代码:
// Student.finalMethod()
sil hidden [ossa] @$s10swift_main7StudentC11finalMethodyyF : $@convention(method) (@guaranteed Student) -> () {
// %0 "self" // user: %1
bb0(%0 : @guaranteed $Student):
debug_value %0 : $Student, let, name "self", argno 1 // id: %1
%2 = tuple () // user: %3
return %2 : $() // id: %3
} // end sil function '$s10swift_main7StudentC11finalMethodyyF'
// Student.vtableMethod()
sil hidden [ossa] @$s10swift_main7StudentC12vtableMethodyyF : $@convention(method) (@guaranteed Student) -> () {
// %0 "self" // user: %1
bb0(%0 : @guaranteed $Student):
debug_value %0 : $Student, let, name "self", argno 1 // id: %1
%2 = tuple () // user: %3
return %2 : $() // id: %3
} // end sil function '$s10swift_main7StudentC12vtableMethodyyF'
// Student.runTimeMethod()
sil hidden [ossa] @$s10swift_main7StudentC13runTimeMethodyyF : $@convention(method) (@guaranteed Student) -> () {
// %0 "self" // user: %1
bb0(%0 : @guaranteed $Student):
debug_value %0 : $Student, let, name "self", argno 1 // id: %1
%2 = tuple () // user: %3
return %2 : $() // id: %3
} // end sil function '$s10swift_main7StudentC13runTimeMethodyyF'
// @objc Student.runTimeMethod()
sil hidden [thunk] [ossa] @$s10swift_main7StudentC13runTimeMethodyyFTo : $@convention(objc_method) (Student) -> () {
// %0 // user: %1
bb0(%0 : @unowned $Student):
%1 = copy_value %0 : $Student // users: %6, %2
%2 = begin_borrow %1 : $Student // users: %5, %4
// function_ref Student.runTimeMethod()
%3 = function_ref @$s10swift_main7StudentC13runTimeMethodyyF : $@convention(method) (@guaranteed Student) -> () // user: %4
%4 = apply %3(%2) : $@convention(method) (@guaranteed Student) -> () // user: %7
end_borrow %2 : $Student // id: %5
destroy_value %1 : $Student // id: %6
return %4 : $() // id: %7
} // end sil function '$s10swift_main7StudentC13runTimeMethodyyFTo'
复制代码
这里对象方法都有使用 @convention(method) Swift调用约定内部需要使用‘self’,fianl 还是虚函数派发这里看不出来统一使用了 @convention(method) 声明。需要配合vtable看,后面会提到。
Student.runTimeMethod 使用的是 @convention(objc_method) 运行时方法,这里也注意到了默认有一个同名 Swift实现方法,在objc_method 中有apply(SIL中apply标识调用方法命令)这个方法,实现用了Swift方法因为可以看出使用OC多了 copy_value destroy_value 这种计数器相关管理调度无形之中多会多执行很多补充代码,而Swift对引用计数有优化(这里还不知道怎么优化),如果函数规模大的话也是很大的优化
为啥没有@convention(witness_method)这里类型是确定的, 协议内的方法约定的。
在来看一下协议调用方式
源码部分:
/// 学习协议
protocol StudyProtocol {
/// 看书
func readBook()
/// 写文章
func writeArticle()
}
/// 遵循协议
class Student: NSObject, StudyProtocol {
func readBook() {
}
func writeArticle() {
}
}
/// 好学生写好文章
class GoodStuden: Student {
override func writeArticle() {
/// 写好文章
}
}
// protocol witness for StudyProtocol.readBook() in conformance Student
sil private [transparent] [thunk] [ossa] @$s10swift_main7StudentCAA13StudyProtocolA2aDP8readBookyyFTW : $@convention(witness_method: StudyProtocol) (@in_guaranteed Student) -> () {
// %0 // user: %1
bb0(%0 : $*Student):
%1 = load_borrow %0 : $*Student // users: %5, %3, %2
%2 = class_method %1 : $Student, #Student.readBook : (Student) -> () -> (), $@convention(method) (@guaranteed Student) -> () // user: %3
%3 = apply %2(%1) : $@convention(method) (@guaranteed Student) -> ()
%4 = tuple () // user: %6
end_borrow %1 : $Student // id: %5
return %4 : $() // id: %6
} // end sil function '$s10swift_main7StudentCAA13StudyProtocolA2aDP8readBookyyFTW'
// protocol witness for StudyProtocol.writeArticle() in conformance Student
sil private [transparent] [thunk] [ossa] @$s10swift_main7StudentCAA13StudyProtocolA2aDP12writeArticleyyFTW : $@convention(witness_method: StudyProtocol) (@in_guaranteed Student) -> () {
// %0 // user: %1
bb0(%0 : $*Student):
%1 = load_borrow %0 : $*Student // users: %5, %3, %2
%2 = class_method %1 : $Student, #Student.writeArticle : (Student) -> () -> (), $@convention(method) (@guaranteed Student) -> () // user: %3
%3 = apply %2(%1) : $@convention(method) (@guaranteed Student) -> ()
%4 = tuple () // user: %6
end_borrow %1 : $Student // id: %5
return %4 : $() // id: %6
} // end sil function '$s10swift_main7StudentCAA13StudyProtocolA2aDP12writeArticleyyFTW'
复制代码
协议使用了 witness_method 函数约定,后面紧跟着 (@in_guaranteed Student)也就是说这个是Student专门派发的调度方式。协议在没有任何遵循情况下,这个中间代码的,同时也可以验证一下多个遵循者是不是多一个实现,结论是会多一个
// protocol witness for StudyProtocol.readBook() in conformance Theacher
sil private [transparent] [thunk] [ossa] @$s10swift_main8TheacherCAA13StudyProtocolA2aDP8readBookyyFTW : $@convention(witness_method: StudyProtocol) (@in_guaranteed Theacher) -> () {
// %0 // user: %1
bb0(%0 : $*Theacher):
%1 = load_borrow %0 : $*Theacher // users: %5, %3, %2
%2 = class_method %1 : $Theacher, #Theacher.readBook : (Theacher) -> () -> (), $@convention(method) (@guaranteed Theacher) -> () // user: %3
%3 = apply %2(%1) : $@convention(method) (@guaranteed Theacher) -> ()
%4 = tuple () // user: %6
end_borrow %1 : $Theacher // id: %5
return %4 : $() // id: %6
} // end sil function '$s10swift_main8TheacherCAA13StudyProtocolA2aDP8readBookyyFTW'
复制代码
上述代码可以看出多了一个(@in_guaranteed Theacher)实现。调度协议方法时是尽管协议是抽象的但是调用的时候却是确定类型的,在编译时根据类型调度不同类的方法。
虚函数表代码:
标记这些方法是走的虚函数派发的
sil_vtable Student {
#Student.getStudentType: (Student.Type) -> () -> Int : @$s10swift_main7StudentC03getC4TypeSiyFZ // static Student.getStudentType()
#Student.name!getter: (Student) -> () -> String : @$s10swift_main7StudentC4nameSSvg // Student.name.getter
#Student.name!setter: (Student) -> (String) -> () : @$s10swift_main7StudentC4nameSSvs // Student.name.setter
#Student.name!modify: (Student) -> () -> () : @$s10swift_main7StudentC4nameSSvM // Student.name.modify
#Student.vtableMethod: (Student) -> () -> () : @$s10swift_main7StudentC12vtableMethodyyF // Student.vtableMethod()
#Student.readBook: (Student) -> () -> () : @$s10swift_main7StudentC8readBookyyF // Student.readBook()
#Student.writeArticle: (Student) -> () -> () : @$s10swift_main7StudentC12writeArticleyyF // Student.writeArticle()
#Student.deinit!deallocator: @$s10swift_main7StudentCfD // Student.__deallocating_deinit
}
sil_vtable Theacher {
#Theacher.readBook: (Theacher) -> () -> () : @$s10swift_main8TheacherC8readBookyyF // Theacher.readBook()
#Theacher.writeArticle: (Theacher) -> () -> () : @$s10swift_main8TheacherC12writeArticleyyF // Theacher.writeArticle()
#Theacher.init!allocator: (Theacher.Type) -> () -> Theacher : @$s10swift_main8TheacherCACycfC // Theacher.__allocating_init()
#Theacher.deinit!deallocator: @$s10swift_main8TheacherCfD // Theacher.__deallocating_deinit
}
sil_vtable GoodStuden {
#Student.getStudentType: (Student.Type) -> () -> Int : @$s10swift_main10GoodStudenC14getStudentTypeSiyFZ [override] // static GoodStuden.getStudentType()
#Student.name!getter: (Student) -> () -> String : @$s10swift_main7StudentC4nameSSvg [inherited] // Student.name.getter
#Student.name!setter: (Student) -> (String) -> () : @$s10swift_main7StudentC4nameSSvs [inherited] // Student.name.setter
#Student.name!modify: (Student) -> () -> () : @$s10swift_main7StudentC4nameSSvM [inherited] // Student.name.modify
#Student.vtableMethod: (Student) -> () -> () : @$s10swift_main10GoodStudenC12vtableMethodyyF [override] // GoodStuden.vtableMethod()
#Student.readBook: (Student) -> () -> () : @$s10swift_main7StudentC8readBookyyF [inherited] // Student.readBook()
#Student.writeArticle: (Student) -> () -> () : @$s10swift_main10GoodStudenC12writeArticleyyF [override] // GoodStuden.writeArticle()
#GoodStuden.deinit!deallocator: @$s10swift_main10GoodStudenCfD // GoodStuden.__deallocating_deinit
}
sil_witness_table hidden Student: StudyProtocol module swift_main {
method #StudyProtocol.readBook: <Self where Self : StudyProtocol> (Self) -> () -> () : @$s10swift_main7StudentCAA13StudyProtocolA2aDP8readBookyyFTW // protocol witness for StudyProtocol.readBook() in conformance Student
method #StudyProtocol.writeArticle: <Self where Self : StudyProtocol> (Self) -> () -> () : @$s10swift_main7StudentCAA13StudyProtocolA2aDP12writeArticleyyFTW // protocol witness for StudyProtocol.writeArticle() in conformance Student
}
sil_witness_table hidden Theacher: StudyProtocol module swift_main {
method #StudyProtocol.readBook: <Self where Self : StudyProtocol> (Self) -> () -> () : @$s10swift_main8TheacherCAA13StudyProtocolA2aDP8readBookyyFTW // protocol witness for StudyProtocol.readBook() in conformance Theacher
method #StudyProtocol.writeArticle: <Self where Self : StudyProtocol> (Self) -> () -> () : @$s10swift_main8TheacherCAA13StudyProtocolA2aDP12writeArticleyyFTW // protocol witness for StudyProtocol.writeArticle() in conformance Theacher
}
复制代码
可以看出每个类都维护了自己一个函数表,而使用了fianl修饰或者@objc修饰就没有加到虚函数表中;每个类都会对应有deinit方法;
这里每个遵循协议都会有一个对应的函数表(sil_witness_table),这里实现是都指向了遵循者sil_vtable中的具体实现。
这里加了 hidden 标记实际上这个是函数表是不会调用的只是一个辅助代码,看实际调度部分SIL代码
源码:
let stu: Student = GoodStuden()
/// 写文章
stu.writeArticle()
复制代码
SIL代码:
%0 = metatype $@thick GoodStuden.Type // user: %2
// function_ref GoodStuden.__allocating_init()
%1 = function_ref @$s10swift_main10GoodStudenCACycfC : $@convention(method) (@thick GoodStuden.Type) -> @owned GoodStuden // user: %2
%2 = apply %1(%0) : $@convention(method) (@thick GoodStuden.Type) -> @owned GoodStuden // user: %3
%3 = upcast %2 : $GoodStuden to $Student // users: %13, %5, %4
debug_value %3 : $Student, let, name "stu" // id: %4
%5 = begin_borrow %3 : $Student // users: %8, %7, %6
%6 = class_method %5 : $Student, #Student.writeArticle : (Student) -> () -> (), $@convention(method) (@guaranteed Student) -> () // user: %7
%7 = apply %6(%5) : $@convention(method) (@guaranteed Student) -> ()
end_borrow %5 : $Student // id: %8
复制代码
这里看上去是走的 Student 函数调度,实际走了虚函数派发机制,最终走了GoodStudent的方法。
这里为啥为啥不是GoodStudent,因为有类型限定,如果把类型限定去调的话SIL代码变成了GoodStudent
%0 = metatype $@thick GoodStuden.Type // user: %2
// function_ref GoodStuden.__allocating_init()
%1 = function_ref @$s10swift_main10GoodStudenCACycfC : $@convention(method) (@thick GoodStuden.Type) -> @owned GoodStuden // user: %2
%2 = apply %1(%0) : $@convention(method) (@thick GoodStuden.Type) -> @owned GoodStuden // users: %12, %4, %3
debug_value %2 : $GoodStuden, let, name "stu" // id: %3
%4 = begin_borrow %2 : $GoodStuden // users: %7, %6, %5
%5 = class_method %4 : $GoodStuden, #GoodStuden.writeArticle : (GoodStuden) -> () -> (), $@convention(method) (@guaranteed GoodStuden) -> () // user: %6
%6 = apply %5(%4) : $@convention(method) (@guaranteed GoodStuden) -> ()
复制代码
四、总结:
总体规则:
-
Swift 编写函数大部分走的是静态方法,这也就是Swift快的原因所在
-
协议继承和类继承确保对象多态性会使用虚函数表进行动态派发
-
继承自NSObject对象通过 dynamic/ @objc dynamic 关键字让其走消息机制派发
-
不同版本对Swift、debug和release都会有不同程度编译优化,派发机制也会存在差异性
开发建议:
-
能用值类型地方就有值类型,不仅仅是因为其拷贝速度快,方法调度也快
-
多使用private final 等关键字,一方面提高代码阅读性,编译器内部也对消息调度进行优化
-
代码分类多使用拓展,拓展中的方法是静态派发(除了定义成运行时方法)
-
遵守的协议(这里说的是Swift协议)尽量写在拓展中,如果希望被子类重写的话。建议不要使用类的多态,而是使用协议进行抽象,将需要属性和多种实现的方法抽取到协议中,拓展实现一些共用方法。这样不仅移除对父类的依赖也可以实现‘多继承’
-
OC混编时候,使用了一些OC特性的框架(例如KVO),不仅仅只需要对属性或者方法进行@objc 声明,还需要对其进行dynamic修饰才能按照预期的来