Swift之深入解析“对象”的底层原理

Swift 编译简介
  • Swift 的编译环境配置和编译流程,请参考我之前的博客:Swift之源码编译的环境搭建和编译流程
  • 新建一个 Swift 工程,在 main.swift 中创建一个 YDWTeacher 类,并通过默认的初始化器,创建一个实例对象并赋值给 t,如下:
	class YDWTeacher {
    
     
		var age: Int = 18 
		var name: String = "YDW"  
	} 
	let t = YDWTeacher()
  • 然后在终端中查看抽象语法树:swiftc -dump-ast main.swift,如下:

在这里插入图片描述

  • 接下来,要研究的是这个初始化器到底做了一个什么样的操作?因此引入 SIL (Swift intermediate language);

  • iOS 的开发语言,不管是 OC 还是 Swift,底层都是通过 LLVM 编译的,生成.o可执行文件,如下所示:
    在这里插入图片描述

  • 不难看出:

    • OC 中通过 clang 编译器,编译成 IR,然后再生成可执行文件.o(即机器码);
    • swift 中通过 swiftc 编译器,编译成 IR,然后再生成可执行文件;
  • 再来看一下:一个 Swift 文件的编译过程经历哪些步骤:
    在这里插入图片描述

  • 下面是 Swift 中的编译流程,其中SIL(Swift Intermediate Language),是 Swift 编译过程中的中间代码,主要用于进一步分析和优化 Swift 代码。如下图所示,SIL 位于在 AST 和 LLVM IR 之间:

在这里插入图片描述

  • Swift 与 OC 的区别在于 Swift 生成了高级的 SIL;Swift 在编译的过程中使用的前端编译器是 Swiftc,和我们之前的 OC 中使用的 clang 是有所区别的。
  • 通过 swiftc -h 终端命令,查看 swiftc 能做什么:

在这里插入图片描述

  • 分析说明:
    • -dump-ast 语法和类型检查,打印AST语法树
    • -dump-parse 语法检查,打印AST语法树
    • -dump-pcm 转储有关预编译Clang模块的调试信息
    • -dump-scope-maps expanded-or-list-of-line:column
      Parse and type-check input file(s) and dump the scope map(s)
    • -dump-type-info Output YAML dump of fixed-size types from all imported modules
    • -dump-type-refinement-contexts
      Type-check input file(s) and dump type refinement contexts(s)
    • -emit-assembly Emit assembly file(s) (-S)
    • -emit-bc 输出一个LLVM的BC文件
    • -emit-executable 输出一个可执行文件
    • -emit-imported-modules 展示导入的模块列表
    • -emit-ir 展示IR中间代码
    • -emit-library 输出一个dylib动态库
    • -emit-object 输出一个.o机器文件
    • -emit-pcm Emit a precompiled Clang module from a module map
    • -emit-sibgen 输出一个.sib的原始SIL文件
    • -emit-sib 输出一个.sib的标准SIL文件
    • -emit-silgen 展示原始SIL文件
    • -emit-sil 展示标准的SIL文件
    • -index-file 为源文件生成索引数据
    • -parse 解析文件
    • -print-ast 解析文件并打印(漂亮/简洁的)语法树
    • -resolve-imports 解析import导入的文件
    • -typecheck 检查文件类型
SIL
一、什么是 SIL 分析?
  • SIL 依赖于 swift 的类型系统和声明,所以 SIL 语法是 swift 的延伸。一个 sil 文件是一个增加了SIL定义的swift源文件;
  • SIL 文件中没有隐式 import,如果使用 swift 或者 Buildin 标准组件的话必须明确的引入;
  • SIL 函数由一个或多个block组成,一个block是一个指令的线性序列,每个block中的最后一条指令将控制转移到另一个block,或从函数返回。
  • 如果想要对 SIL 的内容进行详细地探索,请参考:2015 LLVM Developers’ Meeting
二、SIL 分析 mian 函数
  • 查看抽象语法树之后,继续在终端中调用 swiftc -emit-sil main.swift >> ./main.sil && code main.sil 命令,生成 main.sil 文件;
  • 用 VSCode 打开 SIL 文件:
// main
//`@main`:标识当前main.swift的`入口函数`,SIL中的标识符名称以`@`作为前缀
sil @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 {
    
    
//`%0、%1` 在SIL中叫做寄存器,可以理解为开发中的常量,一旦赋值就不可修改,如果还想继续使用,就需要不断的累加数字(注意:这里的寄存器,与`register read`中的寄存器是有所区别的,这里是指`虚拟寄存器`,而`register read`中是`真寄存器`)
bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>):
//`alloc_global`:创建一个`全局变量`,即代码中的`t`
  alloc_global @$s4main1tAA10YDWTeacherCvp        // id: %2
//`global_addr`:获取全局变量地址,并赋值给寄存器%3
  %3 = global_addr @$s4main1tAA10YDWTeacherCvp : $*YDWTeacher // user: %7
//`metatype`获取`YDWTeacher`的`MetaData`赋值给%4
  %4 = metatype $@thick YDWTeacher.Type           // user: %6
//将`__allocating_init`的函数地址赋值给 %5
  // function_ref YDWTeacher.__allocating_init()
  %5 = function_ref @$s4main10YDWTeacherCACycfC : $@convention(method) (@thick YDWTeacher.Type) -> @owned YDWTeacher // user: %6
//`apply`调用 `__allocating_init` 初始化一个变量,赋值给%6
  %6 = apply %5(%4) : $@convention(method) (@thick YDWTeacher.Type) -> @owned YDWTeacher // user: %7
//将%6的值存储到%3,即全局变量的地址(这里与前面的%3形成一个闭环)
  store %6 to %3 : $*YDWTeacher                   // id: %7
//构建`Int`,并`return`
  %8 = integer_literal $Builtin.Int32, 0          // user: %9
  %9 = struct $Int32 (%8 : $Builtin.Int32)        // user: %10
  return %9 : $Int32                              // id: %10
} // end sil function 'main'
  • 分析:
    • @main 这⾥标识当前 main.swift 的⼊⼝函数,SIL 中的标识符名称以 @ 作为前缀;
    • %0, %1… 在 SIL 也叫做寄存器,这⾥可以理解为⽇常开发中的常量,⼀旦赋值之后就不可以再修改,如果 SIL 中还要继续使⽤,那么就不断的累加数值。 同时这⾥所说的寄存器是虚拟的,最终运⾏到机器上,会使⽤真的寄存器;
    • alloc_gobal:创建⼀个全局变量;
    • global_addr: 拿到全局变量的地址,赋值给 %3;
    • metatype 拿到 YDWTeacher 的 Metadata 赋值给 %4 将 __allocating_init 的函数地址赋值给 %5;
    • __apply 调⽤ __allocating_init , 并把返回值给 %6;
    • 将 %6 的值存储到 %3(也就是刚刚创建的全局变量的地址);
    • 构建 Int , 并 return;
  • 注意:code 命令是在 .zshrc 中做了如下配置,可以在终端中指定软件打开相应文件:
$ open .zshrc
// ****** 添加以下别名
alias subl='/Applications/SublimeText.app/Contents/SharedSupport/bin/subl'
alias code='/Applications/Visual\ Studio\ Code.app/Contents/Resources/app/bin/code'

// ****** 使用
$ code main.sil

// 如果想SIL文件高亮,需要安装插件:VSCode SIL
  • 从 SIL 文件中,可以看出,代码是经过混淆的,可以通过以下命令还原,以s4main1tAA10YDWTeacherCvp 为例:xcrun swift-demangle s4main1tAA10YDWTeacherCvp,结果如下:
	xcrun swift-demangle s4main1tAA10YDWTeacherCvp
	$s4main1tAA10YDWTeacherCvp ---> main.t : main.YDWTeacher
  • 在 SIL 文件中搜索 s4main10YDWTeacherCACycfC,其内部实现主要是分配内存+初始化变量:
    • allocing_ref:创建一个 YDWTeacher 的实例对象,当前实例对象的引用计数为1;
    • 调用init方法;
	// ********* main入口函数中代码 *********
	%5 = function_ref @$s4main10YDWTeacherCACycfC : $@convention(method) (@thick YDWTeacher.Type) -> @owned YDWTeacher 
	
	// s4main10YDWTeacherCACycfC 实际就是__allocating_init()
	// YDWTeacher.__allocating_init()
	sil hidden [exact_self_class] @$s4main10YDWTeacherCACycfC : $@convention(method) (@thick YDWTeacher.Type) -> @owned YDWTeacher {
    
    
	// %0 "$metatype"
	bb0(%0 : $@thick YDWTeacher.Type):
	// 堆上分配内存空间
	%1 = alloc_ref $YDWTeacher                      // user: %3
	// function_ref YDWTeacher.init() 初始化当前变量
	%2 = function_ref @$s4main10YDWTeacherCACycfc : $@convention(method) (@owned YDWTeacher) -> @owned YDWTeacher // user: %3
	// 返回
	%3 = apply %2(%1) : $@convention(method) (@owned YDWTeacher) -> @owned YDWTeacher // user: %4
	return %3 : $YDWTeacher                         // id: %4
	} // end sil function '$s4main10YDWTeacherCACycfC'
符号断点调试
  • 在我们的 TestSwift 工程中设置“__allocating_init”符号断点;

在这里插入图片描述

  • 然后执行,可以看到:内部调用的是swift_allocObject;

在这里插入图片描述

源码分析
  • 在 VSCode 中的REPL(命令交互行,类似于python的,可以在这里编写代码)中编写如下代码(也可以拷贝),并搜索 *_swift_allocObject 函数加一个断点,如下所示:

在这里插入图片描述

  • 然后初始化一个实例对象t,回车:

在这里插入图片描述

  • 这里的 Local 中可以看出:requiredSize 是内存大小,requiredAlignmentMask 是内存对齐方式;requiredAlignmentMask 是 swift 中的字节对齐方式,这个和OC中是一样的,必须是8的倍数,不足的会自动补齐,目的是以空间换时间,来提高内存操作效率;
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;
}
  • swift_allocObject 的源码如下,主要分为:
    • 通过 swift_slowAlloc 分配内存,并进行内存字节对齐;
    • 通过new + HeapObject + metadata初始化一个实例对象;
    • 函数的返回值是 HeapObject 类型,所以当前对象的内存结构就是 HeapObject 的内存结构;
  • 进入 swift_slowAlloc 函数,其内部主要是通过 malloc 在堆中分配 size 大小的内存空间,并返回内存地址,主要是用于存储实例变量:
void *swift::swift_slowAlloc(size_t size, size_t alignMask) {
    
    
  void *p;
  // This check also forces "default" alignment to use AlignedAlloc.
  if (alignMask <= MALLOC_ALIGN_MASK) {
    
    
#if defined(__APPLE__)
    p = malloc_zone_malloc(DEFAULT_ZONE(), size);
#else
	// 堆中创建size大小的内存空间,用于存储实例变量
    p = malloc(size);
#endif
  } else {
    
    
    size_t alignment = (alignMask == ~(size_t(0)))
                           ? _swift_MinAllocationAlignment
                           : alignMask + 1;
    p = AlignedAlloc(size, alignment);
  }
  if (!p) swift::crash("Could not allocate memory.");
  return p;
}
  • 进入 HeapObject 初始化方法,需要两个参数:metadata、refCounts:
struct HeapObject {
    
    
  /// This is always a valid pointer to a metadata object.
  HeapMetadata const *metadata;

  SWIFT_HEAPOBJECT_NON_OBJC_MEMBERS;

#ifndef __swift__
  HeapObject() = default;

  // Initialize a HeapObject header as appropriate for a newly-allocated object.
  constexpr HeapObject(HeapMetadata const *newMetadata) 
    : metadata(newMetadata)
    , refCounts(InlineRefCounts::Initialized)
  {
    
     }
  
  // Initialize a HeapObject header for an immortal object
  constexpr HeapObject(HeapMetadata const *newMetadata,
                       InlineRefCounts::Immortal_t immortal)
  : metadata(newMetadata)
  , refCounts(InlineRefCounts::Immortal)
  {
    
     }
  • 分析:
    • 其中 metadata 类型是 HeapMetadata,是一个指针类型,占8字节;
    • refCounts(引用计数,类型是 InlineRefCounts,而 InlineRefCounts 是一个类RefCounts 的别名,占8个字节),swift 采用 arc 引用计数;
总结
  • 对于实例对象 t 来说,其本质是一个 HeapObject 结构体,默认 16 字节内存大小(metadata 8字节 + refCounts 8字节),与 OC 的对比如下:
    • OC 中实例对象的本质是结构体,是以 objc_object 为模板继承的,其中有一个 isa 指针,占 8 字节;
    • Swift 中实例对象,默认的比 OC 中多了一个。refCounted 引用计数大小,默认属性占 16 字节;
  • Swift 中对象的内存分配流程是:_allocating_init --> swift_allocObject --> _swift_allocObject --> swift_slowAlloc --> malloc;
  • init 在其中的职责就是初始化变量,这点与 OC 中是一致的。

猜你喜欢

转载自blog.csdn.net/Forever_wj/article/details/112001532