Swift编译流程 & Swift类

前言

本篇文章会大致分析一下swift的编译流程,这个作为了解即可,然后会重点分析一下swift中类的结构,这个知识点我们需要掌握

一、swift编译流程

1.1 LLVM

在了解swift编译流程之前,我们最好清楚LLVM是什么?请参考LLVM编译流程

LLVM是架构编译器的框架系统,以C++编写而成,用于优化任意程序语言编写的程序的编译时间(compile-time)链接时间(link-time)运行时间(run-time)以及空闲时间(idle-time)。对开发者保持开放,并兼容已有脚本。

对于我们iOS系统,OC语言前端使用Clang编译器,而swift语言前端使用swift编译器,这两个编译器将我们写的代码编译生成IR中间代码,交给LLVM优化器进行优化,接着交给代码生成器生成机器语言,最终形成.o机器执行文件。整个过程如下图

1.2 swift的编译详细流程

接着我们再详细看看swift的编译流程,如下图


大致分为以下几步

  1. swift源码经过parse解析ast编译,生成AST语法树 。相关指令

swiftc -dump-ast LGPerson.swift >> ast.swift

  1. 通过SIL生成器生成SIL源码。相关指令

swiftc -emit-sil LGPerson.swift >> ./LGPerson.sil

  1. 生成IR中间代码。相关指令

swiftc -emit-ir LGPerson.swift >> ir.swift

  1. 输出.o机器文件。相关指令

swiftc -emit-object LGPerson.swift

1.3 示例查看编译流程

在示例查看之前,我们可以在【终端】中swiftc -h查看swiftc的所有相关指令

常用的一些指令的含义

  -dump-ast             语法和类型检查,打印AST语法树
  -dump-parse           语法检查,打印AST语法树
  -dump-pcm             转储有关预编译Clang模块的调试信息
  -dump-scope-maps 
                         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             检查文件类型

首先,我们创建一个swift demo项目,定义一个类LGPerson.swift类

class LGPerson {
    var age: Int = 18
    var name: String = "luoji"
}

let t = LGPerson()

接着,打开【终端】,进入到项目的目录

  • 查看抽象语法树:swiftc -dump-ast LGPerson.swift
  • 生成SIL文件:swiftc -emit-sil LGPerson.swift >> ./LGPerson.sil


可以使用VSCode打开它看看代码量巨多

附:可以在.zshrc中做了如下配置,这样就能在终端中指定软件打开相应文件

// 打开.zshrc,如果提示打不开,则使用touch .zshrc 创建一个
$ open .zshrc

// 添加以下别名
alias vscode='/Applications/Visual\ Studio\ Code.app/Contents/Resources/app/bin/code'

// 如果之前是创建的,那么还需执行
source .zshrc

//使用
$ swiftc -emit-sil LGPerson.swift >> ./LGPerson.sil && vscode LGPerson.sil

  • 如果想SIL文件高亮,需要安装插件:VSCode SIL

再次打开LGPerson.sil 没有那么全白了

观察LGPerson.sil代码,可以发现很多都是经过混淆处理的,可以通过下面的指令反混淆

xcrun swift-demangle 你的混淆的代码

1.4 SIL分析

SIL: Swift intermediate language --> swift中间语言

1.4.1 main函数入口

// main
sil @main : $@convention(c) (Int32, UnsafeMutablePointer>>) -> Int32 {
bb0(%0 : $Int32, %1 : $UnsafeMutablePointer>>):
  alloc_global @$s8LGPerson1tA2ACvp               // id: %2
  %3 = global_addr @$s8LGPerson1tA2ACvp : $*LGPerson // user: %7
  %4 = metatype $@thick LGPerson.Type             // user: %6
  // function_ref LGPerson.__allocating_init()
  %5 = function_ref @$s8LGPersonAACABycfC : $@convention(method) (@thick LGPerson.Type) -> @owned LGPerson // user: %6
  %6 = apply %5(%4) : $@convention(method) (@thick LGPerson.Type) -> @owned LGPerson // user: %7
  store %6 to %3 : $*LGPerson                     // id: %7
  %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'
  1. @main标识当前LGPerson.swift文件的入口函数,SIL标识符号名称以@作为前缀
  2. %0,%1...在SIL中也叫寄存器,类似代码中的常量,一旦赋值后不可修改。如果SIL中还要继续使用,就需要使用新的寄存器。

我们一句句的看

  • alloc_global @$s8LGPerson1tA2ACvp
    s8LGPerson1tA2ACvp反混淆出来就是LGPerson,那么就是创建全局变量LGPerson
  • %3 = global_addr @$s8LGPerson1tA2ACvp : $*LGPerson 这句很简单,读取全局变量LGPerson地址,赋值给%3
  • %4 = metatype $@thick LGPerson.Type读取LGPerson的Type,赋值给%4
  • %5 = function_ref @$s8LGPersonAACABycfC : $@convention(method) (@thick LGPerson.Type) -> @owned LGPerson 定义一个function_ref即函数,就是%5,这个函数入参是LGPerson.Type
  • %6 = apply %5(%4) : $@convention(method) (@thick LGPerson.Type) -> @owned LGPerson apply调用函数%5,入参就是%4,将返回结果赋给%6
  • store %6 to %3 : $*LGPerson 将%6的结果存储到%3,%3是LGPerson的地址
  • %8 = integer_literal $Builtin.Int32, 0%9 = struct $Int32 (%8 : $Builtin.Int32)就是构建一个Int值,最终返回return %9 : $Int32

综上所述,main函数就是构建了一个全局变量LGPerson,并对其Type值做了一个返回处理,那么,这个Type代表什么意思呢?后面我们会仔细分析swift类的底层结构。

1.4.2 实例化相关代码

// LGPerson.__allocating_init()
sil hidden [exact_self_class] @$s8LGPersonAACABycfC : $@convention(method) (@thick LGPerson.Type) -> @owned LGPerson {
// %0 "$metatype"
bb0(%0 : $@thick LGPerson.Type):
  %1 = alloc_ref $LGPerson                        // user: %3
  // function_ref LGPerson.init()
  %2 = function_ref @$s8LGPersonAACABycfc : $@convention(method) (@owned LGPerson) -> @owned LGPerson // user: %3
  %3 = apply %2(%1) : $@convention(method) (@owned LGPerson) -> @owned LGPerson // user: %4
  return %3 : $LGPerson                           // id: %4
} // end sil function '$s8LGPersonAACABycfC'

同样一句句的看

  • %1 = alloc_ref $LGPerson 读取LGPerson的alloc_ref方法地址,给%1
  • %2 = function_ref @$s8LGPersonAACABycfc : $@convention(method) (@owned LGPerson) -> @owned LGPerson 读取LGPerson.init()函数地址,给%2
  • %3 = apply %2(%1) : $@convention(method) (@owned LGPerson) -> @owned LGPerson 调用alloc_ref创建一个LGPerson实例对象,给%3
  • return %3 : $LGPerson返回%3的实例对象

这个__allocating_init()也不难,其实就是一个简单的方法调用过程。对于这个SIL代码,我们平时可以自己多看多分析,习惯就好了,哈哈!

二、swift类的结构

带着上面的问题LGPerson中的Type 就是存储的是什么值,代表什么意思?我们现在来看看swift的类class在底层中的结构是什么样的。

大家都知道,如果要创建一个对象,OC 与 swift的写法是这样的

  • OC: [[LGPerosn alloc] init]
    一般alloc申请内存空间并创建对象,init对其进行统一初始化处理。
  • Swift:LGPerson()
    直接()就完成了对象的创建。

2.1 找入口

我们现在就来看看LGPerson()在汇编层是调用了哪些函数?
新建一个SwiftDemo项目工程,在ViewController.swift中添加下面的代码,并打上断点

运行项目,查看汇编

我们找到了对象的初始化入口函数__allocating_init(),接着我们在__allocating_init()这一行打上断点,按住control按键 stepinto进入查看

定位到了swift_allocObject,同理还是在swift_allocObject这行加上断点,stepinto进入查看

没找到实用的信息,那么换种思路,添加符号断点swift_allocObject,再次run

接着,添加符号断点swift_slowAlloc

最终来到了比较熟悉的malloc_zone_malloc

至此,swift实例对象的创建流程
__allocating_init -> swift_allocObject -> _swift_allocObject_ -> swift_slowAlloc -> malloc_zone_malloc

2.2 查看源码验证流程

首先,用VSCode打开swift源码项目,搜索__allocating_init,打上断点

接着,run项目,在终端输入代码

class LGPerson {
       var age: Int = 18
       var name: String = "luoji"
}

回车,再输入var t = LGPerson(),会触发断点

我们可以看左上角,发现requiredSize空间大小值是40,requiredAlignmentMask字节对齐值是7。

接着我们顺着流程来到swift_slowAlloc

然后返回申请空间后的地址,即返回指针p。再回到_swift_allocObject_,那么接着执行

  • auto object = reinterpret_cast( swift_slowAlloc(requiredSize, requiredAlignmentMask));
    通过reinterpret_cast将指针p转换成HeapObject类型,然后执行
  • new (object) HeapObject(metadata);
    因为object是强转的HeapObject类型,其里面值仍是一个指向内存空间的对象指针,所以这里执行HeapObject(metadata)做一个初始化操作,这样object就是真正的HeapObject结构了。

整体流程图

2.2.1 类的大小

接下来我们具体看看,类的大小size是怎么算出来的?
首先,我们先在lldb中打印查看一下大小,打开Xcode,新建一个swift项目,加入代码

import UIKit

class LGPerson {
    var age: Int = 18
    var name: String = "luoji"
}

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
                
        print("Int32 大小: \(MemoryLayout.size)")
        print("Int64 大小: \(MemoryLayout.size)")
        print("Int 大小: \(MemoryLayout.size)")
        print("Srtring 大小: \(MemoryLayout.size)")
        
        print("LGPerson 大小: \(class_getInstanceSize(LGPerson.self))")
        
    }
}

run

我们通过MemoryLayout得知Int大小是8String大小是16,但是通过class_getInstanceSize得知类LGPerson的大小却是40,40-16-8=16,这额外的16字节大小是什么?

之前我们在源码调试的时候,也发现类LGPerson初始化实例对象时的requiredSize空间大小值也是40,如下图

额外的16字节从何而来?

我们进入源码工程,查看HeapObject结构体

结构体大小主要看成员变量,HeapObject结构体有两个成员变量元数据metadata引用计数refCounts

metadata

是 HeapMetedata类型,还是查看源码

template  struct TargetHeapMetadata;
using HeapMetadata = TargetHeapMetadata;

template 
struct TargetHeapMetadata : TargetMetadata {
  using HeaderType = TargetHeapMetadataHeader;

  TargetHeapMetadata() = default;
  constexpr TargetHeapMetadata(MetadataKind kind)
    : TargetMetadata(kind) {}
#if SWIFT_OBJC_INTEROP
  constexpr TargetHeapMetadata(TargetAnyClassMetadata *isa)
    : TargetMetadata(isa) {}
#endif
};

TargetHeapMetadata->TargetMetadata,也是struct结构体。接着看TargetMetadata

最后我们看看StoredPointer源码,发现搜不到,但是根据注释

  /// The kind. Only valid for non-class metadata; getKind() must be used to get
  /// the kind value.
  StoredPointer Kind;

我们发现, 可以通过 getKind()方法入参知道

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

/// Try to translate the 'isa' value of a type/heap metadata into a value
/// of the MetadataKind enum.
inline MetadataKind getEnumeratedMetadataKind(uint64_t kind) {
  if (kind > LastEnumeratedMetadataKind)
    return MetadataKind::Class;
  return MetadataKind(kind);
}

getKind是调用getEnumeratedMetadataKind,而入参kinduint64_t类型,占8字节大小,所以元数据metadata8字节大小

MetadataKind

这时我们注意到,getKind()返回值是MetadataKind类型,我们进入源码看看,里面有一个#include "MetadataKind.def",点击进入,其中记录了所有类型的元数据,所以kind种类总结如下:

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类型

如果是Class,则直接对this(当前指针,即metadata)强转ClassMetadata。所以,TargetMetadataTargetClassMetadata 本质上是一样的,因为在内存结构中,可以直接进行指针的转换,那么我们可以这么认为结构体其实就是TargetClassMetadata

接着,我们再看看TargetClassMetadata里还有哪些成员

template 
struct TargetClassMetadata : public TargetAnyClassMetadata {
    ...
    //swift特有的标志
    ClassFlags Flags;
    //实力对象内存大小
    uint32_t InstanceSize;
    //实例对象内存对齐方式
    uint16_t InstanceAlignMask;
    //运行时保留字段
    uint16_t Reserved;
    //类的内存大小
    uint32_t ClassSize;
    //类的内存首地址
    uint32_t ClassAddressPoint;
  ...
}

然后我们看看继承链 TargetClassMetadata继承TargetAnyClassMetadata继承TargetHeapMetadata

template 
struct TargetAnyClassMetadata : public TargetHeapMetadata {
    ...
    ConstTargetMetadataPointer Superclass;
    TargetPointer CacheData[2];
    StoredSize Data;
    ...
}

至此,当metadata的kind为Class时,有如下继承链:

  • 类class在底层中的实际类型是TargetClassMetadata,而TargetMetaData中只有一个属性kindTargetAnyClassMetaData中有4个属性,分别是kind, superclass,cacheData、data(图中未标出)
  • 当前Class在内存中所存放的属性由 TargetClassMetadata属性 + TargetAnyClassMetaData属性 + TargetMetaData属性 构成,所以得出的metadata的数据结构体如下所示
struct swift_class_t: NSObject{
    void *kind;//相当于OC中的isa,kind的实际类型是unsigned long
    void *superClass;
    void *cacheData;
    void *data;
    uint32_t flags; //4字节
    uint32_t instanceAddressOffset;//4字节
    uint32_t instanceSize;//4字节
    uint16_t instanceAlignMask;//2字节
    uint16_t reserved;//2字节
    
    uint32_t classSize;//4字节
    uint32_t classAddressOffset;//4字节
    void *description;
    ...
}

refCounts

接着我们看看refCountsrefCountsInlineRefCounts类型,搜索InlineRefCounts

typedef RefCounts InlineRefCounts;

InlineRefCounts是RefCounts类型,搜索

RefCounts是class类型,确切的说,refCounts是个指针,占8字节大小

综上所述

  1. swift类本质是HeapObject
  2. HeapObject默认大小为16字节: metadata(struct)8字节和refCounts(class)8字节
  3. LGPerson的age(Int)占8字节name(String)占16字节,加上上面的,所以LGPersonsize为40字节

总结

本篇文章首先大致讲述了Swift的编译流程,与OC最大的不同在于,swift在编译过程中会生成SIL中间代码,通过对中间代码的分析,我明知道了Swift实例对象的初始化会调用__allocating_init()方法,接着我们通过LGPerson样例分析了Swift类的底层结构HeapObject,其默认包含了metadata(struct)refCounts(class),共16字节大小。

你可能感兴趣的:(Swift编译流程 & Swift类)