一、异变方法
Swift 中 class 和 struct 都能定义方法。但是有一点区别的是默认情况下,struct内属性不能被自身的实例方法修改。
struct Point {
var x = 0.0, y = 0.0
func movePoint(x deltaX: Double, y deltaY: Double) {
x += deltaX
y += deltaY
}
}
编译运行,会看到错误警告“Left side of mutating operator isn't mutable: 'self' is immutable”。此时可以在方法前加“mutating”关键字来进行修改。
//添加 mutating关键字
struct Point {
var x = 0.0, y = 0.0
mutating func movePoint(x deltaX: Double, y deltaY: Double) {
x += deltaX
y += deltaY
}
}
我们通过生成的.sil文件来看下添加mutating后的方法与之前的方法有什么区别。(生成Swift sil文件的脚本命令:swiftc main.swift -emit-sil),可以使用Xcode打开生成的.sil文件。
1、未添加“mutating”关键字
2、添加关键字“mutating”关键字后
可以看到参数内都会有个Point,但是添加关键字“mutating”后Point前多了一个@inout
,@inout是什么呢,sil文档的说明:An @inout parameter is indirect. The address must be of an initialized object.(当前参数类型是间接的,传递的是已经初始化过的地址)。接着看箭头%5所指的地方,未添加“mutating”前是let self = Ponit,添加“mutating”后是var self = &Point。
通过以上分析,我们可以得出异变方法的本质:对于变异方法, 传入的 self 被标记为 inout 参数。无论在 mutating 方法 内部发生什么,都会影响外部依赖类型的一切。
二、输入输出参数
如果我们想函数能够修改一个形式参数的值,而且希望这些改变在函数结束之后依然生效,那么就需要将形式参数定义为输入输出形式参数。在形式参数定义开始的时候在前边 添加一个"inout"关键字可以定义一个输入输出形式参数:
var age = 26
func editeAge(age: inout Int)
{
//修改完后 会直接影响外部age的数值
age = 18;
}
三、方法调用
我们知道OC中的方法调用本质是objc_msgSend, 那Swift中的方法调用是个什么过程呢?
class Teacher{
func teach(){
print("teach")
}
func teach1(){
print("teach1")
}
func teach2(){
print("teach2")
}
}
我们看下生成的.sil文件
可以看到3个方法是在vtable中,我们打开Swift源码文件。metadata数据结构如下
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,就是对类的一个详细描述
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
}
1、打开Swift源码后我们先找到metadata
struct TargetClassMetadata : public TargetAnyClassMetadata<Runtime>
{
using StoredPointer = typename Runtime::StoredPointer;
using StoredSize = typename Runtime::StoredSize;
......
private:
/// An out-of-line Swift-specific description of the type, or null
/// if this is an artificial subclass. We currently provide no
/// supported mechanism for making a non-artificial subclass
/// dynamically.
//类的描述信息
TargetSignedPointer<Runtime, const TargetClassDescriptor<Runtime> * __ptrauth_swift_type_descriptor> Description;
....
}
2、点击进入类的描述信息“TargetClassDescriptor”内
class TargetClassDescriptor final
: public TargetTypeContextDescriptor<Runtime>,
public TrailingGenericContextObjects<TargetClassDescriptor<Runtime>, TargetTypeGenericContextDescriptorHeader,
.....
}
在这个类内部发现没有vtable,我们尝试内部搜索下“TargetClassDescriptor”,发现有个
using ClassDescriptor = TargetClassDescriptor<InProcess>;
3、全局搜索“ClassDescriptor”,定位“GenMeta.cpp”文件
class ClassContextDescriptorBuilder: public TypeContextDescriptorBuilderBase<ClassContextDescriptorBuilder,
ClassDecl>, public SILVTableVisitor<ClassContextDescriptorBuilder>
{
using super = TypeContextDescriptorBuilderBase;
ClassDecl *getType() {
return cast<ClassDecl>(Type);
}
// Non-null unless the type is foreign.
ClassMetadataLayout *MetadataLayout = nullptr;
Optional<TypeEntityReference> ResilientSuperClassRef;
SILVTable *VTable;
bool Resilient;
bool HasNonoverriddenMethods = false;
....
void layout() {
super::layout();
addVTable();
addOverrideTable();
addObjCResilientClassStubInfo();
maybeAddCanonicalMetadataPrespecializations();
}
}
layout先调用父类“TypeContextDescriptorBuilderBase”的layout创建Descriptor
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();
}
}
接着调用自己的vatable方法。 B代表当前的Descriptor,先设置vtable的size,接着遍历vatble,添加函数指针。
void addVTable() {
......
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);
}
经过以上分析,Descriptor加上偏移量就是方法的起始地址。下面我们可以通过分析macho来验证以上过程。
类,结构体,enum地址信息都存放在Section64(__TEXT__swift5_types)里,可以计算下0xFFFFFBF4 + 0xBC68 = 0x10000B85C
0x10000在Swift里是虚拟内存的地址
0xB85C 就是 Descriptor在data里的内存地址
该内存地址偏移13个4字节(TargetClassDescriptor类中的13个UInt32)得到vtable的地址 0xB890(macho中的地址),再加上ASLR得到实际地址
0x0000000100044000 + 0xB890 = 0x10004F890(teach函数的TargetMethodDescriptor地址)
struct TargetMethodDescriptor {
MethodDescriptorFlags Flags; // 4
TargetRelativeDirectPointer<Runtime, void> Impl; // offset
};
0x10004F890 + 0x4(Flags) = 0x10004F894 0x10004F894 + 0xFFFFC220 = 0x20004BAB4(0x10004BAB4 teach函数地址)
打开Debug-Debug Workflow-Always ShowDisassembly,运行程序。
汇编指令:
- mov: 将某一寄存器的值复制到另一寄存器(只能用于寄存器与寄存器或者寄存器 与常量之间传值,不能用于内存地址),如:
mov x1, x0 将寄存器x0的值 赋值到x1中
- b: 跳转到某地址(无返回)
- bl: 跳转到某地址(有返回)
- ldr: 将内存中的值读取到寄存器中,如:
ldr x0, [x1, x2] 将寄存器x1和寄存器x2的值相加作为地址,取该内存地址的值放入寄存器 x0中
teach函数的地址与我们通过分析macho文件得出的结论一致。我们以上是分析的类的方法存储及调用过程,那结构体的方法是否跟类的方法是一致的呢?
struct Teacher{
func teach(){
print("teach")
}
func teach1(){
print("teach1")
}
func teach2(){
print("teach2")
}
}
我们同样编译成sil文件发现无vtable信息,用汇编调试发现就是函数直接调用,跟类不同。struct无继承关系,编译完成时信息已确定。
4、影响函数派发方式
- final: 添加了 final 关键字的函数无法被重写,使用静态派发,不会在 vtable 中出现,且 对 objc 运行时不可⻅。
- dynamic: 函数均可添加 dynamic 关键字,为非objc类和值类型的函数赋予动态性,但派发方式还是函数表派发。
class Teacher{
dynamic func teach(){
print("teach")
}
}
extension Teacher
{
//替换teach,在调用teach或者teach3都会打印"teach3"
@_dynamicReplacement(for: teach)
func teach3(){
print("teach3")
}
}
- @objc: 该关键字可以将Swift函数暴露给Objc运行时,依旧是函数表派发。
- @objc + dynamic: 消息派发的方式
- extension: 类加extension后里面定义的方法不会存到vatble里,跟结构体内方法一样,直接调用。