一、类与结构体区别深入探索
1、写法
/// 结构体
struct GoodGame {
var age: Int
var name: String
}
/// 类
class GoodGamea {
var age: Int
var name: String
init(age: Int, name: String) {
self.age = age
self.name = name
}
}
2、区别于联系
相同点:
- 定义了存储值的属性
- 定义方法
- 使用extenision来扩展
- 遵循协议
- 定义下标还有下标语法访问值
- 定义初始化器
不同点:
- 类可以继承
- 引用计数可以对一个类实例有多个引用
- 类有析构函数来释放占用的资源deinit
- 类型转化能在运行时,检查和解释类实例的类型
- 类值引用、结构体值类型
- 类存储在堆Heap、结构体同存储在栈Stack
引用类型代表的是,类类型的值并不是直接存储示例对象,是对存储具体实例对象内存地址的引用,引用类型 -> 内存地址
class GoodGamea {
var age: Int
var name: String
init(age: Int, name: String) {
self.age = age
self.name = name
}
}
var g = GoodGamea(age: 22, name: "123")
var g1 = g
g1.age = 20
通过lldb,来查看 g
g1
的内存地址
(lldb) x/4xg g
0x108e05c90: 0x0000000100008218 0x0000000800000003
0x108e05ca0: 0x0000000000000014 0x0000000000333231
(lldb) x/4xg g1
0x108e05c90: 0x0000000100008218 0x0000000a00000003
0x108e05ca0: 0x0000000000000014 0x0000000000333231
可以看出来 g
g1
指向同一个内存。并且 age
的值也可以验证这点
(lldb) po g.age
20
(lldb) po g1.age
20
结构体struct
struct GoodGame {
var age: Int
var name: String
}
var g = GoodGame(age: 22, name: "123")
var g1 = g
g1.age = 20
我看可以lldb查看内存空间的时候会读取不到, p
命令的时候可以发现 g
和 g1
的值是不一样的。
(lldb) x/4gx g
error: invalid start address expression.
error: address expression "g" resulted in a value whose type can't be converted to an address: test.GoodGame
(lldb) p g
(test.GoodGame) $R1 = (age = 22, name = "123")
(lldb) p g1
(test.GoodGame) $R2 = (age = 20, name = "123")
(lldb)
通过lldb的调试, x/4gx g
无法输出,说明了类与结构体的存储位置不同
3、内存区域
栈区(stack):函数内部的局部变量
堆区(heap):存储对象
全局区(Global):全局变量,常量,代码区
可以通过lldb的 frame variable -L
, frame
内存布局的意思。variable
代表想查看的某个变量。-L
-L ( --location ) Show variable location information.
- 结构体
struct GoodGame {
var age: Int
var name: String
}
(lldb) frame variable -L g
0x00000003041333a0: (test.GoodGame) g = {
0x00000003041333a0: age = 22
0x00000003041333a8: name = "123"
}
stack
中 存放 age
name
- 类
class GoodGamea {
var age: Int
var name: String
init(age: Int, name: String) {
self.age = age
self.name = name
}
}
var g = GoodGamea(age: 22, name: "123")
(lldb) frame variable -L g
scalar: (LGSwiftTest.GoodGamea) g = 0x0000000108b5d110 {
0x0000000108b5d120: age = 22
0x0000000108b5d128: name = "123"
}
(lldb) frame variable -R g
(LGSwiftTest.GoodGamea) g = 0x0000000108b5d110 {
age = {
_value = 22
}
name = {
_guts = {
_object = {
_countAndFlagsBits = {
_value = 3355185
}
_object = 0xe300000000000000
}
}
}
}
通过 frame variable -L g
frame variable -R g
。可以看到层级 g
指向 0x0000000108b5d110
0x0000000108b5d110
包含了 age
name
4、总结以及实际用途
类是引用类型,结构体值类型。需要继承就是要类,如果不需要继承使用结构体是不错的选择。
类与结构体灵活的使用可以更好的分配空间,是存放 堆
或者 栈
,对于效率是有影响的。
这也是项目优化的一个角度。
二、类的初始化
1、类和结构体的初始化器
Swift中创建类和结构体时必须为所以属性设置合适的初始值,初始化器在初始化完成之前,不能调用任何实例方法,读取任何实例值,也不能引用self
- 结构体会提供默认的初始化方法,(initializer,初始化方法,构造器,构造方法)
- 类没用默认的初始化方法,必须提供对应的初始化器(也可以是便捷初始化器)
func test() {
struct GoodGame {
var age: Int = 22
var name: String = "123"
}
var g1 = GoodGame()
print("end")
}
test()
func test() {
struct GoodGame {
var age: Int
var name: String
init () {
age = 20
name = "123"
}
}
var g1 = GoodGame()
print("end")
}
test()
通过汇编可以看到,对比后完全一样
test`init() in GoodGame #1 in test():
0x100001900 <+0>: pushq %rbp
...
0x10000197e <+126>: retq
test`init() in GoodGame #1 in test():
0x100001900 <+0>: pushq %rbp
...
0x10000197e <+126>: retq
2、便捷初始化器
/////// 类
class GoodGamea {
var age: Int
var name: String
init(age: Int, name: String) {
self.age = age
self.name = name
}
convenience init () {
self.init(age:22, name:"123")
}
}
便捷初始化器必须从相同的类调用另一个初始化器
- 便捷初始化器必须先委托同类中的其他初始化器,然后再为任意属性赋值,否则便捷初始化器赋予的新值,将被自己类中其他指定初始化器覆盖
3、指定初始化器
class GoodGamea {
var age: Int
var name: String
init(age: Int, name: String) {
self.age = age
self.name = name
}
convenience init () {
self.init(age:22, name:"123")
}
}
class LOL: GoodGamea {
var subName: String
init(subName: String) {
self.subName = subName
}
}
当 GoodGamea
派生一个 LOL
指定初始化器后,会报错。
需加上 super.init(age: 12, name: "124")
- 指定初始化器必须保证在向上委托给父类初始化器之前,其所在类引入所以属性都要初始化完成。
- 指定初始化器必须先向上委托父类初始化器,然后才能为继承的属性设置心智,否则指定初始化器富裕的新值将被父类中的初始化器覆盖
4、可失败初始化器
就是可以 retuen nil
的情况
class GoodGamea {
var age: Int
var name: String
init?(age: Int, name: String) {
if age < 18 {return nil}
self.age = age
self.name = name
}
convenience init? () {
self.init(age:22, name:"123")
}
}
5、必要初始化器
在初始化器前添加 required
修饰符。来表明必须实现
三、类的生命周期
1、.o文件的生成
LLVM
是一套 C++
编写的 架构编译器的框架系统
,用于优化任意程序语言编写的编译时间 complie-time
,链接时间 link-time
、运行时间 run-time
、空闲时间 idle-time
,Swift 通过 Swift编译器
编译为 IR中间代码
,交给 LLVM
进行优化,最终形成 .o机器执行文件
类似于 单片机
的 hex文件
2、Swift的编译流程
通过一张图
1、Swift源码经过 parse解析
、 ast编译
、 生成 AST语法树
swiftc
指令进行生成, 命令行
cd到 main.swift
的根目录
- 分析输出AST
swiftc main.swift -dump-parse >> 输出的文件
- 分析并且检查类型输出AST
swiftc main.swift -dump-ast >> main.ast
- 生成中间体语言 SIL(未优化)
swiftc main.swift -emit-silgen >> main.sil
- 生成中间体语言 SIL(优化)
swiftc main.swift -emit-sil >> main.sil
- 生成LLVM中间语言.ll文件
swiftc main.swift -emit-ir >> main.ll
- 生成LLVM中间语言.bc文件
swiftc main.swift -emit-bc >> main.bc
- 生成汇编语言
swiftc main.swift -emit-assembly >>
- 编译生成可执行.o文件.out
swiftc -o main.o main.swift
2、Swift对象内存分配
上述得到的llvm编译之前的 sil文件
是Swift编译到底层的代码。通过 main
(@main 表示当前的入口文件,%0 %1,叫做寄存器)来定位有用的代码,简单读一遍我们注意到 // function_ref GoodGamea.__allocating_init()
这行代码
// main
sil @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 {
bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>):
// s4main1gAA9GoodGameaCvp main.GoodGamea 创建全局GoodGamea
alloc_global @$s4main1gAA9GoodGameaCvp // id: %2
// 全局地址 = *GoodGamea 读取GoodGamea的地址 给%3
%3 = global_addr @$s4main1gAA9GoodGameaCvp : $*GoodGamea // user: %7
// 赋值
%4 = metatype $@thick GoodGamea.Type // user: %6
// function_ref GoodGamea.__allocating_init()
// 定义函数 %5 = function_ref
%5 = function_ref @$s4main9GoodGameaCACycfC : $@convention(method) (@thick GoodGamea.Type) -> @owned GoodGamea // user: %6
// 调用 函数%5 ,参数是%4 结果是%6
%6 = apply %5(%4) : $@convention(method) (@thick GoodGamea.Type) -> @owned GoodGamea // user: %7
// 将%6 存到 %3(GoodGamea)
store %6 to %3 : $*GoodGamea // id: %7
%8 = integer_literal $Builtin.Int32, 0 // user: %9
%9 = struct $Int32 (%8 : $Builtin.Int32) // user: %10
// 返回一个 int 值
return %9 : $Int32 // id: %10
} //
有很多代码是被混淆过的,用xcrun swift-demangle 你的混淆的代码
。查看
xcrun swift-demangle s4main1gAA9GoodGameaCvp
$s4main1gAA9GoodGameaCvp ---> main.g : main.GoodGamea
构建一个全局变量 GoodGamea
,对其 Type
做了一个返回处理。定位到 allocating_init()
// GoodGamea.__allocating_init()
sil hidden [exact_self_class] @$s4main9GoodGameaCACycfC : $@convention(method) (@thick GoodGamea.Type) -> @owned GoodGamea {
// %0 "$metatype"
bb0(%0 : $@thick GoodGamea.Type):
// GoodGamea的 alloc_ref 方法地址给 %1
%1 = alloc_ref $GoodGamea // user: %3
// function_ref GoodGamea.init()
// %2 拿到 GoodGamea.init()函数地址
%2 = function_ref @$s4main9GoodGameaCACycfc : $@convention(method) (@owned GoodGamea) -> @owned GoodGamea // user: %3
// 调用%2 参数%1 用 GoodGamea.init() 参数 %1
%3 = apply %2(%1) : $@convention(method) (@owned GoodGamea) -> @owned GoodGamea // user: %4
// 返回%3实例对象
return %3 : $GoodGamea // id: %4
} // end sil function '$s4main9GoodGameaCACycfC'
一通操作下来 GoodGamea()
完成了对象的创建。和oc的
- (id)initWithCoder: (NSCoder *)coder {
return [super init];
}
有点类似。有个问题 Type
代表什么,可以通过符号断点到汇编跟踪查找。
找到了 __allocating_init()
在此处进入查看, 按住control
点击下一步
定位到了 swift_allocObject
,继续深入添加符号断点
swift_slowAlloc
继续往下
malloc_zone_malloc
这就是给对象创建内存空间了
- 总结 swift 实例对象创建流程
__allocating_init -> swift_allocObject -> _swift_allocObject_ -> swift_slowAlloc -> malloc_zone_malloc
打开 swift
源码地址 SwiftSource 全局找 _swift_allocObject_
malloc
就是开辟内存空间了
static HeapObject *_swift_allocObject_(HeapMetadata const *metadata,
size_t requiredSize,
size_t requiredAlignmentMask) {
assert(isAlignmentMask(requiredAlignmentMask));
auto object = reinterpret_cast<HeapObject *>(
swift_slowAlloc(requiredSize, requiredAlignmentMask));
// NOTE: this relies on the C++17 guaranteed semantics of no null-pointer
// check on the placement new allocator which we have observed on Windows,
// Linux, and macOS.
new (object) HeapObject(metadata);
// If leak tracking is enabled, start tracking this object.
SWIFT_LEAKS_START_TRACKING_OBJECT(object);
SWIFT_RT_TRACK_INVOCATION(object, swift_allocObject);
return object;
}
object
已经生成了有个细节 new (object) HeapObject(metadata);
swift_slowAlloc开辟内存空间后还有个 auto object = reinterpret_cast<HeapObject *>( swift_slowAlloc(requiredSize, requiredAlignmentMask));
操作。是通过 reinterpret_cast
将指针转为HeapObject
类型然后 new (object) HeapObject(metadata);
这里的逻辑,object 是强转的HeapObject类型,值其实还是指向内存空间的对象指针,new (object) HeapObject(metadata);
这个等于初始化。流程
HeapObject
结构体的属性
HeapObject
由 HeapMetadata
元数据 和 refCounts
引用计数构成。大小的话需要看HeapMetadata
结构体的成员变量
TargetHeapMetadata
继续找到 TargetMetadata
最后看到,只有一个 Kind
StoredPointer
源码无法找到StoredPointer
。
4、StoredPointer
通过注释可知用 getKind()
方法入参。找到 getEnumeratedMetadataKind
/// Get the metadata kind.
MetadataKind getKind() const {
return getEnumeratedMetadataKind(Kind);
}
由此可知 kind
是 uint64_t
类型占 8字节
MetadataKind
是getKind()
返回值的类型,进入
MetadataKind.def
里面记录了所以所以类型的元数据,整理后
name | value | |
---|---|---|
Class | 类 | 0x0 |
Struct | 结构体 | 0x200 |
Enum | 枚举 | 0x201 |
Optional | 可选类型 | 0x202 |
ForeignClass | 外类 | 0x203 |
Opaque | 不透明类型 | 0x300 |
Tuple | 元组 | 0x301 |
Function | 方法 | 0x302 |
Existential | 0x303 | |
Metatype | 元类型 | 0x304 |
ObjCClassWrapper | 0x305 | |
ExistentialMetatype | 0x306 | |
HeapLocalVariable | 0x400 | |
HeapGenericLocalVariable | 0x500 | |
ErrorObject | 0x501 | |
LastEnumerated | 0x7ff |
我们在 TargetMetaData
结构体中,找到方法 getClassObject
,匹配kind返回值 TargetClassMetadata
。
template<> inline const ClassMetadata *
Metadata::getClassObject() const {
switch (getKind()) {
case MetadataKind::Class: {
// Native Swift class metadata is also the class object.
return static_cast<const ClassMetadata *>(this);
}
case MetadataKind::ObjCClassWrapper: {
// Objective-C class objects are referenced by their Swift metadata wrapper.
auto wrapper = static_cast<const ObjCClassWrapperMetadata *>(this);
return wrapper->Class;
}
// Other kinds of types don't have class objects.
default:
return nullptr;
}
}
如果是 Class
直接 this
强转 ClassMetadata
, TargetMetadata
和 TargetClassMetadata
本质上一样,结构体就是 TargetClassMetadata
template <typename Runtime>
struct TargetClassMetadata : public TargetAnyClassMetadata<Runtime> {
...
/// 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;
...
}
继承链 : TargetClassMetadata -> TargetAnyClassMetadata -> TargetHeapMetadata
- Class的属性等于
TargetClassMetadata + TargetAnyClassMetadata + TargetHeapMetadata
。
TargetClassMetadata
有
ClassFlags Flags; // 4字节
uint32_t InstanceAddressPoint; // 4字节
uint32_t InstanceSize; // 4字节
uint16_t InstanceAlignMask; // 2字节
uint16_t Reserved; // 2字节
uint32_t ClassSize; // 4字节
uint32_t ClassAddressPoint; // 4字节
TargetAnyClassMetadata
有
TargetHeapMetadata<Runtime>(MetadataKind::Class), //kind
Superclass(superclass) // superClass
#if SWIFT_OBJC_INTEROP
, CacheData{nullptr, nullptr}, // cacheData
Data(SWIFT_CLASS_IS_SWIFT_MASK) // data
5、 refCounts
RefCounts
是个class 类型,本质 是指针,占8字节
总结:
1、swift 类本质是 HeapObject
2、HeapObject
默认大小16字节: metadata
(struct) 8字节。 RefCounts
(class) 8字节
四、异变方法
1、值类型属性不能被自身修改
struct Point {
var x = 0.0, y = 0.0
func moveBy(x delatX: Double, y delatY: Double) {
self.x += delatX
self.y += delatY
}
}
如上图代码会报错 Left side of mutating operator isn't mutable: 'self' is immutable
,
class Point {
var x = 0.0, y = 0.0
func moveBy(x delatX: Double, y delatY: Double) {
self.x += delatX
self.y += delatY
}
}
如果是类就不会保存。在方法里面修改自身。通过编译器我们为 struct
加上 mutating
就可修改结构体自身属性。通过SIL文件
swiftc -emit-silgen -target
五、方法调度
class testClassFunc {
func test() {
print("testClassFunc")
}
}
struct testStructFunc {
func test() {
print("testStruct")
}
}
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let testClass = testClassFunc()
testClass.test()
let tStruct = testStructFunc()
tStruct.test()
通过汇编查看。M1电脑可以Rosetta模式运行
0x100bb1e54 <+100>: callq 0x100bb1ec0 ; type metadata accessor for testOne.testClassFunc at <compiler-generated>
0x100bb1e59 <+105>: movq %rax, %r13
0x100bb1e5c <+108>: callq 0x100bb1cd0 ; testOne.testClassFunc.__allocating_init() -> testOne.testClassFunc at ViewController.swift:11
0x100bb1e61 <+113>: movq %rax, %r13
0x100bb1e64 <+116>: movq %r13, -0x30(%rbp)
0x100bb1e68 <+120>: movq %r13, -0x28(%rbp)
0x100bb1e6c <+124>: movq (%r13), %rax
0x100bb1e70 <+128>: movq 0x50(%rax), %rax
0x100bb1e74 <+132>: callq *%rax
-> 0x100bb1e76 <+134>: callq 0x100bb1de0 ; testOne.testStruct.init() -> testOne.testStruct at ViewController.swift:17
0x100bb1e7b <+139>: callq 0x100bb1d20 ; testOne.testStruct.test() -> () at ViewController.swift:18
struct 和 class 的区别
1、直接
struct 结构体方法
2、间接
类方法
mirror
机制找到 Metadata
,确定函数值(metadata + 偏移量),执行函数,
Metadata
的数据结构为组成为
var kind: Int
var supreClass: Any.Type
var cacheData: (Int, Int)
var data: Int
var classFlags: Int32
var InstanceAddressPoint: Int32
var InstanceSize: uint32_t
var InstanceAlignMask: uint16_t
var Reserved: uint16_t
var ClassAddressPoint: uint32_t
var typeDecriptor: UnsafeMutableRawPointer
var ivaDesctroyer: UnsafeRawPointer
TargetClassDescriptor
就是就是对类的一个描述
template <typename Runtime>
class TargetClassDescriptor final
: public TargetTypeContextDescriptor<Runtime>,
public TrailingGenericContextObjects<TargetClassDescriptor<Runtime>,
TargetTypeGenericContextDescriptorHeader,
/*additional trailing objects:*/
TargetResilientSuperclass<Runtime>,
TargetForeignMetadataInitialization<Runtime>,
TargetSingletonMetadataInitialization<Runtime>,
TargetVTableDescriptorHeader<Runtime>,
TargetMethodDescriptor<Runtime>,
TargetOverrideTableHeader<Runtime>,
TargetMethodOverrideDescriptor<Runtime>,
TargetObjCResilientClassStubInfo<Runtime>,
TargetCanonicalSpecializedMetadatasListCount<Runtime>,
TargetCanonicalSpecializedMetadatasListEntry<Runtime>,
TargetCanonicalSpecializedMetadataAccessorsListEntry<Runtime>,
TargetCanonicalSpecializedMetadatasCachingOnceToken<Runtime>>
V-Table
总结:方法调度
类型 | 调度方式 | extension |
---|---|---|
值类型 | 静态派发 | 静态派发 |
类 | 函数表派发 | 静态派发 |
NSObject子类 | 函数表派发 | 静态派发 |
六、影响派发
1、final
添加了final关键字的函数无法被重写,使用的是静态派发,不会在V-Table中出现,且对objc运行时不可见。
2、dynamic
函数均可添加dynamic关键字,为非objc类和值类型的函数赋予动态性,但派发方式还是函数表派发。
3、@objc
该关键字将swift函数暴露给Objc运行时,依旧是函数表派发
4、@objc + dynamic
消息派发的方式
七、函数内联
内联函数(inline Function):
如果开启了编译器优化,编译器会自动将某些函数变成内联函数,(将函数调用展开成函数体),release模式默认开启,并且按照速度优化。
有些函数不会内联:
1、函数体较长。调用次数多,因为进行内联会生成过多的汇编代码。
2、递归调用不会被内联
3、动态派发不会被内联