类与结构体(下)
一、异变方法
上一篇文章我们了解到,Swift 中 class 和 struct 都能定义方法。但是有一点区别的是默认情况 下,值类型属性不能被自身的实例方法修改。
不添加mutating访问
struct Point {
var x = 0.0, y = 0.0
func moveBy(x deltaX: Double, y deltaY: Double) {
x += deltaX
y += deltaY
}
}
报错: Left side of mutating operator isn't mutable: 'self' is immutable
原因是值类型本身是不允许修改属性的,因为x和y是属于self, self是let类型。
代码改成如下:
struct Point {
var x = 10.0
func moveBy(x deltaX: Double) {
print(x)
}
}
通过SIL来分析
// Point.moveBy(x:)
sil hidden @main.Point.moveBy(x: Swift.Double) -> () : $@convention(method) (Double, Point) -> () {
// %0 "deltaX" // user: %2
// %1 "self" // users: %10, %3
bb0(%0 : $Double, %1 : $Point):
// 此时的self是let类型,即是不允许修改的
debug_value %0 : $Double, let, name "deltaX", argno 1 // id: %2
debug_value %1 : $Point, let, name "self", argno 2 // id: %3
%4 = integer_literal $Builtin.Word, 1 // user: %6
......
解决方式:方法用 mutating 关键字进行修饰就不报错了
添加mutating
struct Point {
var x = 0.0, y = 0.0
func test(){
let tmp = self.x
}
mutating func moveBy(x deltaX: Double, y deltaY: Double) {
x += deltaX
y += deltaY
}
}
SIL文件
struct Point {
@_hasStorage @_hasInitialValue var x: Double { get set }
@_hasStorage @_hasInitialValue var y: Double { get set }
func test()
mutating func moveBy(x deltaX: Double, y deltaY: Double)
init()
init(x: Double = 0.0, y: Double = 0.0)
}
// Point.test()
// let self = Point 当前self是不可修改的
sil hidden @main.Point.test() -> () : $@convention(method) (Point) -> () {
// %0 "self" // users: %2, %1
bb0(%0 : $Point):
debug_value %0 : $Point, let, name "self", argno 1 // id: %1
%2 = struct_extract %0 : $Point, #Point.x // user: %3
debug_value %2 : $Double, let, name "tmp" // id: %3
%4 = tuple () // user: %5
return %4 : $() // id: %5
} // end sil function 'main.Point.test() -> ()'
// Point.moveBy(x:y:)
// mutaing的本质:添加了inout输入输出
sil hidden @main.Point.moveBy(x: Swift.Double, y: Swift.Double) -> () : $@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
// let self = &Point 当前self是可修改的
// self是var类型,可以修改,而且这里访问的地址,并不是原始的值
debug_value_addr %2 : $*Point, var, name "self", argno 3 // id: %5
%6 = begin_access [modify] [static] %2 : $*Point // users: %15, %7
%7 = struct_element_addr %6 : $*Point, #Point.x // users: %13, %8
%8 = struct_element_addr %7 : $*Double, #Double._value // user: %9
%9 = load %8 : $*Builtin.FPIEEE64 // user: %11
%10 = struct_extract %0 : $Double, #Double._value // user: %11
%11 = builtin "fadd_FPIEEE64"(%9 : $Builtin.FPIEEE64, %10 : $Builtin.FPIEEE64) : $Builtin.FPIEEE64 // user: %12
%12 = struct $Double (%11 : $Builtin.FPIEEE64) // user: %13
store %12 to %7 : $*Double // id: %13
%14 = tuple ()
end_access %6 : $*Point // id: %15
%16 = begin_access [modify] [static] %2 : $*Point // users: %25, %17
%17 = struct_element_addr %16 : $*Point, #Point.y // users: %23, %18
%18 = struct_element_addr %17 : $*Double, #Double._value // user: %19
%19 = load %18 : $*Builtin.FPIEEE64 // user: %21
%20 = struct_extract %1 : $Double, #Double._value // user: %21
%21 = builtin "fadd_FPIEEE64"(%19 : $Builtin.FPIEEE64, %20 : $Builtin.FPIEEE64) : $Builtin.FPIEEE64 // user: %22
%22 = struct $Double (%21 : $Builtin.FPIEEE64) // user: %23
store %22 to %17 : $*Double // id: %23
%24 = tuple ()
end_access %16 : $*Point // id: %25
%26 = tuple () // user: %27
return %26 : $() // id: %27
} // end sil function 'main.Point.moveBy(x: Swift.Double, y: Swift.Double) -> ()'
通过sil文件我们发现
// Point.test()
sil hidden @main.Point.test() -> () : $@convention(method) (Point) -> () {
// self是let类型, 不可修改的
debug_value %0 : $Point, let, name "self", argno 1 // id: %1
简单理解就是: let self = Point
// Point.moveBy(x:y:)
sil hidden @main.Point.moveBy(x: Swift.Double, y: Swift.Double) -> () : $@convention(method) (Double, Double, @inout Point) -> () {
// self是var类型,可以修改,而且这里访问的地址,并不是原始的值
debug_value_addr %2 : $*Point, var, name "self", argno 3 // id: %5
简单理解就是: let self = &Point
总结:用 @inout 修饰接受的是一个地址,是可以修改的
举例:
struct Point {
var x = 0.0, y = 0.0
func test(){
let tmp = self.x
}
mutating func moveBy(x deltaX: Double, y deltaY: Double) {
x += deltaX
y += deltaY
}
}
var p = Point()
let x1 = p
var x2 = withUnsafePointer(to: &p ) { return $0 }
var x3 = p
p.x = 30.0
print(x2.pointee.x) // 30.0
print(x3.x) // 0.0
发现修改 point 的值,x3不能被修改,x2 可以被修改
@inout
官方文档解释
An @inout parameter is indirect. The address must be of an initialized object.
当前参数类型是间接的,传递的是已经初始化过的地址
一般情况下,在函数的声明中,默认的参数都是不可变的,如果想要直接修改,需要给参数加上inout关键字
- 未加inout关键字,给参数赋值,编译报错
var age = 10
//函数的形式参数都是let类型
func modifyage1(_ age: Int) {
// age += 1 // 报错 Left side of mutating operator isn't mutable: 'age' is a 'let' constant
var tmp = age
tmp += 1
}
- 添加inout关键字,可以给参数赋值
var age = 10
func modifyage(_ age: inout Int) {
age += 1
}
modifyage(&age)
print(age) // 11
总结
- 结构体中的函数如果想修改其中的属性,需要在函数前加上mutating,而类则不用
- mutating本质也是加一个 inout修饰的self
- inout相当于取地址,可以理解为地址传递,即引用
- mutating修饰方法,而inout 修饰参数
- 对于变异方法, 传入的self被标记为inout参数。无论在mutating方法内部发生什么,都会影响外部依赖类型的一切。
- 如果我们想函数能够修改一个形式参数的值,而且希望这些改变在函数结束之后依然生效,那么就需要将形式参数定义为 输入输出形式参数。在形式参数定义开始的时候在前边添加一个inout关键字可以定义一个输入输出形式参数
二、方法调度
2.1 静态派发
值类型对象的函数的调用方式是静态调用,即直接地址调用,调用函数指针,这个函数指针在编译、链接完成后就已经确定了,存放在代码段,而结构体内部并不存放方法。因此可以直接通过地址直接调用
我们先来看一下Swift中的方法调度
class Teacher {
func teach() {
print("teach")
}
func teach1() {
print("teach1")
}
func teach2() {
print("teach2")
}
}
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
var t = Teacher()
t.teach() //断点
t.teach1() //断点
t.teach2() //断点
}
}
2.1.1 汇编探索
-
上图中
x8、x8、x8分别代表teach()、teach1()、teach2() -
读取
x8,进行验证:(lldb) register read x8 x8 = 0x00000001041825d8 SwiftDemo`SwiftDemo.Teacher.teach() -> () at ViewController.swift:12 -
同时我们看到函数调用前都有偏移的操作
[x8, #0x50]、[x8, #0x58]、[x8, #0x60]
总结
所以函数的调用过程是: 找到Metadata,然后确定函数地址(metadata + 偏移量),最后在执行函数。编译后,我们看到函数地址已经确定的。
2.1.2 SIL验证
下面进行分析
添加Run Script
swiftc -emit-silgen -Onone -target arm64-apple-ios15.2-simulator -sdk $(xcrun --show-sdk-path --sdk iphonesimulator) ${SRCROOT}/LGSwift/ViewController.swift > ./ViewController.sil && open ViewController.sil
# swiftc -emit-silgen -Onone -target x86_64-apple-ios15.2-simulator -sdk $(xcrun --show-sdk-path --sdk iphonesimulator) ${SRCROOT}/LGSwift/ViewController.swift > ./ViewController.sil && open ViewController.sil
SIL文件如下所示
sil_vtable Teacher {
#Teacher.teach: (Teacher) -> () -> () : @$s14ViewController7TeacherC5teachyyF // Teacher.teach()
#Teacher.teach1: (Teacher) -> () -> () : @$s14ViewController7TeacherC6teach1yyF // Teacher.teach1()
#Teacher.teach2: (Teacher) -> () -> () : @$s14ViewController7TeacherC6teach2yyF // Teacher.teach2()
#Teacher.init!allocator: (Teacher.Type) -> () -> Teacher : @$s14ViewController7TeacherCACycfC // Teacher.__allocating_init()
#Teacher.deinit!deallocator: @$s14ViewController7TeacherCfD // Teacher.__deallocating_deinit
}
可以看到有 vtable函数表,罗列了类中所有的函数方法
2.1.3 源码分析
之前我们在上一篇文章讲到了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
template <typename Runtime>
struct TargetClassMetadata : public TargetAnyClassMetadata<Runtime> {
using StoredPointer = typename Runtime::StoredPointer;
using StoredSize = typename Runtime::StoredSize;
TargetClassMetadata() = default;
constexpr TargetClassMetadata(const TargetAnyClassMetadata<Runtime> &base,
ClassFlags flags,
ClassIVarDestroyer *ivarDestroyer,
StoredPointer size, StoredPointer addressPoint,
StoredPointer alignMask,
StoredPointer classSize, StoredPointer classAddressPoint)
: TargetAnyClassMetadata<Runtime>(base),
Flags(flags), InstanceAddressPoint(addressPoint),
InstanceSize(size), InstanceAlignMask(alignMask),
Reserved(0), ClassSize(classSize), ClassAddressPoint(classAddressPoint),
Description(nullptr), IVarDestroyer(ivarDestroyer) {}
// The remaining fields are valid only when isTypeMetadata().
// The Objective-C runtime knows the offsets to some of these fields.
// Be careful when accessing them.
/// Swift-specific class flags.
ClassFlags Flags;
/// The address point of instances of this type.
uint32_t InstanceAddressPoint;
/// The required size of instances of this type.
/// 'InstanceAddressPoint' bytes go before the address point;
/// 'InstanceSize - InstanceAddressPoint' bytes go after it.
uint32_t InstanceSize;
/// The alignment mask of the address point of instances of this type.
uint16_t InstanceAlignMask;
/// Reserved for runtime use.
uint16_t Reserved;
/// The total size of the class object, including prefix and suffix
/// extents.
uint32_t ClassSize;
/// The offset of the address point within the class object.
uint32_t ClassAddressPoint;
// Description is by far the most likely field for a client to try
// to access directly, so we force access to go through accessors.
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;
......
简化:
struct TargetClassMetadata : public TargetAnyClassMetadata<Runtime> {
ClassFlags Flags;
uint32_t InstanceAddressPoint;
uint32_t InstanceSize;
uint16_t InstanceAlignMask;
uint16_t Reserved;
uint32_t ClassSize;
uint32_t ClassAddressPoint;
private:
TargetSignedPointer<Runtime, const TargetClassDescriptor<Runtime> * __ptrauth_swift_type_descriptor> Description; //这里Description描述的类型是TargetClassDescriptor
这里我们看到Description,描述的类型是TargetClassDescriptor
搜索TargetClassDescriptor
using ClassDescriptor = TargetClassDescriptor<InProcess>;
ClassDescriptor 是它的一个别名,全局搜索,在 GenMeta.cpp 中找到下面的内容:
// swift/lib/IRGen/GenMeta.cpp
class ClassContextDescriptorBuilder
: public TypeContextDescriptorBuilderBase<ClassContextDescriptorBuilder, ClassDecl>,
public SILVTableVisitor<ClassContextDescriptorBuilder> {
.......
void layout() {
super::layout();
addVTable();
addOverrideTable();
addObjCResilientClassStubInfo();
maybeAddCanonicalMetadataPrespecializations();
}
......
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就是descriptor, 添加函数指针和函数的数量
B.addInt32(offset / IGM.getPointerSize());
B.addInt32(VTableEntries.size());
for (auto fn : VTableEntries)
emitMethodDescriptor(fn);
}
}
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();
}
}
上面的代码这就是在创建descriptor,做了一些赋值的操作,我们也看到了addVTable()
还原出 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
// 上面共12个4字节
var size: UInt32
// V-Table
}
2.1.4 Mach-O:
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是一张包含很多内容的表。内容包括区域的位置、符号表、动态符号表等。
| 参数 | 说明 |
|---|---|
| LC_SEGMENT_64 | 将文件中(32位或64位)的段映射到进程地址空间中 |
| LC_DYLD_INFO_ONLY | 动态链接相关信息 |
| LC_SYMTAB | 符号地址 |
| LC_DYSYMTAB | 动态符号表地址 |
| LC_LOAD_DYLINKER | dyld加载 |
| LC_UUID | 文件的UUID |
| LC_VERSION_MIN_MACOSX | 支持最低的操作系统版本 |
| LC_SOURCE_VERSION | 源代码版本 |
| LC_MAIN | 设置程序主线程的入口地址和栈大小 |
| LC_LOAD_DYLIB | 依赖库的路径,包含三方库 |
| LC_FUNCTION_STARTS | 函数起始地址表 |
| LC_CODE_SIGNATURE | 代码签名 |
- Data区主要就是负责代码和数据记录的。Mach-O是以Segment这种结构来组织数据的,一个Segment可以包含0个或多个Section。根据Segment是映射的哪一个Load Command,Segment中section就可以被解读为是是代码,常量或者一些其他的数据类 型。在装载在内存中时,也是根据Segment做内存映射的。
Swift除了兼容了OC的存储结构外,还具备自己的存储结构,通过MachOView能看到Mach-O文件中存储了很多以swift5命名的section
这些section中,__swift5_types中存储的是Class、Struct、Enum的地址。具体每个section存储Swift的哪些数据,在Swift metadata一文中有较为详细的描述。
如果此时你打开MachOView,查看__swift5_types的二进制数据后你会发现它与OC的存储有很大的不同。在OC中,存储地址通常都是8字节的直接存储对应的地址。但是types不是8字节地址,而是4字节,并且所存储的数据明显不是直接地址,而是相对地址。那么如何得出Teach类的地址呢?当前文件偏移 + 随后4字节中存储的value即可得到地址。
为什么Swift要采用这种方式来存储数据呢?猜测是为了节省包大小,按照OC的存储习惯存储一个地址需要8字节,而在这里4字节就够了。 经过计算后可发现,Teach类的偏移位于__TEXT,__ __const中。
2.1.5 分析V-Table
-
Section64(_TEXT,__swift5_types)中存放的就是Descriptor:计算 Descriptor 在 Mach-O 的内存地址:
0xBB8C + 0xFFFFFBA4 = 0x10000B730
0x10000 是虚拟地址的开端,B730 就是 Descriptor 在 Mach-O 中的偏移量,定位位置如下:
如图最上面红圈就是 Descriptor 的首地址,后面就是 Descriptor 结构体里面的内容,Descriptor 中有 13 个 UInt32,也就是13 个 4 字节。定位到下面的位置
B764 就是 teach() 在 Mach-O 文件中的偏移量, B764 + ASLR(随机偏移地址) 就是 teach() 的地址
通过 image list 命令得到 ASLR 程序运行的基地址 0x0000000100b0c000:
所以 teach() 函数的首地址是:
0x0000000100b0c000 + 0xB764 = 0x100B17764
在源码中找到下面的结构TargetMethodDescriptor
struct TargetMethodDescriptor {
MethodDescriptorFlags Flags; //4字节
TargetRelativeDirectPointer<Runtime, void> Impl;
};
using MethodDescriptor = TargetMethodDescriptor<InProcess>;
- 计算 Impl 的地址
0x100B17764 + 0x4 + 0xFFFFAE70 = 0x200B125D8
- teach() 函数地址
0x200B125D8 - 0x100000000 = 0x100B125D8
- 读取的 teach() 的地址:
2.1.6 V-Table偏移
源码中搜索initClassVTable,并加上断点,然后写上源码进行调试
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());
}
}
if (description->hasOverrideTable()) {
auto *overrideTable = description->getOverrideTable();
auto overrideDescriptors = description->getMethodOverrideDescriptors();
for (unsigned i = 0, e = overrideTable->NumEntries; i < e; ++i) {
auto &descriptor = overrideDescriptors[i];
auto *baseClass = cast_or_null<ClassDescriptor>(descriptor.Class.get());
auto *baseMethod = descriptor.Method.get();
if (baseClass == nullptr || baseMethod == nullptr)
continue;
auto baseClassMethods = baseClass->getMethodDescriptors();
if (baseMethod < baseClassMethods.begin() ||
baseMethod >= baseClassMethods.end()) {
fatalError(0, "resilient vtable at %p contains out-of-bounds "
"method descriptor %p\n",
overrideTable, baseMethod);
}
auto baseVTable = baseClass->getVTableDescriptor();
auto offset = (baseVTable->getVTableOffset(baseClass) +
(baseMethod - baseClassMethods.data()));
swift_ptrauth_init_code_or_data(&classWords[offset],
descriptor.Impl.get(),
baseMethod->Flags.getExtraDiscriminator(),
!baseMethod->Flags.isAsync());
}
}
}
其内部是通过for循环编码,然后offset+index偏移,然后获取method,将其存入到偏移后的内存中,从这里可以印证函数是连续存放的
对于class中函数来说,类的方法调度是通过V-Table,其本质就是一个连续的内存空间(数组结构)
2.2 类类型
代码分析
class Teacher {
func teach() {
print("teach")
}
func teach1() {
print("teach1")
}
func teach2() {
print("teach2")
}
}
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
var t = Teacher()
t.teach() //断点
t.teach1()
t.teach2()
}
}
teach、teach1、teach2都是函数表派发
新增extension函数调度
extension Teacher {
func teach3() {
print("teach3")
}
}
类类型中,teach、teach1、teach2函数表派发, teach3函数被优化成直接调用
2.3 值类型
把class改成struct
struct Teacher {
func teach() {
print("teach")
}
func teach1() {
print("teach1")
}
func teach2() {
print("teach2")
}
}
我们可以看到值类型中,teach、teach1、teach2直接的地址调用
新增extension函数调度
extension Teacher {
func teach3() {
print("teach3")
}
}
我们可以看到值类型中,teach、teach1、teach2直接调用, teach3函数被优化成直接调用
2.4 NSObject子类
class Teacher: NSObject {
func teach() {
print("teach")
}
func teach1() {
print("teach1")
}
func teach2() {
print("teach2")
}
}
extension Teacher {
func teach3() {
print("teach3")
}
}
class MarkTeacher: Teacher {
func teach4() {
print("teach4")
}
}
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
var t = MarkTeacher()
t.teach() //断点
t.teach1()
t.teach2()
t.teach3()
t.teach4()
}
}
teach、teach1、teach2、teach4函数表派发, teach3函数被优化成直接调用。teach4用空间换时间的方式
查看SIL文件
sil_vtable Teacher {
#Teacher.teach: (Teacher) -> () -> () : @$s14ViewController7TeacherC5teachyyF // Teacher.teach()
#Teacher.teach1: (Teacher) -> () -> () : @$s14ViewController7TeacherC6teach1yyF // Teacher.teach1()
#Teacher.teach2: (Teacher) -> () -> () : @$s14ViewController7TeacherC6teach2yyF // Teacher.teach2()
#Teacher.init!allocator: (Teacher.Type) -> () -> Teacher : @$s14ViewController7TeacherCACycfC // Teacher.__allocating_init()
#Teacher.deinit!deallocator: @$s14ViewController7TeacherCfD // Teacher.__deallocating_deinit
}
sil_vtable MarkTeacher {
#Teacher.teach: (Teacher) -> () -> () : @$s14ViewController7TeacherC5teachyyF [inherited] // Teacher.teach()
#Teacher.teach1: (Teacher) -> () -> () : @$s14ViewController7TeacherC6teach1yyF [inherited] // Teacher.teach1()
#Teacher.teach2: (Teacher) -> () -> () : @$s14ViewController7TeacherC6teach2yyF [inherited] // Teacher.teach2()
#Teacher.init!allocator: (Teacher.Type) -> () -> Teacher : @$s14ViewController11MarkTeacherCACycfC [override] // MarkTeacher.__allocating_init()
#MarkTeacher.teach4: (MarkTeacher) -> () -> () : @$s14ViewController11MarkTeacherC6teach4yyF // MarkTeacher.teach4()
#MarkTeacher.deinit!deallocator: @$s14ViewController11MarkTeacherCfD // MarkTeacher.__deallocating_deinit
}
我们发现子类只继承了class中定义的函数,即函数表中的函数
其原因是因为子类将父类的函数表全部继承了,如果此时子类增加函数,会继续在连续的地址中插入,假设extension函数也是在函数表中,则意味着子类也有,但是子类无法并没有相关的指针记录函数是父类方法还是子类方法,所以不知道方法该从哪里插入,导致extension中的函数无法安全的放入子类中。所以在这里可以侧面证明extension中的方法是直接调用的,且只属于类,子类是无法继承的
2.5 总结
方法调度方式总结:
| 类型 | 调度方式 | extension |
|---|---|---|
| 值类型 | 静态派发 | 静态派发 |
| 类 | 函数表派发 | 静态派发 |
| NSObject子类 | 函数表派发 | 静态派发 |
extension
-
继承方法和属性,不能写在extension中。
-
而extension中创建的函数,一定是只属于自己类,但是其子类也有其访问权限,只是不能继承和重写
三、函数派发方式
3.1 final:
class Teacher {
final func teach() {
print("teach")
}
func teach1() {
print("teach1")
}
func teach2() {
print("teach2")
}
}
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
var t = Teacher()
t.teach() //断点
t.teach1()
t.teach2()
}
}
teach被优化成了直接地址调用, 就是静态派发。teach1、teach2函数调度都是首地址+偏移,也就是函数表派发
查看SIL文件
sil_vtable Teacher {
#Teacher.teach1: (Teacher) -> () -> () : @$s14ViewController7TeacherC6teach1yyF // Teacher.teach1()
#Teacher.teach2: (Teacher) -> () -> () : @$s14ViewController7TeacherC6teach2yyF // Teacher.teach2()
#Teacher.deinit!deallocator: @$s14ViewController7TeacherCfD // Teacher.__deallocating_deinit
}
添加了final关键字的函数无法被重写,使用静态派发,不会在vtable中出现,且对objc运行时不可⻅。
实际开发过程中属性,方法,类不需要被重载,可以使用final修饰
3.2 dynamic
函数均可添加dynamic关键字,为非objc类和值类型的函数赋予动态性,但派发方式还是函数表派发。
class Teacher {
dynamic func teach() {
print("teach")
}
func teach1() {
print("teach1")
}
func teach2() {
print("teach2")
}
}
我们看到teach、teach1、teach2函数都是函数表派发
类继承自NSObject
class Teacher: NSObject {
dynamic func teach() {
print("teach")
}
func teach1() {
print("teach1")
}
func teach2() {
print("teach2")
}
}
为非objc类和值类型的函数赋予动态性,但派发方式还是函数表派发。
使用dynamic的意思是可以动态修改,意味着当类继承自NSObject时,可以使用method-swizzling
场景:swift中实现方法交换
class Teacher {
dynamic func teach() {
print("teach")
}
func teach1() {
print("teach1")
}
func teach2() {
print("teach2")
}
}
extension Teacher {
@_dynamicReplacement(for: teach)
func teach3() {
print("teach3")
}
}
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
var t = Teacher()
t.teach3()
t.teach()
}
}
输出:
teach
teach
3.3 @objc:
该关键字可以将Swift函数暴露给Objc运行时,依旧是函数表派发。
class Teacher: NSObject {
@objc func teach() {
print("teach")
}
func teach1() {
print("teach1")
}
func teach2() {
print("teach2")
}
}
extension Teacher {
@objc func teach3() {
print("teach3")
}
}
我们查看SIL文件
// Teacher.teach() swift中的函数
sil hidden [ossa] @$s14ViewController7TeacherC5teachyyF : $@convention(method) (@guaranteed Teacher) -> () {
// %0 "self" // user: %1
bb0(%0 : @guaranteed $Teacher):
debug_value %0 : $Teacher, let, name "self", argno 1 // id: %1
%2 = integer_literal $Builtin.Word, 1 // user: %4
.......
} // end sil function '$s14ViewController7TeacherC5teachyyF'
// @objc Teacher.teach() OC中的函数,实际内部调用swift中的函数
sil hidden [thunk] [ossa] @$s14ViewController7TeacherC5teachyyFTo : $@convention(objc_method) (Teacher) -> () {
// %0 // user: %1
bb0(%0 : @unowned $Teacher):
%1 = copy_value %0 : $Teacher // users: %6, %2
%2 = begin_borrow %1 : $Teacher // users: %5, %4
// function_ref Teacher.teach()
%3 = function_ref @$s14ViewController7TeacherC5teachyyF : $@convention(method) (@guaranteed Teacher) -> () // user: %4
%4 = apply %3(%2) : $@convention(method) (@guaranteed Teacher) -> () // user: %7
end_borrow %2 : $Teacher // id: %5
destroy_value %1 : $Teacher // id: %6
return %4 : $() // id: %7
} // end sil function '$s14ViewController7TeacherC5teachyyFTo'
即在SIL文件中生成了两个方法
- swift原有的函数
- @objc标记暴露给OC来使用的函数: 内部调用swift的
3.4 @objc + dynamic: 消息派发的方式
用 @objc + dynamic 修饰方法,我们就可以使用runtime的api
class Teacher {
@objc dynamic func teach() {
print("teach")
}
func teach1() {
print("teach1")
}
func teach2() {
print("teach2")
}
}
extension Teacher {
@objc dynamic func teach3() {
print("teach3")
}
}
通过断点调试,teach使用消息派发的机制, 走的是objc_msgSend流程,即动态消息转发
原生的Swift类添加了@objc + dynamic,可以使用Runtime的方法进行交换
增加NSObject
class Teacher: NSObject {
@objc dynamic func teach() {
print("teach")
}
func teach1() {
print("teach1")
}
func teach2() {
print("teach2")
}
}
class MarkTeacher: Teacher {
@objc dynamic func teach4() {
print("teach4")
}
}
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
var t = MarkTeacher()
t.teach()
t.teach4()
}
}
SwiftDemo-Swift.h
原生的Swift类添加了@objc + dynamic,继承NSObject,可以做动态交换,还可以暴露给oc使用
四、函数内联
函数内联是一种编译器优化技术,它通过使用方法的内容替换直接调用该方法,从而优化性能。
- 将确保有时内联函数。这是默认行为,我们无需执行任何操作. Swift 编译器可能会自动内联函数作为优化。
- always - 将确保始终内联函数。通过在函数前添加 @inline(__always) 来实现此行为
- never - 将确保永远不会内联函数。这可以通过在函数前添加 @inline(never) 来实现。
- 如果函数很⻓并且想避免增加代码段大小,请使用@inline(never)(使用@inline(never))
如果对象只在声明的文件中可⻅,可以用 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 p = Person(sex: true)
p.test()
五、补充
5.1 汇编指令
- blr:带返回的跳转指令,跳转到指令后边跟随寄存器中保存的地址
- mov:将某一寄存器的值复制到另一寄存器(只能用于寄存器与起存起或者 寄存器与常量之间 传值,不能用于内存地址)
- mov x1, x0 将寄存器x0的值复制到寄存器x1中
- ldr:将内存中的值读取到寄存器中
- ldr x0, [x1, x2] 将寄存器x1和寄存器x2 相加作为地址,取该内存地址的值翻入寄存器x0中
- str:将寄存器中的值写入到内存中
- str x0, [x0, x8] 将寄存器x0的值保存到内存[x0 + x8]处
- bl:跳转到某地址
5.2 ASDL:
下面是针对函数地址的一个验证
- 通过运行发现,Mach-O中的地址与调试时直接获取的地址是由一定偏差的,其主要原因是实际调用时地址多了一个ASLR(地址空间布局随机化 address space layout randomizes)
struct Teacher {
var age: Int = 18
func teach(){
print("speak")
}
}
var t = Teacher()
t.teach()
SwiftTest`main:
0x100003a18 <+0>: sub sp, sp, #0x40 ; =0x40
0x100003a1c <+4>: stp x29, x30, [sp, #0x30]
0x100003a20 <+8>: add x29, sp, #0x30 ; =0x30
0x100003a24 <+12>: bl 0x100003c28 ; SwiftTest.Teacher.init() -> SwiftTest.Teacher at main.swift:11
0x100003a28 <+16>: mov x8, x0
0x100003a2c <+20>: adrp x9, 5
0x100003a30 <+24>: str x9, [sp]
0x100003a34 <+28>: adrp x0, 5
0x100003a38 <+32>: add x0, x0, #0x150 ; =0x150
0x100003a3c <+36>: str x8, [x9, #0x150]
0x100003a40 <+40>: add x1, sp, #0x18 ; =0x18
0x100003a44 <+44>: str x1, [sp, #0x8]
0x100003a48 <+48>: mov w8, #0x20
0x100003a4c <+52>: mov x2, x8
0x100003a50 <+56>: mov x3, #0x0
0x100003a54 <+60>: bl 0x100003d98 ; symbol stub for: swift_beginAccess
0x100003a58 <+64>: ldr x8, [sp]
0x100003a5c <+68>: ldr x0, [sp, #0x8]
0x100003a60 <+72>: ldr x8, [x8, #0x150]
0x100003a64 <+76>: str x8, [sp, #0x10]
0x100003a68 <+80>: bl 0x100003db0 ; symbol stub for: swift_endAccess
0x100003a6c <+84>: ldr x0, [sp, #0x10]
-> 0x100003a70 <+88>: bl 0x100003ab0 ; SwiftTest.Teacher.teach() -> () at main.swift:14 直接地址调用,即静态派发
0x100003a74 <+92>: mov w0, #0x0
0x100003a78 <+96>: ldp x29, x30, [sp, #0x30]
0x100003a7c <+100>: add sp, sp, #0x40 ; =0x40
0x100003a80 <+104>: ret
-
可以通过image list查看,其中0x0000000100000000是程序运行的首地址,后8位是随机偏移00000000(即ASLR)
(lldb) image list [ 0] 54AE1596-8635-3684-9671-254ED47F9017 0x0000000100000000 /Users/jxwbjmac0003/Library/Developer/Xcode/DerivedData/SwiftTest-ezgreflbigvpqjcibzqxeikfdqsy/Build/Products/Debug/SwiftTest [ 1] 38657979-1ABE-3C9A-BF64-EF3B746216AB 0x0000000100014000 /usr/lib/dyld [ 2] A23D1D3A-AD28-3AC2-AEAF-53F4B7A5B2F5 0x000000018a3f2000 -
将Mach-O中的文件地址0x100003ab0 + 0x00000000 = 0x100003ab0,正好对应上面调用的地址
还可以通过终端命令nm,获取项目中的符号表
- 查看符号表:
nm mach-o文件路径
// 获取所有符号
nm /Users/jxwbjmac0003/Library/Developer/Xcode/DerivedData/SwiftTest-ezgreflbigvpqjcibzqxeikfdqsy/Build/Products/Debug/SwiftTest
- 通过命令还原符号名称:
xcrun swift-demangle 符号
// 还原第一个符号
% xcrun swift-demangle s14ViewController7TeacherC5teachyyF
$s14ViewController7TeacherC5teachyyF ---> ViewController.Teacher.teach() -> ()