一、异变方法
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中的消息传递。