一、异变方法
1.1 mutating 关键字
上一篇文章 中我们了解到,Swift 中 class 和 struct 都能定义方法。但是有一点区别的是默认情况下,值类型属性不能被自身的实例方法修改。
- 可以看到提示说的是 self 不可被修改,因为 x 和 y 是属于 self 的,修改它们就是修改 self 本身,在自己的方法里修改自己。
解决方式:方法用 mutating
关键字进行修饰就不报错了
struct Point {
var x = 0.0, y = 0.0
mutating func moveBy() {
x = 10
y = 20
}
}
1.2 sil 分析
为什么加上 mutating
就不报错了呢,main.swift 添加下面测试代码:
struct Point {
var x = 0.0, y = 0.0
func test() {
let tmp = self.x
}
mutating func moveBy() {
x = 10
y = 20
}
}
通过下面命令,生成 main.sil 文件
swiftc -emit-sil main.swift > ./main.sil && open main.sil
得到 main.sil 文件,通过 sil 来对比一下,不添加 mutating 访问和添加 mutating 两者有什么本质的区别
// test 函数:
sil hidden @$s4main5PointV4testyyF : $@convention(method) (Point) -> () {
debug_value %0 : $Point, let, name "self", argno 1 // id: %1
...
}
// moveBy 函数:
sil hidden @$s4main5PointV6moveByyyF : $@convention(method) (@inout Point) -> () {
debug_value_addr %0 : $*Point, var, name "self", argno 1 // id: %1
...
}
- 对比两个函数可以发现 test 隐藏参数是 Point, moveBy 的隐藏参数是 @inout Point,关于
@inout
的官方解释:An @inout parameter is indirect. The address must be of an initialized object.(当前参数 类型是间接的,传递的是已经初始化过的地址)
- test 函数的赋值过程可以表示为:
let self = Point
- moveBy 函数的赋值过程可以表示为:
var self = &Point
- 总结起来就是:用 @inout 修饰接受的是一个地址就,是可以修改的,否则接受的就是一个值,就不能够被修改。
可以用一个示例来表示:
var point = Point()
let p1 = point
var p2 = withUnsafePointer(to: &point){$0}
point.x = 10
print(p1.x)
print(p2.pointee.x)
运行结果:
0.0
10.0
- 根据结果可以发现修改 point 的值,p1 不能被修改,p2 可以被修改。
1.3 输入输出参数 inout
输入输出参数
:如果我们想函数能够修改一个形式参数的值,而且希望这些改变在函数结束之后依然生效,那么就需要将形式参数定义为输入输出形式参数。在形式参数定义开始的时候在前边添加一个 inout 关键字可以定义一个输入输出形式参数,下面通过例子进行说明。
如果形参没有被 inout 修饰,修改形参就会报错:
想要修改形参,需要用 inout 进行修饰,并传入地址类型:
func changeAge(_ age: inout Int) {
age += 10
}
changeAge(&age)
print(age)
打印结果:
0.0
10.0
15
二、函数表的调度
2.1 汇编探索
class LGTeacher {
func teach() {
print("teach")
}
func teach1() {
print("teach")
}
func teach2() {
print("teach")
}
}
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let t = LGTeacher()
t.teach()
t.teach1()
t.teach2()
}
}
- 上图中
x8
、x9
、x9
分别代表teach()
、teach1()
、teach2()
- 读取
x8
,进行验证:(lldb) register read x8 x8 = 0x00000001023f22f4 SSLTwoTest`SSLTwoTest.LGTeacher.teach() -> ()
- 我们也可以看到函数调用前都有偏移的操作
[x8, #0x50]
、[x9, #0x58]
、[x9, #0x60]
函数的调用过程是:找到 Metadata
,确定函数地址(metadata + 偏移量), 执行函数,下面进行分析。
2.2 sil 验证
通过下面命令,生成 sil 文件
swiftc -emit-silgen -Onone -target x86_64-apple-ios14.2-simulator -sdk $(xcrun --show-sdk-path --sdk iphonesimulator) ViewController.swift > ./ViewController.sil && open ViewController.sil
sil 文件中,可以看到有 vtable
函数表,里面罗列了类中所有的函数
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
}
2.3源码分析 查找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 ,就是对类的一个详细描述。
打开源码,在 metadata.h 中找到 Description
:
TargetSignedPointer<Runtime, TargetClassDescriptor> Description;
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 ,做了一些赋值的操作,我们也看到了 addVTable()
,点击查看:
void addVTable() {
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);
}
- B 就是 descriptor,遍历添加了
函数指针
,和函数的数量
。
至此,还原出 TargetClassDescriptor 结构如下:
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
}
2.4 什么是 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。
Macho 文件格式:
- 首先是文件头,表明该文件是 Mach-O 格式,指定目标架构,还有一些其他的文件属性信息,文件头信息影响后续的文件结构安排
- Load commands 是一张包含很多内容的表。内容包括区域的位置、符号表、动态符号表等
- Data 区主要就是负责代码和数据记录的。Mach-O 是以 Segment 这种结构来组织数据 的,一个 Segment 可以包含 0 个或多个 Section。根据 Segment 是映射的哪一个 Load Command,Segment 中 section 就可以被解读为是代码,常量或者一些其他的数据类型。在装载在内存中时,也是根据 Segment 做内存映射的。
2.5 Mach-O 分析V-Table
Section64(_TEXT,__swift5_types)
中存放的就是 Descriptor
:
计算 Descriptor 在 Mach-O 的内存地址:
FFFFFBA8 + 0000BB7C = 0x10000B724
0x10000 是虚拟地址的开端,B724
就是 Descriptor 在 Mach-O 中的偏移量,定位位置如下:
如上图红圈就是 Descriptor 的首地址,后面就是 Descriptor 结构体里面的内容,Descriptor 中有 13 个 UInt32,也就是13 个 4 字节。
移动 13 个 4 字节,定位到下面的位置:
B758
就是 teach() 在 Mach-O 文件中的偏移量, B758 + ASLR(随机偏移地址)
就是 teach() 的地址,
通过 image list
命令得到 ASLR 程序运行的基地址 0x00000001023ec000
:
所以 teach() 函数的首地址是:0x00000001023ec000
+ B758
= 0x1023F7758
,也就是下面这个结构
在源码中找到下面的结构,这个就是 Swift 中的方法:
struct TargetMethodDescriptor {
/// Flags describing the method.
MethodDescriptorFlags Flags; // 4字节
/// The method implementation. // offset
TargetRelativeDirectPointer<Runtime, void> Impl;
};
- 计算 Impl 的地址:
0x1023F7758
+4
+FFFFAB98
=0x2023F22F4
- 所以 teach() 函数地址:
0x2023F22F4
-0x1000
=0x1023F22F4
读取的 teach() 的地址:
可以看到地址都是 0x1023F22F4
证明了我们上面的说法,V-Table 就是在 Descriptor 结构的后面。
2.6 获取函数 为什么要偏移
static void initClassVTable(ClassMetadata *self) {
const auto *description = self->getDescription();
auto *classWords = reinterpret_cast<void **>(self);
if (description->hasVTable()) {
auto *vtable = description->getVTableDescriptor();
auto vtableOffset = vtable->getVTableOffset(description);
auto descriptors = description->getMethodDescriptors();
for (unsigned i = 0, e = vtable->VTableSize; i < e; ++i) {
auto &methodDescription = descriptors[i];
swift_ptrauth_init_code_or_data(
&classWords[vtableOffset + i], methodDescription.Impl.get(),
methodDescription.Flags.getExtraDiscriminator(),
!methodDescription.Flags.isAsync());
}
}
...
}
可以看到,在函数存储到 VTable 时就进行了偏移操作 vtableOffset,所以取的时候也要进行偏移操作。
三、其他函数调度方式
3.1 struct 函数调度
将 class 换成 struct,再次进行汇编调试:
- 可以看到 struct 的函数调用,就是直接的地址调用,也就是
静态派发
。
3.2 struct 的 extension 函数调度
给 SSLTeacher 添加 extension:
extension SSLTeacher {
func teach3() {
print("teach3")
}
}
汇编调试:
- 可以看到 teach3 也是直接的地址调用, struct 的 extension 也是
静态派发
。
3.3 class 的 extension 函数调度
将 struct 重新改回 class:
class SSLTeacher {
func teach() {
print("teach")
}
func teach1() {
print("teach1")
}
func teach2() {
print("teach2")
}
}
汇编调试:
- 可以看到,class 的 extension 也是
静态派发
。
3.4 方法调度方式总结
四、关键字对派发方式的影响
4.1 final
添加了 final 关键字的函数无法被重写,使用静态派发,不会在 vtable 中出现,且 对 objc 运行时不可⻅。
class SSLTeacher {
final func teach() {
print("teach")
}
...
}
4.2 dynamic
函数均可添加 dynamic 关键字,为非objc类和值类型的函数赋予动态性,但派发 方式还是函数表派发。
class SSLTeacher {
dynamic func teach() {
print("teach")
}
}
extension SSLTeacher {
@_dynamicReplacement(for: teach)
func teach3() {
print("teach3")
}
}
let t = SSLTeacher()
t.teach()
打印结果:
teach3
4.3 @objc
该关键字可以将 Swift 函数暴露给Objc运行时,依旧是函数表派发。
class SSLTeacher: NSObject {
// 消息调度的机制
@objc func teach() {
print("teach")
}
func teach1() {
print("teach1")
}
func teach2() {
print("teach2")
}
}
extension SSLTeacher {
@objc func teach3() {
print("teach3")
}
}
查看:
4.4 @objc + dynamic
用 @objc + dynamic 修饰方法,我们就可以使用 runtime 的 api
class SSLTeacher {
// 消息调度的机制
@objc dynamic func teach() {
print("teach")
}
func teach1() {
print("teach1")
}
func teach2() {
print("teach2")
}
}
extension SSLTeacher {
@objc dynamic func teach3() {
print("teach3")
}
}
五、函数内联
函数内联 是一种编译器优化技术,它通过使用方法的内容替换直接调用该方法,从而优化性能。
5.1 OC 项目优化示例
先创建一个 OC 项目,添加代码:
int sum(int a, int b) {
return a + b;
}
int main(int argc, char * argv[]) {
int a = sum(1, 2);
NSLog(@"%d",a);
return 0;
}
没有优化时
在下面就可以配置优化等级,Debug 环境下我们先不选择优化:
进行汇编调试:
- 可以看到没有优化时,会先将 1 和 2 的值分别存入 w0 和 w1
- 然后再调用 sum 函数。
最快最小优化
接下来选择 最快最小
优化:
再次进行汇编调试:
可以看到优化后的代码,将计算结果 3 直接存入到 w8 寄存器,然后调用了 NSLog 函数。
5.2 Swift 中的内联
优化的设置:
内联的操作
- Swift 中的内联函数是默认行为,我们无需执行任何操作. Swift 编译器可能会自动内联函数作为优化。
// 编译器会认为 test 没有太多意义,会省略test的符号调用,直接调用print func test() { print("test"); }
- always - 将确保始终内联函数。通过在函数前添加 @inline(__always) 来实现此行为
- never - 将确保永远不会内联函数。这可以通过在函数前添加 @inline(never) 来实现。
- 如果函数很⻓并且想避免增加代码段大小,请使用@inline(never)(使用@inline(never))
5.3 private 的优化操作
如果对象只在声明的文件中可⻅,可以用 private 或 fileprivate 进行修饰。编译器会对 private 或 fileprivate 对象进行检查,确保没有其他继承关系的情形下,自动打上 final 标记,进而使得对象获得静态派发的特性(fileprivate: 只允许在定义的源文件中访问,private : 定义的声明中访问)
添加代码:
class SSLPerson {
private var sex: Bool
func unpdateSex() {
self.sex = !self.sex
}
init(sex innerSex: Bool) {
self.sex = innerSex
}
func test() {
self.unpdateSex()
}
}
let t = SSLPerson(sex: true)
t.test()
可以看到 unpdateSex 的调用是直接的地址调用,并没有使用函数表。