iOS疑问之Swift 探索结构体和类

127 阅读11分钟

一、类与结构体区别深入探索

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 命令的时候可以发现 gg1 的值是不一样的。

(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、内存区域

neicun.png

栈区(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文件的生成

image (2).png

LLVM 是一套 C++ 编写的 架构编译器的框架系统 ,用于优化任意程序语言编写的编译时间 complie-time ,链接时间 link-time 、运行时间 run-time 、空闲时间 idle-time ,Swift 通过 Swift编译器 编译为 IR中间代码 ,交给 LLVM 进行优化,最终形成 .o机器执行文件 类似于 单片机hex文件

2、Swift的编译流程

通过一张图

编组1.png

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
// 返回一个 intreturn %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 代表什么,可以通过符号断点到汇编跟踪查找。

image (1).png 找到了 __allocating_init() 在此处进入查看, 按住control 点击下一步

image (3).png

定位到了 swift_allocObject ,继续深入添加符号断点

image (4).png

swift_slowAlloc 继续往下

image (5).png

malloc_zone_malloc 这就是给对象创建内存空间了

  • 总结 swift 实例对象创建流程

__allocating_init -> swift_allocObject -> _swift_allocObject_ -> swift_slowAlloc -> malloc_zone_malloc

打开 swift 源码地址 SwiftSource 全局找 _swift_allocObject_

image (6).png

image (7).png

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 结构体的属性

image.png

HeapObjectHeapMetadata 元数据 和 refCounts 引用计数构成。大小的话需要看HeapMetadata 结构体的成员变量

image.png TargetHeapMetadata 继续找到 TargetMetadata

image.png 最后看到,只有一个 Kind StoredPointer 源码无法找到StoredPointer

4、StoredPointer

通过注释可知用 getKind() 方法入参。找到 getEnumeratedMetadataKind

 /// Get the metadata kind.
  MetadataKind getKind() const {
    return getEnumeratedMetadataKind(Kind);
  }

image.png

由此可知 kinduint64_t 类型占 8字节

MetadataKindgetKind() 返回值的类型,进入

image.png

MetadataKind.def 里面记录了所以所以类型的元数据,整理后

name value
Class0x0
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

image.png

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 强转 ClassMetadataTargetMetadataTargetClassMetadata 本质上一样,结构体就是 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

image.png

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

image (9).png

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、动态派发不会被内联