Swift进阶(一)——类与结构体上

198 阅读4分钟

类与结构体

首先我们必须明确一点,类是引用类型,而结构体是值类型,一般情况下,类是存储在堆上,类类型的变量中存储的是地址,而结构体是在栈上,存储的是具体的实例。类通过一段具体的代码我们就可以清楚的认识他们:

截屏2022-01-14 上午10.44.07.png

可以清楚的看到,tt1指向的是同一个地址,(实例对象分配过程:首先栈上会分配8字节的空间存储实例对象的引用,接着在堆上寻找合适的内存区域并把内存地址返回回来,把当前value拷贝到内存空间中,接着把栈上的内存地址指向堆区,从而完成实例对象的内存分配) 而值类型则不然,同样以一段代码,观察:

截屏2022-01-14 下午3.44.47.png

从代码中,可以清晰看到st.age改变不影响stu.age的改变,所以st相当于是对stu的一份拷贝。我们也可以查看下它的内存分布,通过 frame variable -L xxx

截屏2022-01-20 下午2.51.11.png

上图我们很明显可以看到,ststu在两个不同的内存地址上,而且都在栈区。

内存插件:libfooplugin.dylib,lldab调试:cat address 内存地址(如0x00ff)

接下来,我们需要对内存区域有个基本的认知,可以看下这张图:

截屏2022-01-14 上午11.01.16.png

  • Heap:存储所有对象;
  • Global:存储全局变量;常量;代码区;
  • Segment & Section: 我们常研究的内存结构就是Mach-o文件, Mach-o文件有多个段(Segment),每个段有不同的功能,每个段又分为很多小的Section
    • TEXT.text: 机器码
    • TEXT.cstring:硬编码的字符串
    • TEXT.const: 初始化过的常量
    • DATA.data: 初始化过的可变的(静态/全局)数据
    • DATA.const: 没有初始化过的常量
    • DATA.bss: 没有初始化的(静态/全局)变量
    • DATA.common: 没有初始化过的符号声明 我们可以通过几段示例验证下:

截屏2022-01-20 下午1.55.41.png

上图明确看到,初始化过的变量存放在DATA.data

截屏2022-01-20 下午2.21.44.png

上图可以看到,硬编码的字符串,放在TEXT.cstring

类的生命周期

ios开发的语言不论是OC还是swift,后端都是通过llvm进行编译的,如下图:

截屏2022-01-20 下午3.56.28.png

OC通过clang编译器,编译成IR,再生成可执行文件.O,Swift则通过Swift编译器生成IR,再生成可执行文件,具体流程如下图:

截屏2022-01-20 下午4.02.24.png

常用编译命令:

  • swiftc main.swift -dump-parse //分析输出AST
  • swiftc main.swift -dump-ast //分析并检查类型输出AST
  • swiftc main.swift -emit-silgen //生成中间体语言(SIL),为优化的
  • swiftc main.swift -emit-sil //生成中间体语言,优化后的
  • swiftc main.swift -emit-ir //生成llvm中间体语言(.ll文件)
  • swiftc main.swift -emit-bc //生成llvm中间体语言(.bc文件)
  • swiftc main.swift -emit-assembly //生成汇编
  • swiftc -o main.o main.swift //编译生成可执行.out文件

接下来,我们看下生成SIL文件的示例:

通过把相关编译命令写在脚本里,然后运行生成并自动打开SIL文件

截屏2022-01-20 下午4.33.35.png

生成的SIL文件:

截屏2022-01-20 下午7.09.19.png

xcrun swift-demangle 还原混写名称
SIL部分代码解析:alloc_global分配一个全局变量,global_addr拿到CTPerson这个全局变量给到%3metatype $thick CTPerson.Type,获取CTPerson.Type的元类型,function_ref @CTPerson.__allocating_init():获取到__allocating_init()函数指针 @main: 入口函数 @是一种标识
%x:寄存器(虚拟),一旦赋值,不可被更改

通过断点调试我们能知道Swift内存分配的前两步__allocating_init()->swift_allocObject

截屏2022-01-20 下午7.24.28.png

截屏2022-01-20 下午7.25.10.png 接下来我们去源码中看详细的步骤,直接在HeapObject.cpp中搜swift_allocObject,我们可以看到

截屏2022-01-20 下午7.33.47.png

截屏2022-01-20 下午7.37.54.png

源码中可以看到,最终swift_allocObject调用的_swift_allocObject_,_swift_allocObject_调用swift_slowAlloc,最终调用malloc分配堆空间。

对象分配流程:__allocating_init()-->swift_allocObject-->swift_allocObject-->swift_slowAlloc-->malloc

截屏2022-01-21 上午10.23.20.png 从源码中,可以看到swift对象的内存结构:HeapObject,以及它的两个属性:metadata(HeapMetadata const *__ptrauth_objc_isa_pointer metadata;)、refCounts(64位的位域信息相当于8字节),默认占16字节大小。
进一步跟进源码,看HeapMetadata的具体信息,它是TargetHeapMetadata的别名:

截屏2022-01-21 下午1.51.40.pngTargetHeapMetadata继承于TargetMetadata,在swift下传入的是个MetadataKind,而oc下传入的是isa,这里其实只是为了兼容oc,本质上没什么区别,下图源码:

截屏2022-01-21 下午1.56.33.png 源码中MetadataKind只是个uint32的数据类型,并没有其他信息,我们想看类的具体信息,只能去父类TargetMetadata中寻找:

截屏2022-01-21 下午2.02.44.png

截屏2022-01-21 下午2.22.27.png 我们会发现一个关键的函数getTypeContextDescriptor(),通过观察它基本可以确定TargetMetadata是所有元类的基类:

截屏2022-01-21 下午2.39.59.png

MetadataKind::Class:{ const auto cls = static_cast<const TargetClassMetadata *>(this); }
当前MetadataKind是Class时,当前的this指针会被强转为TargetClassMetadata类型,TargetClassMetadata会不会是我们要找的类型呢?我们看下它具体的内容

截屏2022-01-21 下午4.07.13.png 根据这个TargetClassMetadata类的内容,我们基本可以窥探到metadata所有的内容了,从而得出swift类的数据结构:

struct Metadata {
    var kind:Int
    var superClass:Any.Type
    var cacheData:(Int,Int)
    var data:Int
    var classFlags:Int32
    var instanceAddressPoint:UInt32
    var instancSize:UInt32
    var instanceAlignmentMask:UInt16
    var reserved:UInt16
    var classSize:UInt32
    var classAddressPoint:UInt32
    var typeDescriptor:UnsafeMutableRawPointer
    var iVarDestroyer:UnsafeRawPointer
}

根据源码中观察到的类的数据结构,我们用代码做了最终还原:

截屏2022-01-21 下午4.52.25.png

可以清楚看到,相关对应的值都已赋值成功。