一、异变方法
1 mutating关键字
上篇文章中,了解到 Swift 中 class 和 struct 都能定义方法。但是有一点区别的是默认情况下,值类型属性不能被自身的实例方法修改。
可得出,
self不可修改,因为当前x和y属于self
解决方式
在func关键字前加mutating,就可允许修改行为
- 思考🤔:不添加
mutating访问与添加mutating访问两者有什么本质区别❓
1.1 通过生成SIL文件分析
main.swift文件添加测试代码
struct Point {
var x = 0.0,
y = 0.0
func test(){
let tmp = self.x
print(tmp)
}
mutating func moveBy(x deltaX: Double, y deltaY: Double){
x += deltaX
y += deltaY
}
}
- 生成
SIL文件命令:
swiftc -emit-sil main.swift >> main.sil
得到main.sil文件,通过sil文件对比分析如下
test函数
// Point.test()
sil hidden @$s4main5PointV4testyyF : $@convention(method) (Point) -> () {
// %0 "self" // users: %2, %1
bb0(%0 : $Point):
debug_value %0 : $Point, let, name "self", argno 1 // id: %1
......
}
moveBy函数
// Point.moveBy(x:y:)
sil hidden @$s4main5PointV6moveBy1x1yySd_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
......
}
1.2 对比分析
test函数默认参数是Point, 接收的是结构体的实例(self,也就是值)- 赋值过程:
let self = Poit
- 赋值过程:
- moveBy的默认参数是
@inoutPoint, 接收的是地址- 赋值过程:
var self = &Poit
- 赋值过程:
注意:
let声明是 不可变的
var声明是 可变的
1.3 实例代码分析
var p = Point()
// let self = Poit
var x1 = p
// var sefl = &Poit
var x2 = withUnsafePointer(to: &p) { return $0}
var x3 = p
p.x = 30.0
print(x2.pointee.x)
print(x3.x)
输出结果:
30.0
0.0
- 根据结果可得出修改
Point的值:x2可修改,x3不可修改
mutating(异变方法)的本质: 对于变异方法, 传入的self被标记为inout参数。无论在mutating方法 内部发生什么,都会影响外部依赖类型的一切。
2. inout输入输出参数
2.1 inout SIL文档解释
An @inout parameter is indirect. The address must be of an initialized object.(当前参数 类型是间接的,传递的是已经初始化过的地址)
实例代码分析
var age = 10
func modifyage(_ age: inout Int) {
age += 1
}
modifyage(&age)
print(age)
输出结果:
11
inout地址的传递
如果我们想函数能够修改一个形式参数的值,而且希望这些改变在函数结束之后 依然生效,那么就需要将形式参数定义为 输入输出形式参数 。在形式参数定义开始的时候在前边 添加一个 inout关键字可以定义一个输入输出形式参数
二、方法调度
回顾OC语言,调用方法的本质是消息传递,底层是通过objc_mgsend消息机制的方法去调度
补充:常见汇编指令
- mov: 将某一寄存器的值复制到另一寄存器(只能用于寄存器与寄存器或者寄存器 与常量之间传值,不能用于内存地址),
mov x1, x0 将寄存器 x0 的值复制到寄存器x1中
- add: 将某一寄存器的值和另一寄存器的值 相加 并将结果保存在另一寄存器中
add x0, x1, x2 将寄存器x1 和 x2 的值相加后保存到寄存器 x0 中
- sub: 将某一寄存器的值和另一寄存器的值 相减 并将结果保存在另一寄存器中
sub x0, x1, x2 将寄存器 x1 和 x2 的值相减后保存到寄存器 x0 中
- and: 将某一寄存器的值和另一寄存器的值 按位与 并将结果保存到另一寄存器中
and x0, x0, #0x1 将寄存器 x0 的值和常量 1 按位与后保存到寄存器 x0 中
- orr: 将某一寄存器的值和另一寄存器的值 按位或 并将结果保存到另一寄存器中
orr x0, x0, #0x1 将寄存器 x0 的值和常量 1 按位或 并将结果保存到寄存器 x0 中
- str : 将寄存器中的值写入到内存中
str x0, [x0, x8] ; 将寄存器 x0 中的值保存到栈内存 [x0 + x8] 处
- ldr: 将内存中的值读取到寄存器中,
ldr x0, [x1, x2] 将寄存器 x1 和寄存器 x2 的值相加作为地址,去改地址的值放入寄存器 x0 中
- cbz: 和 0 比较,如果结果为零就转移(只能跳到后面的指令)
- cbnz: 和非 0 比较,如果结果非零就转移(只能跳到后面的指令)
- cmp: 比较指令
- blr: (branch)跳转到某地址(无返回)
- bl: 跳转到某地址(有返回)
- ret: 子程序(函数调用)返回指令,返回地址已默认保存在寄存器 lr (x30) 中
2.1 汇编分析
新建Swift项目。定义LGTeacher类,定义teach函数。打上符号断点如下:
-
运行查看汇编断点如下
-
汇编分析,从初始化开始
- 调用函数的初始化的指令
bl, 有返回值。返回的是LGTeacher实例对象,函数的返回值放在x0寄存器中 - x0的第一个8字节是: medata
- x8 + (0x50)偏移量 函数的调用地址
- 调用函数的初始化的指令
-
可得出,函数调用前都有偏移量的操作:
ldr x8, [x8, #0x50]、ldr x8, [x8, #0x58]、ldr x8, [x8, #0x60]
teach函数的调用过程: 找到
Metadata基于函数表的调度,确定函数地址(metadata + 偏移量), 执行函数
2.2 SIL验证
命令生成ViewController.sil文件
swiftc -emit-silgen -Onone -target x86_64-apple-ios13.3-simulator -sdk $(xcrun --show-sdk-path --sdk iphonesimulator) ViewController.swift > ./ViewController.sil
滑到ViewController.sil文件的最底部
sil_vtable LGTeacher {
#LGTeacher.teach: (LGTeacher) -> () -> () : @$s14ViewController9LGTeacherC5teachyyF // LGTeacher.teach()
#LGTeacher.teach1: (LGTeacher) -> () -> () : @$s14ViewController9LGTeacherC6teach1yyF // LGTeacher.teach1()
#LGTeacher.teach2: (LGTeacher) -> () -> () : @$s14ViewController9LGTeacherC6teach2yyF // LGTeacher.teach2()
#LGTeacher.init!allocator: (LGTeacher.Type) -> () -> LGTeacher : @$s14ViewController9LGTeacherCACycfC // LGTeacher.__allocating_init()
#LGTeacher.deinit!deallocator: @$s14ViewController9LGTeacherCfD // LGTeacher.__deallocating_deinit
}
sil_vtable ViewController {
#ViewController.deinit!deallocator: @$s14ViewControllerAACfD // ViewController.__deallocating_deinit
}
vtable 叫函数表,sil_vtable含有类中的所以函数。
2.3 swift 源码分析 V-table
之前在上一篇章讲到了 Metdata 的数据结构,那么 V-Table 是存放在什么地方那? 我们先来回顾一下当前的数据结构
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 ,就是对类的一个详细描述
-
打开Swift 源码分析
-
打开
Metadata.h文件 -
找到
TargetClassMetadata结构体 -
在内部找到
Description变量
点击 TargetClassDescriptor 进入结构体,搜索 TargetClassDescriptor,找到别名(ClassDescriptor)如下:
using ClassDescriptor = TargetClassDescriptor<InProcess>;
全局搜索ClassDescriptor别名,进入 GenMeta.cpp文件,定位到 ClassContextDescriptorBuilder类,这个类是描述建立在,创建metadata和Descriptor类的地方.
进入layout方法
void layout() {
assert(!getType()->isForeignReferenceType());
super::layout();
addVTable();
addOverrideTable();
addObjCResilientClassStubInfo();
maybeAddCanonicalMetadataPrespecializations();
}
在函数中实现了父类,点击super::layout()进入父类实现:
void layout() {
asImpl().computeIdentity();
super::layout();
asImpl().addName();
asImpl().addAccessFunction();
asImpl().addReflectionFieldDescriptor();
asImpl().addLayoutInfo();
asImpl().addGenericSignature();
asImpl().maybeAddResilientSuperclass();
asImpl().maybeAddMetadataInitialization();
}
到此已推出部分的 TargetClassDescriptor类成员变量。返回子类进入addVTable函数,如下:
void addVTable() {
LLVM_DEBUG(
llvm::dbgs() << "VTable entries for " << getType()->getName() << ":\n";
for (auto entry : VTableEntries) {
llvm::dbgs() << " ";
entry.print(llvm::dbgs());
llvm::dbgs() << '\n';
}
);
// Only emit a method lookup function if the class is resilient
// and has a non-empty vtable, as well as no elided methods.
if (IGM.hasResilientMetadata(getType(), ResilienceExpansion::Minimal)
&& (HasNonoverriddenMethods || !VTableEntries.empty()))
IGM.emitMethodLookupFunction(getType());
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);
}
addVTable函数分析:offset计算偏移量,调用addInt32添加偏移量到B(Descriptor);最后for循环添加函数的指针。
最终得出 TargetClassDescriptor结构如下:
struct TargetClassDescriptor{
var flags: UInt32
var parent: UInt32
var name: Int32
var accessFunctionPointer: Int32
var fieldDescriptor: Int32
var superClassType: Int32
var metadataNegativeSizeInWords: UInt32
metadataPositiveSizeInWords: UInt32
var numImmediateMembers: UInt32
var numFields: UInt32
var fieldOffsetVectorOffset: UInt32
var Offset: UInt32
var size: UInt32
//V-Table
}
2.4 什么是Mahco ❓
Mach-O 其实是Mach Object文件格式的缩写,是 mac 以及 iOS 上可执行文件的格 式, 类似于 windows 上的 PE 格式 (Portable Executable ), linux 上的 elf 格式 (Executable and Linking Format) 。常⻅的 .o、 .a、 .dylib、 Framework、 dyld 、 .dsym。
Mahoc文件格式:
2.4.1 Header(头文件)
首先是文件头,表明该文件是 Mach-O 格式,指定目标架构,还有一些其他的文件属性信 息,文件头信息影响后续的文件结构安排
2.4.2 Load commands
Load commands是一张包含很多内容的表。内容包括区域的位置、符号表、动态符号表 等。
2.4.3 Data数据
Data区主要就是负责代码和数据记录的。Mach-O 是以 Segment 这种结构来组织数据 的,一个 Segment 可以包含 0 个或多个 Section。根据 Segment 是映射的哪一个 Load Command,Segment 中 section 就可以被解读为是是代码,常量或者一些其他的数据类型。在装载在内存中时,也是根据 Segment 做内存映射的。
2.2.4 MachOView工具展示项目Mach-O文件格式
- 打开项目Products 目录
- 显示包内容
- 拖入MachOView工具打开
Mach-O文件格式如下
2.5 class 函数 Mach-O 文件分析
执行 class 函数源码
class LGTeacher{ // 继承关系吗?没有
func teach() {
print("teach")
}
func teach1(){
print("teach1")
}
func teach2(){
print("teach2")
}
}
class ViewController: UIViewController{
override func viewDidLoad() {
let t = LGTeacher()
t.teach()
t.teach1()
t.teach2()
}
}
2.5.1 查找函数在 Mach-O文件中的地址
__swift5_types 存放的是Class、Struct、Enum的Descriptor
前4个字节F4 FB FF FF就是LGTeacher的Descriptor信息, F4 FB FF FF加上pFile字段下的 0000BC58得到的就是Descriptor在Mach-O文件中的地址信息
由于当前ios是小端模式,需从右往左读;Descriptor地址信息相加等式:
FFFFFBF4+0000BC58 = 0x10000B84C
得出Descriptor的地址信息为 0x10000B84C, 0x100000000是 Mach-O 文件中虚拟内存的基地址
0x10000B84C - 0x100000000 = 0xB84C,0xB84C 为 LGTeacher的首地址,后面是 TargetClassDescriptor 结构体里面的内容
其中TargetClassDescriptor结构size前有12个字段,加上size13个,往后就是V-Table中teach、teach1、teach2方法结构体地址, 定位如下图所示:
最终teach函数在Mach-O文件偏移量为B880
2.5.2 验证当前函数Mach-O中文件地址在程序运行中的地址
teach()在程序中的运行地址:B880 + ASLR(随机偏移地址)
断点执行函数,执行lldb 函数 image list得到 ASLR(程序运行的基地址) 0x0000000104da8000
得出teach()函数的结构地址为0x0000000104da8000 + B880 = 0x104DB3880,下图所示:
在源码中找到TargetMethodDescriptor结构体,是 Swift 的方法在内存中的结构
template <typename Runtime>
struct TargetMethodDescriptor {
/// Flags describing the method.
// 4 字节
MethodDescriptorFlags Flags;
/// The method implementation.
// offset
TargetRelativeDirectPointer<Runtime, void> Impl;
// TODO: add method types or anything else needed for reflection.
};
Impl地址:0x104DB3880+0x4+0xFFFFB9D4=0x204DAF258- 0x4: Flags
- 0xFFFFB9D4: Offset (
D4 B9 FF FF) 小端模式从右往左
teach函数的地址为:0x204DAF258-0x100000000=0x104DAF258- 0x100000000: Mach-O 文件中虚拟内存的基地址
打开汇编调试模式,读取汇编中的teach函数地址,如下图所示:
最终得出-> 0x104DAF258是teach函数的地址,V-Table 是在 Descriptor 结构的后面,并且 Swift 类的方法是存放在 V-Table函数表中。
؏؏☝ᖗ乛◡乛ᖘ☝؏؏
2.5.3 为啥 MethodData + Offset (偏移操作)?
函数存储到vtable后,进行了
vtableOffset偏移,所以取的时候也要进行偏移
2.6 struct 函数调度
struct LGTeacher{ // 继承关系吗?没有
func teach() {
print("teach")
}
func teach1(){
print("teach1")
}
func teach2(){
print("teach2")
}
}
class ViewController: UIViewController{
override func viewDidLoad() {
let t = LGTeacher()
t.teach()
t.teach1()
t.teach2()
}
}
汇编调试结果:
struct函数调用,编译完成之后,当前函数内的地址已经确定了,就是
静态派发
2.7 struct 的 extension 函数调度
struct LGTeacher 添加 extension
struct LGTeacher{
......
}
extension LGTeacher {
func teach3(){
print("teach2")
}
}
汇编调试结果:
teach3 直接是地址调用,extension 在 struct 也是 静态派发
2.8 class 的 extension 函数调度
class LGTeacher 添加 extension实现类
class LGTeacher{
......
}
extension LGTeacher {
func teach3(){
print("teach2")
}
}
汇编调试结果:
teach3 没有在 V-Table函数表中,而是直接地址调用,extension 在 class 也属于静态派发
方法调度方式总结
| 类型 | 调度方式 | extension |
|---|---|---|
| 值类型 | 静态派发 | 静态派发 |
| 类 | 函数表派发 | 静态派发 |
| NSObject子类 | 函数表派发 | 静态派发 |
三、影响函数派发方式
3.1 final关键字
- final: 添加了 final 关键字的函数无法被重写,使用静态派发,不会在 vtable 中出现,且 对 objc 运行时不可⻅。
汇编调试结果:
SIL分析(无teach函数):
添加了final关键字, 从sil_vtable中移除掉了,优化成了直接调用
实际开发过程中属性,方法,类不需要被重载,可使用final
3.2 dynamic关键字
- dynamic: 函数均可添加 dynamic 关键字,为非objc类和值类型的函数赋予动态性,但派发 方式还是函数表派发。
class LGTeacher{ // 继承关系吗?没有
dynamic func teach() {
print("teach")
}
}
extension LGTeacher {
@_dynamicReplacement(for: teach)
func teach3(){
print("teach3")
}
}
class ViewController: UIViewController{
override func viewDidLoad() {
let t = LGTeacher()
t.teach()
}
}
输出结果:
teach3
3.3 @objc关键字
- 该关键字可以将Swift函数暴露给Objc运行时,依旧是函数表派发。
class LGTeacher : NSObject {
@objc func teach() {
print("teach")
}
}
class ViewController: UIViewController{
override func viewDidLoad() {
let t = LGTeacher()
t.teach()
}
}
查看暴露给Objc的方式
查看暴露给Objc的API
3.4 @objc + dynamic关键字
- 消息派发的方式
class LGTeacher{ // 继承关系吗?没有
@objc dynamic func teach() {
print("teach")
}
}
class ViewController: UIViewController{
override func viewDidLoad() {
let t = LGTeacher()
t.teach()
}
}
汇编调试:
四、函数内联
函数内联 是一种编译器优化技术,它通过使用方法的内容替换直接调用该方法,从而优 化性能。
4.1 OC项目编译器优化
int sum(int a, int b) {
return a + b;
}
int main(int argc, char * argv[]) {
int a = sum(3, 5);
NSLog(@"%d", a);
}
未优化
Debug 默认None 模式 进行测试
汇编调试:
先把
3和5分别存入w0、w1,在进行sum计算
优化
Debug 选择 Fastest, Samallest 模式 进行测试
汇编调试:
得出,直接将计算结果存入到w8,调用NSLog函数输出。
4.2 Swift 函数内联
优化设置
-
将确保有时内联函数。这是默认行为,我们无需执行任何操作. Swift 编译器可能会自动内联函数作为优化。
-
always - 将确保始终内联函数。通过在函数前添加 @inline(__always) 来实现此行为
@inline(__always) func test() {
print("test")
}
- never - 将确保永远不会内联函数。这可以通过在函数前添加 @inline(never) 来实现
@inline(never) func test() {
print("test")
}
- 如果函数很⻓并且想避免增加代码段大小,请使用@inline(never)(使用@inline(never))
4.3 Private 修饰函数
如果对象只在声明的文件中可见,可以用 private 或 fileprivate 进行修饰。编译器会对 private 或 fileprivate 对象进行检查,确保没有其他继承关系的情形下,自动打上 final 标记,进而使得 对象获得静态派发的特性(fileprivate: 只允许在定义的源文件中访问,private : 定义的声明 中访问)
class LGPerson{
private var sex: Bool
private func unpdateSex(){
self.sex = !self.sex
}
init(sex innerSex: Bool) {
self.sex = innerSex
}
func test() {
self.unpdateSex()
}
}