一、异变方法
1.1 异变方法
值类型属性不能被自身的实例方法修改
注意:moveBy是实例方法,不是构建器。
值类型如果想要实现能被自身的实例方法修改,那要在前面加一个mutating
修饰
那么加不加mutating
底层什么样?来加一个常规方法,然后通过SIL文件来看看:
// Point.move(_:)
sil hidden @$s4main5PointV4moveyySdF : $@convention(method) (Double, Point) -> () {
// %0 "deltaX" // users: %4, %2
// %1 "self" // user: %3
bb0(%0 : $Double, %1 : $Point):
debug_value %0 : $Double, let, name "deltaX", argno 1 // id: %2
debug_value %1 : $Point, let, name "self", argno 2 // id: %3
debug_value %0 : $Double, let, name "u" // id: %4
%5 = tuple () // user: %6
return %5 : $() // id: %6
} // end sil function '$s4main5PointV4moveyySdF'
// Point.moveBy(_:_:)
sil hidden @$s4main5PointV6moveByyySd_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
//省略部分代码
return %26 : $() // id: %27
} // end sil function '$s4main5PointV6moveByyySd_SdtF'
我们都知道,默认方法还传递self
参数,观察以上代码
区别一:mutating
修饰的方法给Point
前面加上了 inout
关键字
区别二:move(_:) 方法的 %1 参数:$Point, let, name "self", argno 2 意思是self
是let常量值
moveBy(::) 方法的 %2 参数:$*Point, var, name "self", argno 3 意思是self
是var变量值
也就是说,mutating
修饰的方法的self
传递的是指针的,所以能修改地址对应的值。(Ponit参数代指的就是self,所以我们在方法中可以使用self)
【注意,以上SIL代码中的s4main5PointV4moveyySdF和s4main5PointV6moveByyySd_SdtF,分别是 move(:)函数 和 moveBy(:_:)函数,只是SIL混写后的名字,可以使用命令:xcrun swift-demangle <混写后的名称>
对其还原查看】
异变方法的本质:对于异变方法,传入的 self 被标记成 inout 参数。无论在mutating方法内部发生什么,都会影响外部依赖类型的一切。
1.2 inout
如果我们想让函数能够修改一个形式参数的值,而且希望这些改变在函数结束后依旧生效,那么就需要将形式参数定义成inout。
struct Point{
var x = 0.0, y = 0.0
func move(_ deltaX:Double, _ deltaY: inout Double){
let u = deltaX
deltaY = 50
}
}
看看SIL:
// Point.move(_:_:)
sil hidden @$s4main5PointV4moveyySd_SdztF : $@convention(method) (Double, @inout Double, Point) -> () {
// %0 "deltaX" // users: %6, %3
// %1 "deltaY" // users: %9, %4
// %2 "self" // user: %5
bb0(%0 : $Double, %1 : $*Double, %2 : $Point):
debug_value %0 : $Double, let, name "deltaX", argno 1 // id: %3
debug_value_addr %1 : $*Double, var, name "deltaY", argno 2 // id: %4
debug_value %2 : $Point, let, name "self", argno 3 // id: %5
debug_value %0 : $Double, let, name "u" // id: %6
//省略代码
return %12 : $() // id: %13
} // end sil function '$s4main5PointV4moveyySd_SdztF'
二、方法调度
先来点儿预备知识:寄存器指令
2.1 class方法调度
来一段测试代码:
class CCTeacher{
func teach(){
print("teach")
}
func teach1(){
print("teach1")
}
func teach2(){
print("teach2")
}
}
class ViewController: UIViewController{
override func viewDidLoad() {
let t = CCTeacher()
t.teach()
t.teach1()
t.teach2()
}
}
下面通过两个角度来探索class的方法调度方式
1、lldb断点,观察寄存器指令
2、观察sil代码
2.1.1 LLDB断点,观察寄存器指令
来一波断点,运行真机
观察断点:刚好三个blr方法跳转,也就是方法调用
函数的实例对象是放在x0
寄存器里的
mov x20, x0
这句意思是把x0
的值,复制到x20
里,也就是把实例对象复制到x20
里
ldr x8, [x20]
这句意思是取x20
里的值,放到x8
里面,那么现在x8
存就是实例对象,取的是这个实例对象的前8个字节,因为寄存器是64位,存放8个字节,而x20的第一个8字节是什么?没错,metadata
ldr x8, [x8, #0x50]
这句意思是把 x8
和 #0x50
的值相加(偏移量),然后放到x8
里面去
blr x8
最后,调用x8
总结:class函数的调用过程:找到
Metadata
-> 确定函数地址(metadata+偏移量)-> 执行函数
2.1.2 函数表v_table
仔细看一下调用前相加的偏移量:
#0x50
#0x58
#0x60
互相之间相差8个字节!那这8个字节就是函数指针的大小,且他们在内存中是连续的内存空间,到这里,就可以引出swift第一种函数调度方式:基于 函数表 v_table
的调度
编译个SIL来看看:
sil_vtable CCTeacher {
#CCTeacher.teach: (CCTeacher) -> () -> () : @$s14ViewController9CCTeacherC5teachyyF // CCTeacher.teach()
#CCTeacher.teach1: (CCTeacher) -> () -> () : @$s14ViewController9CCTeacherC6teach1yyF // CCTeacher.teach1()
#CCTeacher.teach2: (CCTeacher) -> () -> () : @$s14ViewController9CCTeacherC6teach2yyF // CCTeacher.teach2()
#CCTeacher.init!allocator: (CCTeacher.Type) -> () -> CCTeacher : @$s14ViewController9CCTeacherCACycfC // CCTeacher.__allocating_init()
#CCTeacher.deinit!deallocator: @$s14ViewController9CCTeacherCfD // CCTeacher.__deallocating_deinit
}
sil文件中,这个sil_vtable
就是函数表,它就罗列出CCTeacher这个类有哪些函数
2.1.3从源码查询vtable
在上一篇中,我们把Swift类的本质整理出来了,里面有一个metadata
,其中一个成员是:
var typeDescriptor: UnsafeMutableRawPointer
类、结构体、枚举 都有这个成员变量,是存放对自己的描述
在源码中找到typeDescriptor
的定义,查找流程为:
- 在HeapObject.h文件中找到HeapMetadata
using HeapMetadata = TargetHeapMetadata<InProcess>;
HeapMetadata
是TargetHeapMetadata
的别名
- 进入
TargetHeapMetadata
结构体 找到:
ConstTargetMetadataPointer<Runtime, TargetClassDescriptor> Description;
此时,发现 Description
是一个为 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
}
接着往下翻会发现TargetClassDescriptor
有个别名:ClassDescriptor
using ClassDescriptor = TargetClassDescriptor<InProcess>;
全局搜索:ClassDescriptor
,可以找到GenMeta.cpp
文件,这里就是生成元数据的地方
进入GenMeta.cpp
文件,定位到ClassContextDescriptorBuilder
,就是它,创建的metadata
和Descriptor
往下翻,看到layout()
方法
void layout() {
super::layout();
addVTable();
addOverrideTable();
addObjCResilientClassStubInfo();
}
它先调用了super::layout()
,那先看看它父类吧:
void layout() {
asImpl().computeIdentity();
super::layout();
asImpl().addName();
asImpl().addAccessFunction();
asImpl().addReflectionFieldDescriptor();
asImpl().addLayoutInfo();
asImpl().addGenericSignature();
asImpl().maybeAddResilientSuperclass();
asImpl().maybeAddMetadataInitialization();
}
它也调用了void layout()
:
void layout() {
asImpl().addFlags();
asImpl().addParent();
}
那行,串起来看,是不是似曾相识?
基本都对上了,也就是说,这个layout
,就是在构建TargetClassDescriptor
关注到addVTable()
函数来:
void addVTable() {
if (VTableEntries.empty())
return;
if (IGM.hasResilientMetadata(getType(), ResilienceExpansion::Minimal))
IGM.emitMethodLookupFunction(getType());
auto offset = MetadataLayout->hasResilientSuperclass()
? MetadataLayout->getRelativeVTableOffset()
: MetadataLayout->getStaticVTableOffset();
B.addInt32(offset / IGM.getPointerSize());
B.addInt32(VTableEntries.size());
for (auto fn : VTableEntries)
emitMethodDescriptor(fn);
}
前面的代码不用管,直接看到B.addInt32
,计算 offset
偏移量之后,添加到B
,B
还添加了vtable的size,然后遍历了VTableEntries数组
,调用emitMethodDescriptor(SILDeclRef fn)
函数 添加了函数指针fn
,这个B
就是当前TargetClassDescriptor
,也就是说,往B
里面添加内容,就是往TargetClassDescriptor
添加内容
2.1.4 在Mach-O中分析类方法
打开MachOView查看mach-o,查看一下虚拟内存的基地址:0x100000000
看到:Section64(__Text,__swift5_types)
这个section,这里存放的是结构体、枚举、类的 Descriptor
,前四个字节存的是类的Descriptor
,也就是说 90 FB FF FF
就是 CCTeacher
的 Descriptor
的地址信息,
那如何验证它存的就是地址信息?
拿它跟前面的四个字节,也就是在mach-o中的偏移量相加,就能得出这个Descriptor
在mach-o文件中的内存地址
由于这个地址是小端模式,所以从右往左读:FF FF FB 90
FFFFFB90
+ 0000BBCC
= 0x10000B75C
然后拿这个计算的结果,减去虚拟内存基地址,就可以得到Descriptor
在mach-o文件中的内存地址
0x10000B75C
- 0x100000000
= B75C
ok,到const段去找:
也就是说,从50
开始,找个地方里面存的就是Descriptor
的内容,也就是说50
是的TargetClassDescriptor
这个结构体的首地址,后面儿的内容应该要跟结构体的内容一一对应,既然如此,我对着TargetClassDescriptor
数12个4字节,直接找到vtable(因为vtable排第13):
2.1.5 通过程序来验证Mach-O的分析
上面的分析得出teach()
在mach-o对应的偏移地址是000B790
命令image list
这个 0x0000000100a8c000
就是程序运行的基地址也就是ASLR,就是这个项目加载进内存后,它的起始地址。
那我们拿这个程序运行的基地址加上teach()
方法在mach-o中的偏移量000B790
,就能得出teach()在内存中的地址
0x0000000100a8c000
+ 000B790
= 0x100A97790
也就是说0x100A97790
指向这个结构:
再来看一下源码,看 TargetMethodDescriptor
这个结构体,它表示的是swift方法的结构
Flags
标识这个方法是什么类型
class MethodDescriptorFlags {
public:
typedef uint32_t int_type;
enum class Kind {
Method,
Init,
Getter,
Setter,
ModifyCoroutine,
ReadCoroutine,
};
Impl
是一个相对指针,一个Offset
那也就是说我们刚才算出来的0x100A97790
指向这个TargetMethodDescriptor
这个结构,由于这个结构的首个成员是Flags
,所以要做一个偏移,Flags
是一个uint32
的枚举,所以是4字节,要偏移4字节,又由于Impl数一个相对指针,存的内容是一个offset,所以要找到方法的imp就还得偏移上它存的内容,总结:
方法地址 = 0x100A97790
+ flags
(4字节) + impl
impl内容是什么?
前面的4个字节是flags
,后面4个字节的内容就是impl
,那么impl
就是:FF FF C2 5C
0x100A97790
+ 4
+ FFFFC25C
= 0x200A939F0
最后再减去mach-o虚内存的基地址0x100000000
0x200A939F0
- 0x100000000
= 0x100A939F0
0x100A939F0
就是teach()函数地址
汇编打断点验证:
2.2 struct方法调度
直接把上面的类换成struct,跑个断点看看
struct CCTeacher{
func teach(){
print("teach")
}
func teach1(){
print("teach1")
}
func teach2(){
print("teach2")
}
}
class ViewController: UIViewController{
override func viewDidLoad() {
let t = CCTeacher()
t.teach()
t.teach1()
t.teach2()
}
}
好家伙,直接bl函数地址,也就是直接调用函数了。。
也就意味着,编译之后,函数地址就确定了。因为结构体没有继承关系,它没有必要另外开辟内存空间来另外记录每个函数所在地址,所以编译链接完函数地址就确定了,直接优化成静态调用
三、函数派发方式
3.1 final
两个点要注意:
- 被
final
修饰的类,不能够被继承 - 被
final
修饰的方法、下标、属性,禁止被重写,对于被被final
修饰的方法直接静态派发,不会添加到vtable
中,且对objc
运行时不可见。
3.2 dynamic
- 函数均可添加
dynamic
关键字,为非objc类和值类型的函数赋予动态性
,但派发方式还是函数表vtable
派发。 - 可跟
@_dynamicReplacement(for:)
配合使用
class CCTeacher {
dynamic func teach(){
print("teach")
}
}
extension CCTeacher {
//用teach3()替代teach()
@_dynamicReplacement(for:teach())
func teach3(){
print("teach3")
}
}
let t = CCTeacher()
t.teach()
//打印结果:teach3
3.3 @objc / @objc + dynamic
@objc
关键字可以将Swift函数
暴露给objc运行时
,从而使用runtime大法,如方法交换等,但依旧是函数表vtable派发,也就是说oc类依旧不能使用该swift函数。
class CCTeacher {
@objc dynamic func teach(){
print("teach")
}
}
- 要使oc类可以使用该swift函数,需要让swift类继承自
NSObject
@objc + dynamic
消息派发的方式,也就是oc中的消息传递。