异变方法
Swift中 类和结构体都能定义方法,但默认情况下值类型不允许修改实例本身
上面代码编译器会报错。如果真的想修改本身。需要加上mutating关键字
extension Int {
mutating func square() {
self = self * self
}
/**
*以下代码会报错
*原因:结构体不能自己改自己。必须在函数前面加mutating变成了 异变方法
*底层:会在参数里面加上inout修饰
*把地址传递变成了值传递
*/
// func square() {
// self = self * self
// }
}
如果需要修改值本身,需要添加mutating关键字。添加完mutating后,查看SIL,会发现对self参数加上@inout修饰
SIL 文档的解释
An @inout parameter is indirect. The address must be of an initialized object.(当前参数类型是间接的,传递的是已经初始化过的地址)
异变方法的本质:对于变异方法, 传入的 self 被标记为 inout 参数。无论在 mutating 方法 内部发生什么,都会影响外部依赖类型的一切。
inout 输入输出参数:如果我们想函数能够修改一个形式参数的值,而且希望这些改变在函数结束之后依然生效,那么就需要将形式参数定义为输入输出形式参数。在形式参数定义开始的时候在前边添加一个inout关键字可以定义一个输入输出形式参数。代码如下:
func inoutMethod() {
let age1 = 10;
modifyAge(age1);
var age2 = 10;
modifyAge(&age2);
print(age1,age2)
//函数形式参数默认为let,需要转化为var,再进行计算
func modifyAge(_ age: Int) {
var temp = age;
temp = temp + 1;
}
func modifyAge(_ age: inout Int) {
age = age + 1;
}
}
输出为
10 11
可以看出,加上inout修饰,函数内的改变结果,在函数结束之后依然生效。
方法调度
我们知道在OC中,方法调度是通过msgSend来实现动态调度的。在swift中方法是如何调度的,我们可以通过汇编调试来看一看。
swift代码
class OSDogModel: NSObject {
func sleep() {
print("dog will go to sleep")
}
}
class OSSwiftMethodController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor.white
self.dogSleep()
}
func dogSleep() {
let dog = OSDogModel()
dog.sleep()
}
}
在dog.sleep()这行下断点,开启汇编调试。(本文截图机型为M1)
在看汇编代码前,可以简单的认识几个汇编指令,就可以大致了解以上汇编内容
- mov:将某一寄存器的值复制到另一寄存器(只能用于寄存器与寄存器或者寄存器与常量之间传值,不能用于内存地址),如
mov x1, x0 将寄存器x0的值符知道寄存器x1中
- ldr:将内存中的值读取到寄存器中,如:
ldr x0, [x1, x2] 将寄存器x1和寄存器x2的值相加作为地址,取改地址的值放入寄存器x0中
- bl、blr:跳转到某地址(有返回)
x代表寄存器,x0用来存放函数计算结果
mov x20, x0x0里面放的是刚刚新建好的dog对象。把dog对象放到x20中。
str x20 [sp, #0x8]
str x20 [sp, #0x10]
这两句先忽略,不影响正常流程
ldr x8, [x20]
把dog的metaData放入x8中。x20是dog对象,取dog对象的地址,并且把前8字节放入x8中。而dog对象的地址的前8字节正好放的就是metaDate。
通过register read 可以不断的打印寄存器里面的只。看到这串字符,可以通过xcrun来还原
$ xcrun swift-demangle _TtC11ObjectStudy10OSDogModel
_TtC11ObjectStudy10OSDogModel ---> ObjectStudy.OSDogModel
最后得到我们的类名。而这时候在x8寄存器中存放的地址,应该是我们 ObjectStudy.OSDogModel 的metaData,在这之后又做了 ldr 0x50 计算,也就是从ObjectStudy.OSDogModel 的metaData之后偏移了 0x50个地址。而这个地址是sleep 函数的地址
到这里我们大概理解了swift中函数调用分为了3个步骤
- 找到metadata
- 确定函数地址(metadata + 偏移量)
- 执行函数
接下来,我们给DogModel增加几个方法,并且调用。
class OSDogModel: NSObject {
func sleep() {
print("dog will go to sleep")
}
func eat() {
print("dog will go to eat")
}
func catchMouse() {
print("dog will go to catch mouse")
}
}
继续来到汇编断点中
不难发现这个偏移量是个等差数列,差值正好是8,这个正好是函数地址的大小。那就可以大胆的猜想,这三个函数的地址是放在连续的地址中,存放在一个list中。
我们可以通过swiftc工具,把OSDogModel这个类转换成SIL文件,看看swift中间语言是怎么实现的。
swiftc -emit-sil OSDogModel.swift >osdog.sil
在sil文件中可以看到代码
sil_vtable OSDogModel {
#OSDogModel.sleep: (OSDogModel) -> () -> () : @$s10OSDogModelAAC5sleepyyF // OSDogModel.sleep()
#OSDogModel.eat: (OSDogModel) -> () -> () : @$s10OSDogModelAAC3eatyyF // OSDogModel.eat()
#OSDogModel.catchMouse: (OSDogModel) -> () -> () : @$s10OSDogModelAAC10catchMouseyyF // OSDogModel.catchMouse()
#OSDogModel.init!allocator: (OSDogModel.Type) -> () -> OSDogModel : @$s10OSDogModelAACABycfC // OSDogModel.__allocating_init()
#OSDogModel.deinit!deallocator: @$s10OSDogModelAACfD // OSDogModel.__deallocating_deinit
}
目前看到的都是一系列预测和猜想,可以通过Mach-O文件来证实这些猜想。
tips:Mahco:Mach-O其实是MachObject文件格式的缩写,是mac以及iOS上可执行文件的格式,类似于windows上的PE格式(PortableExecutable),linux上的elf格式(ExecutableandLinkingFormat)。常⻅的.o,.a.dylib Framework,dyld.dsym。 mahco文件格式如下
mahco文件主要由3部分组成
- 首先是文件头,表明该文件是Mach-O格式,指定目标架构,还有一些其他的文件属性信息,文件头信息影响后续的文件结构安排
- Loadcommands是一张包含很多内容的表。内容包括区域的位置、符号表、动态符号表等。
- Data区主要就是负责代码和数据记录的。Mach-O是以Segment这种结构来组织数据的,一个Segment可以包含0个或多个Section。根据Segment是映射的哪一个LoadCommand,Segment中section就可以被解读为是是代码,常量或者一些其他的数据类型。在装载在内存中时,也是根据Segment做内存映射的。例如__Text,__text Assembly 里面存放汇编指令,OC的类都记录在__Data,__objc_classlist中。
可以通过烂苹果(MachOView)来查看Mach-O文件。找到app的可执行文件,并拖入到MachOview中。
在swift5__types中找到OSDogModel的地址。顺序为编译顺序,每4个字节是一个类或者结构体。我的是在第三个。
0x32F24 + 0xFFFF7EB4 = 0x10002ADD8
这个值需要减去一个基地址,基地址可以在LoadCommands中找到
0x10002ADD8 -0x100000000 = 0x2ADD8
我们可以在__const中找到0x2ADD8对应的内容。
右边的OSDogModel的提示已经证实找对了地方。
在上文中我们解了
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
}
而现在,0x2ADD8所指向的就是OSDogModel的typeDescriptor,而他的数据结构大致如下
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-Tabl
}
每个Uint32占4个字节。我们要找到V-tab需要向后偏移13个4字节,我们来到0x2AE0C。而V-Table里面存放的是TargetMethodDescriptor,其数据结构如下:
tip:方法放在v-tab中是为了继承。所以需要继承的方法都会放在v-tab中(对象方法、类方法)、而在extention中定义的方法不能被继承,所以也不在v-tab中。
struct TargetMethodDescriptor {
/// Flags describing the method.
MethodDescriptorFlags Flags;
/// The method implementation.
TargetRelativeDirectPointer<Runtime, void> Impl;
// TODO: add method types or anything else needed for reflection.
};
也就是说sleep 前4个字节是存放Flogs,后4个字节存放Impl,Impl的地址是0x2A1E0.
可以先看Flags的定义,其实这个就是一个枚举值,来标记方法类型的。
class MethodDescriptorFlags {
public:
typedef uint32_t int_type;
enum class Kind {
Method,
Init,
Getter,
Setter,
ModifyCoroutine,
ReadCoroutine,
};
private:
enum : int_type {
KindMask = 0x0F, // 16 kinds should be enough for anybody
IsInstanceMask = 0x10,
IsDynamicMask = 0x20,
IsAsyncMask = 0x40,
ExtraDiscriminatorShift = 16,
ExtraDiscriminatorMask = 0xFFFF0000,
};
int_type Value;
}
目前这三个方法的flags都是0x10,对应的枚举是IsInstanceMask,顾名思义是对象方法的意思。
再看真正的函数地址,需要用impl的内存地址+偏移量。而在程序正真运行的时候,还要➕上随机偏移地址(aslr)。
aslr 可以在lldb中通过 image list获取,如下:
sleep函数地址 = 0x2A1E0 + 0xFFFFBEB0 + 0x000000010420c000 - 0x100000000 = 0x104232CC0
这是通过手动计算的结果。我们再用汇编断点来看看寄存器内的地址,如果一样就能证实以上的猜想。运行输出如下:
可以看到,函数地址和我们计算的一模一样!。如果觉得这只是巧合,可以同理计算
eat()函数catchMouse()函数的地址。
影响函数派发方式
- final: 添加了 final 关键字的函数无法被重写, 使用静态派发, 不会在vtable中出现, 且对objc运行时不可见. 如果在实际开发过程中, 属性,方法, 类不需要被重载的时候, 可以添加final关键字.
- dynamic: 函数均可添加 dynamic 关键字, 为非objc类和值类型的函数赋予动态性,但派发方式还是函数表派发.
- @objc: 该关键字可以将swift函数暴露给Objc运行时, 依旧是函数表派发.
- @objc + dynamic: 消息派发的方式.
函数内联
函数内联是一种编译器优化技术,它通过使用方法的内容替换直接调用方法,从而优化性能. 在xcode里面可以看到编译器的这一选项,默认情况下debug,是None的,Release是Fastest,Smallest(最快最小)如图:
swift的如图:
- 将确保有时内联函数.这是默认行为,我们无需执行任何操作.Swift编译器可能会自动内联函数作为优化.
- always - 将确保始终内联函数.通过在函数前添加 @inline(_always)来实现此行为
- never - 将确保永远不会内联函数. 这可以通过在函数前添加 @inline(never)来实现.
- 如果函数很长并且想避免增加代码段大小,请使用 @inline(never) (使用@inline(never))
/**
内敛函数
*/
@inline(never) func neverInline() {
}
@inline(always) func alwaysInline() {
}