Swift編譯流程 & Swift類

前言

本篇文章會大致分析一下swift的編譯流程,這個作為了解即可,然后會重點分析一下swift中類的結構,這個知識點我們需要掌握

一、swift編譯流程

1.1 LLVM

在了解swift編譯流程之前,我們最好清楚LLVM是什么?請參考??LLVM編譯流程

LLVM是架構編譯器的框架系統(tǒng),以C++編寫而成,用于優(yōu)化任意程序語言編寫的程序的編譯時間(compile-time)、鏈接時間(link-time)運行時間(run-time)以及空閑時間(idle-time)。對開發(fā)者保持開放,并兼容已有腳本。

對于我們iOS系統(tǒng),OC語言前端使用Clang編譯器,而swift語言前端使用swift編譯器,這兩個編譯器將我們寫的代碼編譯生成IR中間代碼,交給LLVM優(yōu)化器進行優(yōu)化,接著交給代碼生成器生成機器語言,最終形成.o機器執(zhí)行文件。整個過程如下圖??

1.2 swift的編譯詳細流程

接著我們再詳細看看swift的編譯流程,如下圖??


大致分為以下幾步??

  1. swift源碼經(jīng)過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模塊的調(diào)試信息
  -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       輸出一個可執(zhí)行文件
  -emit-imported-modules 展示導入的模塊列表
  -emit-ir               展示IR中間代碼
  -emit-library          輸出一個dylib動態(tài)庫
  -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            為源文件生成索引數(shù)據(jù)
  -parse                 解析文件
  -print-ast             解析文件并打?。ㄆ?簡潔的)語法樹
  -resolve-imports       解析import導入的文件
  -typecheck             檢查文件類型

首先,我們創(chuàng)建一個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 創(chuàng)建一個
$ open .zshrc

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

// 如果之前是創(chuàng)建的,那么還需執(zhí)行
source .zshrc

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

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

再次打開LGPerson.sil?? 沒有那么全白了

觀察LGPerson.sil代碼,可以發(fā)現(xiàn)很多都是經(jīng)過混淆處理的,可以通過下面的指令反混淆

xcrun swift-demangle 你的混淆的代碼

1.4 SIL分析

SIL: Swift intermediate language --> swift中間語言

1.4.1 main函數(shù)入口

// main
sil @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 {
bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>):
  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文件的入口函數(shù),SIL標識符號名稱以@作為前綴。
  2. %0,%1...在SIL中也叫寄存器,類似代碼中的常量,一旦賦值后不可修改。如果SIL中還要繼續(xù)使用,就需要使用新的寄存器。

我們一句句的看

  • alloc_global @$s8LGPerson1tA2ACvp
    s8LGPerson1tA2ACvp反混淆出來就是LGPerson,那么就是創(chuàng)建全局變量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即函數(shù),就是%5,這個函數(shù)入?yún)⑹荓GPerson.Type
  • %6 = apply %5(%4) : $@convention(method) (@thick LGPerson.Type) -> @owned LGPerson apply調(diào)用函數(shù)%5,入?yún)⒕褪?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函數(shù)就是構建了一個全局變量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()函數(shù)地址,給%2
  • %3 = apply %2(%1) : $@convention(method) (@owned LGPerson) -> @owned LGPerson 調(diào)用alloc_ref創(chuàng)建一個LGPerson實例對象,給%3
  • return %3 : $LGPerson返回%3的實例對象

這個__allocating_init()也不難,其實就是一個簡單的方法調(diào)用過程。對于這個SIL代碼,我們平時可以自己多看多分析,習慣就好了,哈哈!

二、swift類的結構

帶著上面的問題??LGPerson中的Type 就是存儲的是什么值,代表什么意思?我們現(xiàn)在來看看swift的類class在底層中的結構是什么樣的。

大家都知道,如果要創(chuàng)建一個對象,OC 與 swift的寫法是這樣的??

  • OC: [[LGPerosn alloc] init]
    一般alloc申請內(nèi)存空間并創(chuàng)建對象,init對其進行統(tǒng)一初始化處理。
  • Swift:LGPerson()
    直接()就完成了對象的創(chuàng)建。

2.1 找入口

我們現(xiàn)在就來看看LGPerson()在匯編層是調(diào)用了哪些函數(shù)?
新建一個SwiftDemo項目工程,在ViewController.swift中添加下面的代碼,并打上斷點??

運行項目,查看匯編??

我們找到了對象的初始化入口??函數(shù)__allocating_init(),接著我們在__allocating_init()這一行打上斷點,按住control按鍵 stepinto進入查看??

定位到了swift_allocObject,同理還是在swift_allocObject這行加上斷點,stepinto進入查看??

沒找到實用的信息,那么換種思路,添加符號斷點swift_allocObject,再次run??

接著,添加符號斷點swift_slowAlloc??

最終來到了比較熟悉的malloc_zone_malloc。

至此,swift實例對象的創(chuàng)建流程??
__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(),會觸發(fā)斷點??

我們可以看左上角,發(fā)現(xiàn)requiredSize空間大小值是40,requiredAlignmentMask字節(jié)對齊值是7。

接著我們順著流程來到swift_slowAlloc??

然后返回申請空間后的地址,即返回指針p。再回到_swift_allocObject_,那么接著執(zhí)行

  • auto object = reinterpret_cast<HeapObject *>( swift_slowAlloc(requiredSize, requiredAlignmentMask));
    通過reinterpret_cast將指針p轉換成HeapObject類型,然后執(zhí)行
  • new (object) HeapObject(metadata);
    因為object是強轉的HeapObject類型,其里面值仍是一個指向內(nèi)存空間的對象指針,所以這里執(zhí)行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<Int32>.size)")
        print("Int64 大小: \(MemoryLayout<Int64>.size)")
        print("Int 大小: \(MemoryLayout<Int>.size)")
        print("Srtring 大小: \(MemoryLayout<String>.size)")
        
        print("LGPerson 大小: \(class_getInstanceSize(LGPerson.self))")
        
    }
}

run??

我們通過MemoryLayout得知Int大小是8String大小是16,但是通過class_getInstanceSize得知類LGPerson的大小卻是40,40-16-8=16,這額外的16字節(jié)大小是什么?

之前我們在源碼調(diào)試的時候,也發(fā)現(xiàn)類LGPerson初始化實例對象時的requiredSize空間大小值也是40,如下圖??

額外的16字節(jié)從何而來?

我們進入源碼工程,查看HeapObject結構體??

結構體大小主要看成員變量,HeapObject結構體有兩個成員變量元數(shù)據(jù)metadata引用計數(shù)refCounts。

metadata

是 HeapMetedata類型,還是查看源碼??

template <typename Target> struct TargetHeapMetadata;
using HeapMetadata = TargetHeapMetadata<InProcess>;

template <typename Runtime>
struct TargetHeapMetadata : TargetMetadata<Runtime> {
  using HeaderType = TargetHeapMetadataHeader<Runtime>;

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

TargetHeapMetadata->TargetMetadata,也是struct結構體。接著看TargetMetadata??

最后我們看看StoredPointer源碼,發(fā)現(xiàn)搜不到,但是根據(jù)注釋

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

我們發(fā)現(xiàn), 可以通過 getKind()方法入?yún)⒅??

  /// 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是調(diào)用getEnumeratedMetadataKind,而入?yún)?code>kind是uint64_t類型,占8字節(jié)大小,所以元數(shù)據(jù)metadata8字節(jié)大小。

MetadataKind

這時我們注意到,getKind()返回值是MetadataKind類型,我們進入源碼看看,里面有一個#include "MetadataKind.def",點擊進入,其中記錄了所有類型的元數(shù)據(jù),所以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 本質上是一樣的,因為在內(nèi)存結構中,可以直接進行指針的轉換,那么我們可以這么認為??結構體其實就是TargetClassMetadata。

接著,我們再看看TargetClassMetadata里還有哪些成員??

template <typename Runtime>
struct TargetClassMetadata : public TargetAnyClassMetadata<Runtime> {
    ...
    //swift特有的標志
    ClassFlags Flags;
    //實力對象內(nèi)存大小
    uint32_t InstanceSize;
    //實例對象內(nèi)存對齊方式
    uint16_t InstanceAlignMask;
    //運行時保留字段
    uint16_t Reserved;
    //類的內(nèi)存大小
    uint32_t ClassSize;
    //類的內(nèi)存首地址
    uint32_t ClassAddressPoint;
  ...
}

然后我們看看繼承鏈?? TargetClassMetadata繼承TargetAnyClassMetadata繼承TargetHeapMetadata。

template <typename Runtime>
struct TargetAnyClassMetadata : public TargetHeapMetadata<Runtime> {
    ...
    ConstTargetMetadataPointer<Runtime, swift::TargetClassMetadata> Superclass;
    TargetPointer<Runtime, void> CacheData[2];
    StoredSize Data;
    ...
}

至此,當metadata的kind為Class時,有如下繼承鏈:

  • 類class在底層中的實際類型是TargetClassMetadata,而TargetMetaData中只有一個屬性kindTargetAnyClassMetaData中有4個屬性,分別是kind, superclass,cacheData、data(圖中未標出)
  • 當前Class在內(nèi)存中所存放的屬性由 TargetClassMetadata屬性 + TargetAnyClassMetaData屬性 + TargetMetaData屬性 構成,所以得出的metadata的數(shù)據(jù)結構體如下所示
struct swift_class_t: NSObject{
    void *kind;//相當于OC中的isa,kind的實際類型是unsigned long
    void *superClass;
    void *cacheData;
    void *data;
    uint32_t flags; //4字節(jié)
    uint32_t instanceAddressOffset;//4字節(jié)
    uint32_t instanceSize;//4字節(jié)
    uint16_t instanceAlignMask;//2字節(jié)
    uint16_t reserved;//2字節(jié)
    
    uint32_t classSize;//4字節(jié)
    uint32_t classAddressOffset;//4字節(jié)
    void *description;
    ...
}

refCounts

接著我們看看refCounts,refCountsInlineRefCounts類型,搜索InlineRefCounts??

typedef RefCounts<InlineRefCountBits> InlineRefCounts;

InlineRefCounts是RefCounts類型,搜索??

RefCounts是class類型,確切的說,refCounts是個指針,占8字節(jié)大小

綜上所述

  1. swift類本質是HeapObject
  2. HeapObject默認大小為16字節(jié): metadata(struct)8字節(jié)和refCounts(class)8字節(jié)
  3. LGPerson的age(Int)占8字節(jié),name(String)占16字節(jié),加上上面的,所以LGPersonsize為40字節(jié)

總結

本篇文章首先大致講述了Swift的編譯流程,與OC最大的不同在于,swift在編譯過程中會生成SIL中間代碼,通過對中間代碼的分析,我明知道了Swift實例對象的初始化會調(diào)用__allocating_init()方法,接著我們通過LGPerson樣例分析了Swift類的底層結構HeapObject,其默認包含了metadata(struct)refCounts(class),共16字節(jié)大小。

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容