1、异变方法
1.1 异变 mutating
- Swift 中 class 和 struct 都能定义方法。但是有一点区别的是:默认情况下,值类型属性不能被自身的实例方法修改.因为此时如果实例化一个对象point,调用moveBy方法会直接影响到point本身属性。如果要实现可在方法前加上mutating关键字。
- mutating 的本质探索(sil方式)
首先在target中添加一个script,然后添加下面的语句,即可生成sil代码并自动打开(第一次需手动打开)
swiftc -emit-sil main.swift > ./main.sil && open main.sil
接下来就用下面的代码进行探索
struct Point {
var x = 0.0, y = 0.0
mutating func moveBy(x deltaX: Double, y deltaY: Double) {
self.x += deltaX
self.y += deltaY
}
func moveBy_2(x deltaX: Double, y deltaY: Double) {
print(x,y)
}
}
-
moveBy
- moveBy_2
通过对比可以发现
- 函数默认传入 self 参数的类型有区别,其中,moveBy函数传入的是inout Point类型,本质传入的是实例对象的地址,而moveBy_2传入的是Point类型,本质传入的是实例对象本身。
- 函数底层声明的行参类型不同,moveBy函数中,inout 参数会将self声明为一个 var 常量,而moveBy_2则生成的是let常量,这也解释了为什么不加mutating无法更改属性,而加了就可以。
简单来说可以用以下伪代码来表示
//moveBy
var self = &Point
//moveBy_2
let self = Point
总结:
值类型中的属性都是直接存储在实例中,因此在方法内部修改属性就相当于修改 self,而只有在mutating关键字修饰的函数中,传入的默认self是有inout关键字修饰的,而self此时是var类型,因此才可以修改自身self的属性。
1.2 输入输出 inout
关于inout,sil文档的解释是
An @inout parameter is indirect. The address must be of an initialized object.(当前参数 类型是间接的,传递的是已经初始化过的地址)
也就是说如果我们想函数能够修改一个形式参数的值,而且希望这些改变在函数结束之后依然生效,那么就需要将形式参数定义为输入输出形式参数 。在形式参数定义开始的时候在前边 添加一个inout关键字可以定义一个输入输出形式参数
注意:inout不能用于let常量,只能用于var常量
2、方法调度
oc中方法调度使用的是objc_mgsend,swift中则是基于函数表的调度,接下来将通过汇编来探索
2.1 汇编探索
首先先解释下常见的指令
mov:将某一寄存器的值复制到另一寄存器(只能用于寄存器与寄存器或者寄存器
与常量之间传值,不能用于内存地址),如:
mov x1, x0 将寄存器 x0 的值复制到寄存器 x1 中
ldr: 将内存中的值读取到寄存器中,如:
ldr x0, [x1, x2] 将寄存器 x1 和寄存器 x2 的值相加作为地址,取该内存地址的值放入寄存器 x0 中
str : 将寄存器中的值写入到内存中,如:
str x0, [x0, x8] 将寄存器 x0 的值保存到栈内存 [x0 + x8] 处
bl:跳转到某地址(有返回)
blr:跳转到某地址(无返回)
本次探索采用的是真机调试,所以此处的汇编为arm64
import UIKit
class Person {
func sex() {
print("sex")
}
func age() {
print("age")
}
func name() {
print("name")
}
}
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let t = Person()
t.sex()
t.age()
t.name()
}
}
汇编分析
图中的metadata证明,可用register read 命令查询
总结:
函数的调用过程:找到 Metadata -> 确定函数地址(metadata + 偏移量 -> 执行函数
2.2 SIL探索
通过汇编探索,可发现三个函数的偏移量分别为0x50,0x58,0x60,是一个连续的内存地址,那么是否可以推断函数在内存中是连续存放的,是基于函数表的调度,继续探索
从sil文件中可以发现有一个vtable的函数表,罗列了Person中类的所有函数,由此也可以推断是基于函数表的调度。
2.3 源码探索vTable
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
}
对于这个metadata需要关注 typeDescriptor ,不管是 Class,Struct , Enum 都有自己的Descriptor,就是对类的一个详细描述,打开源码,在 Metadata.h 中找到 Description:
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;
通过 TargetClassDescriptor 结构的源码得知其继承关系 TargetClassDescriptor :TargetTypeContextDescriptor : TargetContextDescriptor 根据继承关系可以获取 Descriptor 的大致结构如下
class 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
}
通过继承关系还原的结构仍然没有发现vTable,此时通过搜索TargetClassDescriptor可以发现一条语句
using ClassDescriptor = TargetClassDescriptor<InProcess>;
ClassDescriptor 是它的一个别名,全局搜索,在 GenMeta.cpp 中找到下面的内容:
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();
}
}
class ClassContextDescriptorBuilder
: public TypeContextDescriptorBuilderBase<ClassContextDescriptorBuilder,
ClassDecl>,
public SILVTableVisitor<ClassContextDescriptorBuilder>
{
...
void layout() {
assert(!getType()->isForeignReferenceType());
super::layout();
//添加函数表
addVTable();
//添加继承函数表
addOverrideTable();
addObjCResilientClassStubInfo();
maybeAddCanonicalMetadataPrespecializations();
}
}
通过上面的代码,可以知道这就是在创建 Descriptor ,此处做了一些赋值的操作,跟一开始还原的结构体基本对应,同时也发现vTable的踪迹。
点开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为ClassContextDescriptorBuilder
//添加偏移量
B.addInt32(offset / IGM.getPointerSize());
//添加函数size
B.addInt32(VTableEntries.size());
//添加函数指针
for (auto fn : VTableEntries)
emitMethodDescriptor(fn);
}
从addVTable函数中,我们可知计算 offset 之后,调用了 addInt32 函数,这个函数就是去计算添加方法到函数表的偏移量后添加到B,然后添加函数size,最后 for 循环添加函数的指针。至此,可以将TargetClassDescriptor结构体补充为:
class 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
}
2.4 Mach-O探索
2.4.1 Mach-O基础
Macho:Mach-O 其实是Mach Object文件格式的缩写,是 mac 以及 iOS 上可执行文件的格式,类似于 windows 上的 PE 格式 (Portable Executable ), linux 上的 elf 格式 (Executable and Linking Format) 。常⻅的 .o,.a, .dylib, Framework,dyld, .dsym。
Mach-O文件格式
首先是文件头,表明该文件是 Mach-O 格式,指定目标架构,还有一些其他的文件属性信息,文件头信息影响后续的文件结构安排
-
Load commands是一张包含很多内容的表。内容包括区域的位置、符号表、动态符号表等。
Data 区主要就是负责代码和数据记录的。Mach-O 是以 Segment 这种结构来组织数据 的,一个 Segment 可以包含 0 个或多个 Section。根据 Segment 是映射的哪一个 Load Command,Segment 中 section 就可以被解读为是是代码,常量或者一些其他的数据类 型。在装载在内存中时,也是根据 Segment 做内存映射的。
2.4.2 Mach-O探索vTable
-
从工程中的Products文件夹中找到 .app 文件,新版xcode可用点击Product -> Show Build Folder in Finder,找到应用程序后显示包内容即可。
-
用Mach-OView打开.app文件
Section64(_TEXT,__swift5_types) 这里存放的是Swift 结构体、枚举、类的 Descriptor,在这里找到 Descriptor 的地址信息
在Mach-O文件中,是按4个字节来区分的,因此计算 Descriptor 在 Mach-O 的内存地址:
FFFFBF0 + 0000BC58 = 0x1000B848
因为Mach-O文件有虚拟内存的基地址
所以,当前得到的地址还需要减去这个基地址才是Person的 Descriptor在Data 区的首地址,也就是B848 就是 Descriptor 在 Mach-O 中的偏移量,定位位置如下:
第一个红圈就是 Descriptor 的首地址,后面就是 Descriptor 结构体里面的内容,通过上文我们可以得出 Descriptor 中有 13 个 UInt32,也就是13 个 4 字节,所以可以得出结论:
B87C就是第一个函数sex的首地址(偏移量),又因为方法的指针应该是8个字节,所以B884是函数age的首地址(偏移量),B88C是函数name的首地址(偏移量)。
- 找到函数真实调用地址
iOS中每个应用程序都有一个ASLR(随机偏移地址),所以函数的真实调用地址应该是函数在 Mach-O 文件中的偏移量加上ASLR,然后偏移 TargetMethodDescriptor中 flag 的4个字节,加上impl中的偏移量,最后再减去 Mach-O 的虚拟基地址
(1) 通过 image list 命令得到 ASLR 程序运行的基地址 0x0000000104efc000
(2) 通过源码中的Metadata.h,找到方法在内存的数据结构
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.
};
其中:
Flags 一个UInt32 类型,占4个字节;Impl 不是真正的 imp,而是相对指针 Offset
所以sex函数的真实地址为
0x0000000104efc000 + B87C + 4 + FFFFB9AC - 0x100000000 = 0x104F0322C
通过register read 命令,可以得出我们的计算是相符的
2.5 其它函数的调度
2.5.1 结构体的函数调度
import UIKit
struct Person {
func sex() {
print("sex")
}
func age() {
print("age")
}
func name() {
print("name")
}
}
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let t = Person()
t.sex()
t.age()
t.name()
}
}
可以看到 struct 的函数调用,就是直接的地址调用,也就是静态派发。
2.5.2 extension 函数调用
import UIKit
class Person {
}
extension Person{
func name() {
print("name")
}
}
struct People {
}
extension People{
func name() {
print("name")
}
}
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let t = Person()
t.name()
let t1 = People()
t1.name()
}
}
可以看到 无论是 class或者是struct 在extension 中的的方法都是通过静态调用的方式
2.5.3 子类的函数调用
import UIKit
class Person {
func age() {
print("age")
}
}
class People : Person{
func sex() {
print("sex")
}
}
extension Person{
func name() {
print("name")
}
}
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let t = People()
t.name()
t.sex()
t.age()
}
}
通过汇编可以发现只有name函数是静态派发,其它的方法仍然是函数表的调度
2.6 总结
3、影响函数派发的方式
3.1 final
添加了 final 关键字的函数无法被继承重写,使用静态派发,不会在 vtable 中出现,且 对 objc 运行时不可见。实际开发过程中属性,方法,类不需要被重载时使用
class Person {
final func name() {
print("name")
}
}
3.2 dynamic
函数均可添加 dynamic 关键字,为非objc类和值类型的函数赋予动态性,但派发方式还是函数表派发。
import UIKit
class Person {
dynamic func age() {
print("age")
}
}
extension Person{
@_dynamicReplacement(for: age)
func name() {
print("name")
}
}
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let t = Person()
t.age()
}
}
动态性类似于oc中的交换方法,上列中@_dynamicReplacement(for: age) 即用name方法替换了age方法,打印结果为 name。
3.3 @objc
该关键字可以将Swift函数暴露给Objc运行时,依旧是函数表派发。
class Person {
@objc func name() {
print("name")
}
}
3.4 @objc + dynamic:
消息派发的方式,也就是Objc_msgSend,意味着可以使用oc中的runtime,多用于swift和oc的交互使用
class Person {
@objc dynamic func name() {
print("name")
}
}
4、函数内联
4.1 基础认知
函数内联是一种编译器优化技术,它通过使用方法的内容替换直接调用该方法,从而优化性能。
- Swift 中的内联函数是默认行为,我们无需执行任何操作. Swift 编译器可能会自动内联函数作为优化
- always - 将确保始终内联函数。通过在函数前添加 @inline(__always) 来实现此行为
- never - 将确保永远不会内联函数。这可以通过在函数前添加 @inline(never) 来实现。
- 如果函数很⻓并且想避免增加代码段大小,请使用@inline(never)(使用@inline(never))
可以通过xcode 更设置
4.2 优化
如果对象只在声明的文件中可见,可以用 private 或 fileprivate 进行修饰。编译器会对 private 或 fileprivate 对象进行检查,确保没有其他继承关系的情形下,自动打上 final 标记,进而使得对象获得静态派发的特性。
- fileprivate: 只允许在定义的源文件中访问
- private : 定义的声明中访问
class Person {
private var sex: Bool
private func unpdateSex() {
self.sex = !self.sex
}
init(sex innerSex: Bool) {
self.sex = innerSex
}
func test() {
self.unpdateSex()
}
}
let t = Person(sex: true)
t.test()
从上图可得,unpdateSex函数被优化为静态派发