异变方法
在Swift
中,值类型属性不能被自身的实例方法修改,编译器不会通过编译,报错Left side of mutating operator isn't mutable: 'self' is immutable
,自身是不能修改自身的。
当加上mutating
关键字后就可以通过编译
struct LLPerson {
var x = 0.0
var y = 0.0
//方法默认会有self参数,类似于OC中方法会有self和_cmd一样
func test() {
let temp = x
print(temp)
}
//默认会有参数self
mutating func moveBy(x deltaX: Double, y deltaY: Double) {
x += deltaX
y += deltaY
}
}
可以通过sil
来进行分析:
// LLPerson相当于self
// LLPerson.test()
sil hidden @$s4main8LLPersonV4testyyF : $@convention(method) (LLPerson) -> () {
// %0 "self" // users: %2, %1
bb0(%0 : $LLPerson):
//$ LLPerson, let, name "self" 是直接传入的值类型self
debug_value %0 : $LLPerson, let, name "self", argno 1 // id: %1
在test
方法中,相当于传入了值类型self
,而在moveBy
方法中
// 传入了@inout关键字的LLPerson,及@inout关键字的self
// LLPerson.moveBy(x:y:)
sil hidden @$s4main8LLPersonV6moveBy1x1yySd_SdtF : $@convention(method) (Double, Double, @inout LLPerson) -> () {
// %0 "deltaX" // users: %10, %3
// %1 "deltaY" // users: %20, %4
// %2 "self" // users: %16, %6, %5
bb0(%0 : $Double, %1 : $Double, %2 : $*LLPerson):
debug_value %0 : $Double, let, name "deltaX", argno 1 // id: %3
debug_value %1 : $Double, let, name "deltaY", argno 2 // id: %4
// 从这里可以看出,是*LLPerson类型的self,即LLPerson的指针
debug_value_addr %2 : $*LLPerson, var, name "self", argno 3 // id: %5
在moveBy
方法中传入了*LLPerson
,即指针self
。
SIL
文档的解释为:
An @inout parameter is indirect. The address must be of an initialized object.(当前参数 类型是间接的,传递的是已经初始化过的地址)
异变方法的本质:
对于变异方法,传入的 self
被标记为 inout
参数。无论在 mutating
方法内部发生什么,都会影响外部依赖类型的一切。
inout
输入输出参数:
如果我们想函数能够修改一个形式参数的值,而且希望这些改变在函数结束之后依然生效,那么就需要将形式参数定义为输入输出形式参数 。在形式参数定义开始的时候在前边添加一个 inout
关键字可以定义一个输入输出形式参数。
var person = LLPerson()
// 相当于传入了&self,即一个地址
person.moveBy(x: 30.0, y: 20)
我们可以在函数中看这个inout
:
var age = 10
//添加了inout
func modifyage(_ age: inout Int) {
// 如果使用temp,只是一个普通的赋值,不会改变外部age
var temp = age
temp += 1
}
// 必须使用&age
modifyage(&age)
print(age)
当我们这样时:
var age = 10
// 相当去传入了&self
func modifyage(_ age: inout Int) {
age += 1
}
modifyage(&age)
print(age)
相当于我们在写C
函数时:
void test(int a) {
a += 1;
}
int main(){
int a = 10; //定义值类型int
test(a); //传入值类型
printf("%d\n", a);
return 0;
}
我们修改不了a
的值。
但当我们这样时:
void test(int *a) {
*a += 1;
}
int main(){
int a = 10;
test(&a); // 我们传入一个地址,即&a
printf("%d\n", a);
return 0;
}
我们是可以修改a
的值的。
方法调度
在OC
中,方法调度是objc_mgsend
的方式。而在Swift
中,我们可以通过代码和汇编来探究。
class LLTeacher {
func teach() {
print("teach")
}
func teach1() {
print("teach1")
}
func teach2() {
print("teach2")
}
}
let teacher = LLTeacher()
teacher.teach()
teacher.teach1()
teacher.teach2()
我们通过断点调试,获取相关汇编代码
0x1026d7888 <+116>: bl 0x1000077c4 ; _23456.LLTeacher.__allocating_init() -> _23456.LLTeacher at ViewController.swift:10
// __allocating_init返回LLTeacher 返回值存储到x0寄存器中 x0的第一个8字节存储的是metadata
0x1026d788c <+120>: str x0, [sp, #0x20] // 将x0的值保存到栈中
0x1026d7890 <+124>: ldr x8, [x0] // 将x0的地址给x8
0x1026d7894 <+128>: ldr x8, [x8, #0x50] // x8加上0x50个字节的地址给x8 即metadate + 0x50就是 teach 这时候拿到了teach
0x1026d7898 <+132>: mov x20, x0
0x1026d789c <+136>: str x0, [sp, #0x8]
0x1026d78a0 <+140>: blr x8 // 执行teach
0x1026d78a4 <+144>: ldr x8, [sp, #0x8]
0x1026d78a8 <+148>: ldr x9, [x8]
0x1026d78ac <+152>: ldr x9, [x9, #0x58] // 0x50 0x58 相差8字节,正好是指针8字节,所以这些函数时连续的内存空间
0x1026d78b0 <+156>: mov x20, x8
0x1026d78b4 <+160>: blr x9 // 执行teach1
0x1026d78b8 <+164>: ldr x8, [sp, #0x8]
0x1026d78bc <+168>: ldr x9, [x8]
0x1026d78c0 <+172>: ldr x9, [x9, #0x60]
0x1026d78c4 <+176>: mov x20, x8
0x1026d78c8 <+180>: blr x9 // 执行teach2
0x1026d78cc <+184>: ldr x0, [sp, #0x8]
0x1026d78d0 <+188>: bl 0x10000992c ; symbol stub for: swift_release
寄存器x0
读取出来,正好是metadata
。
通过读取x8
寄存器的值,可以得出,位置为teach
方法
汇编相关资料可以参考:常用指令
所以我们可以猜测:teach
函数的调用过程是找到 Metadata
,确定函数地址(metadata
+ 偏移量),然后执行函数,是基于函数表的调度。
我们还可以通过sil
查看一些相关内容:
class LLTeacher {
func teach()
func teach1()
func teach2()
@objc deinit
init()
}
sil_vtable LLTeacher { // 一种vtable的形式存储
#LLTeacher.teach: (LLTeacher) -> () -> () : @$s14ViewController9LLTeacherC5teachyyF // LLTeacher.teach()
#LLTeacher.teach1: (LLTeacher) -> () -> () : @$s14ViewController9LLTeacherC6teach1yyF // LLTeacher.teach1()
#LLTeacher.teach2: (LLTeacher) -> () -> () : @$s14ViewController9LLTeacherC6teach2yyF // LLTeacher.teach2()
#LLTeacher.init!allocator: (LLTeacher.Type) -> () -> LLTeacher : @$s14ViewController9LLTeacherCACycfC // LLTeacher.__allocating_init()
#LLTeacher.deinit!deallocator: @$s14ViewController9LLTeacherCfD // LLTeacher.__deallocating_deinit
}
一种vtable
的形式存储。
同时,我们可以从源码来看一下相关的结构。上一章节我们获取到Metdata
的一个结构:
struct Metadata { //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
,就是对类的一个详细描述。我们通过对源码的分析,可以得出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-Table //V-Table位置
}
下面我们通过mach-o
文件来查看V-Table
的位置。
我们通过MachOView
打开mach-o
文件:
下面我们通过内存地址,来查找V-Table
:
首先,我们拿到存放Swift
类的前四个字节地址0xFFFFFBA8
+ 0x0000BBCC
0xFFFFFBA8
+ 0x0000BBCC
------------------
0x10000B774 //通过这个虚拟内存地址就可以查找到Descriptor的位置
接着我们通过0x10000B774
减去虚拟内存基地址0x100000000
拿到Descriptor
的位置
0x10000B774
+ 0x100000000
------------------
0xB774 //Descriptor的位置
下面,我们通过MachOView
查找到0xB774
(Descriptor
)的位置:
在接下来,我们通过Descriptor
的结构,去标记V-Table
的位置:
这时候我们获取了V-Table
的位置,即teach
的地址0x0000B7A8
。
我们通过0x0000B7A8
+ ASLR
(随机偏移地址,即程序运行的基地址)。
通过image list
获取程序运行的基地址是0x0000000104790000
:
0x0000B7A8
+ ASLR
,我们可以获取teach
函数在内存中的地址:
0x0000B7A8
+ 0x0000000104790000
----------------------
0x10479B7A8 //teach函数在内存中的地址
我们通过源码,可以获取函数在内存中的结构为:
struct TargetMethodDescriptor {
MethodDescriptor Flags; //4字节
//存储的offset
TargetRelativeDriectPointer<Runtime, void> Impl;
}
接下来,我们通过0x10479B7A8
(TargetMethodDescriptor
的地址) + 0x4
( Flags
) + 0xFFFFC234
(Impl offset
),就可以得到teach
的函数地址。
0x10479B7A8
0x4
+ 0xFFFFC234
-----------------------
0x2047979E0
- 0x100000000 //还需要减去虚拟内存基地址0x100000000
-----------------------
0x1047979E0 //teach的函数地址
我们通过上面的汇编,可以读取x8
寄存器里面的内容,就是我们计算的地址:
Mahco
的一些知识:
Mahco
:Mach-O
其实是Mach Object
文件格式的缩写,是mac
以及iOS
上可执行文件的格 式, 类似于windows
上的PE
格式 (Portable Executable
),linux
上的elf
格式 (Executable and Linking Format
) 。常见的.o,.a .dylib Framework,dyld .dsym
。
Macho
文件格式:
首先是文件头,表明该文件是Mach-O
格式,指定目标架构,还有一些其他的文件属性信 息,文件头信息影响后续的文件结构安排
Load commands
是一张包含很多内容的表。内容包括区域的位置、符号表、动态符号表等。
Data
区主要就是负责代码和数据记录的。Mach-O
是以Segment
这种结构来组织数据 的,一个Segment
可以包含0
个或多个Section
。根据Segment
是映射的哪一个Load Command
,Segment
中section
就可以被解读为是是代码,常量或者一些其他的数据类 型。在装载在内存中时,也是根据Segment
做内存映射的。
当我们将class
改为struct
时
struct LLTeacher {
func teach() {
print("teach")
}
func teach1() {
print("teach1")
}
func teach2() {
print("teach2")
}
}
let teacher = LLTeacher()
teacher.teach()
teacher.teach1()
teacher.teach2()
通过汇编我们发现,结构体的函数调用是直接静态派发,即编译的时候就已经确定了地址。
当我们使用extension
时,调用方式也是使用的静态派发,包括类的extension
也是如此。
extension LLPerson {
func teach3() {
print("teach3")
}
}
这样做的好处是,不需要再V-Table
中插入extension
中的方法,减少消耗。
最后总结方法调度方式:
类型 | 调度方式 | extension |
---|---|---|
值类型 | 静态派发 | 静态派发 |
类 | 函数表派发 | 静态派发 |
NSObject子类 | 函数表派发 | 静态派发 |
影响函数派发方式
final
:添加了 final
关键字的函数无法被重写,使用静态派发,不会在vtable
中出现,且对 objc
运行时不可见。
final func teach() {
print("teach")
}
dynamic
:函数均可添加 dynamic
关键字,为非objc
类和值类型的函数赋予动态性,但派发方式还是函数表派发。配合@dynamicReplacement(for:)
使用
@objc
:该关键字可以将Swift
函数暴露给Objc
运行时,依旧是函数表派发。
@objc + dynamic
:消息派发的方式
class LLPerson: NSObject {
@objc dynamic func teach() {
print("teach")
}
}
会暴露函数给OC
使用,可以使用methed-swizzing
等
SWIFT_CLASS("_TtC9_234567898LLPerson")
@interface LLPerson : NSObject
- (void)teach;
- (nonnull instancetype)init OBJC_DESIGNATED_INITIALIZER;
@end