swift高級1. 類與結構體(上)
類與結構體
本文主要介紹為什麼 結構體是值類型, 類是引用類型 , 對應的方法調度, 內存插件libfooplugin, 類生命週期 以及 Swift源碼分析
初識類與結構體
類與結構體相同點與不同點
Swift中類用class修飾,結構體用struct修飾 ``` struct/class Teacher { var age: Int var name: String
init(age: Int, name: String) {
self.age = age
self.name = name
}
deinit{}
} ```
結構體和類的主要相同點有:
- 定義存儲值的屬性
- 定義方法
- 定義下標以使用下標語法提供對其值的訪問
- 定義初始化器
- 使用 extension 來拓展功能
- 遵循協議來提供某種功能
主要的不同點有:
- 類有繼承的特性,而結構體沒有
- 類型轉換使您能夠在運行時檢查和解釋類實例的類型
- 類有析構函數用來釋放其分配的資源
- 引用計數允許對一個類實例有多個引用
類是引用類型
對於類與結構體我們需要區分的第一件事就是:
類是引用類型。也就意味着一個類類型的變量並不直接存儲具體的實例對象,是對當前存儲具體實例內存地址的引用。
``` class Teacher { var age = 18 var name = "aa"
init(age: Int, name: String) {
self.age = age
self.name = name
}
} var t = Teacher(age: 18, name: "tony") var t1 = t print("end") // 斷點
(lldb) po t
(lldb) po t1
結構體是值類型
swift 中有引用類型,就有值類型,最典型的就是 Struct ,結構體的定義也非常簡單,相比較 類類型的變量中存儲的是地址,那麼值類型存儲的就是具體的實例(或者説具體的值)。
``` struct Student { var age: Int var name: String } var s = Student(age: 19, name: "tony") var s1 = s print("end") // 斷點
(lldb) po s ▿ Student - age : 19 - name : "tony"
(lldb) po s1 ▿ Student - age : 19 - name : "tony"
(lldb) po withUnsafePointer(to: &s) { print($0) } 0x0000000100008158 0 elements
(lldb) po withUnsafePointer(to: &s1) { print($0) } 0x0000000100008170 0 elements (lldb) cat address 0x0000000100008158 address:0x0000000100008158, 0SwiftTest.s : SwiftTest.Student <+0> , ($s9SwiftTest1sAA7StudentVvp), External: NO SwiftTest.__DATA.__common +0 (lldb) cat address 0x0000000100008170 address:0x0000000100008170, 18SwiftTest.s1 : SwiftTest.Student <+0> , ($s9SwiftTest2s1AA7StudentVvp), External: NO SwiftTest.__DATA.__common +18 ``` 可以看到,結構體是直接存儲值。
- 其實引用類型就相當於在線的 Excel ,當我們把這個鏈接共享給別人的時候,別人的修改我們 是能夠看到的;
- 值類型就相當於本地的 Excel ,當我們把本地的 Excel 傳遞給別人的時候,就 相當於重新複製了一份給別人,至於他們對於內容的修改我們是無法感知的。
存儲區域的不同
另外引用類型和值類型還有一個最直觀的區別就是存儲的位置不同:一般情況,值類型存儲的在 棧上,引用類型存儲在堆上。
結構體的內存分配
來看例子 ``` struct Teacher { var age = 18 var name = "tony" }
func test() { var t = Teacher() print("end") // 斷點 } test()
(lldb) frame variable -L t 0x000000016fdff318: (SwiftTest.Teacher) t = { 0x000000016fdff318: age = 18 0x000000016fdff320: name = "tony" } (lldb) cat address 0x000000016fdff318 address:0x000000016fdff318, stack address (SP: 0x16fdff2d0 FP: 0x16fdff330) SwiftTest.test() -> () ``` - t是存儲在棧上的,age和name也是存儲在棧上的 - t的首地址直接是存儲的age變量,age和name也是連續的 - 當執行var t = Teacher()這段代碼時,會在棧上開闢所需的內存空間,當test()函數執行完時,回收所開闢的內存空間。
當前結構體在內存當中的分佈示意圖:
類的內存分配
如果我們把其他條件不變,將strcut修改成class的情況我們來看一下:
``` class Teacher { var age = 18 var name = "tony" }
func test() { var t = Teacher() print("end") // 斷點 } test()
(lldb) frame variable -L t scalar: (SwiftTest.Teacher) t = 0x0000000100708020 { 0x0000000100708030: age = 18 0x0000000100708038: name = "tony" } (lldb) cat address 0x0000000100708020 address:0x0000000100708020, (String) $R0 = "0x100708020 heap pointer, (0x30 bytes), zone: 0x1f4a1c000"
(lldb) x/8g 0x100708020 0x100708020: 0x0000000100008288 0x0000000200000003 0x100708030: 0x0000000000000012 0x00000000796e6f74 0x100708040: 0xe400000000000000 0x00000001a1398348 0x100708050: 0x20000000100729f6 0x900000001007296a ``` - 在棧上會開闢8字節內存空間,來存儲Teacher的地址 - 在堆上尋找合適大小的內存塊分配,然後將value拷貝到堆內存存儲,並在棧上存儲一個指向這塊堆內存的指針, 其中t佔了48個字節,age佔了8個字節,age佔16個字節 - 函數結束時,離開作用域,堆內存銷燬(查找並把內存塊重新插入到棧內存中),棧內存銷燬(移動棧針) - 當然棧內存在分配的過程中還要保證線程安全(這也是一筆很大的開銷)
結構體和類的時間分配
這裏我們也可以通過github上 StructVsClassPerformanc 這個案例來直觀的測試當前結構體和類的時間分配。
``` Running tests
class (1 field) 2.280184208328137
struct (1 field) 1.5351142500003334
class (10 fields) 2.1326438749965746
struct (10 fields) 1.6441537916689413 ``` 經過測試可以發現,結構體速度快於類, 但demo中也不是特別快。
優化案例
Understanding Swift Performance參考資料
下面看一個在實際應用中的優化方案 ``` enum Color { case blue, green, gray } enum Orientation { case left, right } enum Tail { case none, tail, bubble }
var cache = String : UIImage
func makeBalloon(_ color: Color, orientation: Orientation, tail: Tail) -> UIImage { let key = "(color):(orientation):(tail)" if let image = cache[key] { return image } ... } ```
這段代碼的應用場景是我們常見的iMessage聊天界面氣泡生產能function,三個enum表示氣泡的不同情況,比方藍色朝左帶尾巴,因為用户可能經常滑動聊天列表來查看消息makeBalloon這個方法的調用時非常頻繁的,為了避免多次生成UIImage,我們用Dictionary來充當一個cache,用不同情況下的enum序列化一個String來充當cache的key。
思考一下這樣寫有什麼問題麼?
問題就出在key上面,用String充當key,首先在類型安全上面無法保證存放的一定是一個氣泡的類型,因為是String所以你可以存放阿貓阿狗等等。其次String的character是存儲在堆上面的,所以每次調用makeBalloon雖然命中cache,但仍然不停的設計堆內存分配和銷燬
怎麼解決?
``` struct Attributes : Hashable { var color: Color var orientation: Orientation var tail: Tail }
let key = Attributes(color: color, orientation: orientation, tail: tail) ``` 看到優化代碼你應該恍然大悟,原來Swift已經為我們準備好了更好地類型struct並且他可以作為Dictionary的key,這樣就不涉及任何堆內存分配銷燬,並且Attributes可以確保類型安全
下面看一個在實際應用中的優化方案
``` struct Attachment { let fileURL: URL let uuid: String let mineType: String
init?(fileURL: URL, uuid: String, mimeType: String) {
guard mineType.isMineType else { return nil }
self.fileURL = fileURL
self.uuid = uuid
self.mineType = mimeType
}
} ``` 這段代碼是我們給message新建的一個model,表示我們聊天時發送的文件,它含有一個URL類型的fileURL表示文件的位置,一個String類型的uuid方便我們在不同設備定位這個文件,因為不可能支持所有的文件格式,所以有一個String類型的mineType用於過濾不支持的文件格式
想一下這麼創建model會有什麼性能上的問題或者有什麼優化的方案麼?
來看一下Attachment的存儲情況
因為URL為引用類型,String的character存儲在堆上,所以Attachment在引用計數的層面消耗有點大,感覺可以優化,那怎麼優化?
首先uuid聲明為String類型真的好麼?答案是不好,一是無法在類型上限制uuid的安全性,二是涉及到引用計數,那怎麼優化呢?Fundation中加入了UUID這個類型,是一個值類型不涉及引用計數,也能在類型安全上讓我們滿意,
public struct UUID : ReferenceConvertible, Hashable, Equatable, CustomStringConvertible {}
再來看一下mineType
extension String {
var isMimeType: Bool {
switch self {
case "image/jpeg":
return true
case "image/png":
return true
case "image/gif":
return true
default:
return false
}
}
}
我們是通過給String擴展的方式來過濾我們支持的文件格式,看到這裏你肯定想到了Swift為我們提供的更好的表示多種情況的抽象類型enum吧,它同為值類型,不涉及引用計數
優化後的模型:
``` struct Attachment { let fileURL: URL let uuid: UUID let mineType: MimeType
init?(fileURL: URL, uuid: UUID, mimeType: String) {
guard let mimeType = MimeType(rawValue: mimeType) else { return nil }
self.fileURL = fileURL
self.uuid = uuid
self.mineType = mimeType
}
}
enum MimeType : String { case jpeg = "image/jpeg" case png = "image/png" case gif = "image/gif" } ```
類的初始化器
Swift 中創建類和結構體的實例時必須為所有的存儲屬性設置一個合適的初始值。
結構體初始化器
``` struct Teacher { var age: Int var name: String }
// 配置Run Script swiftc -emit-sil ${SRCROOT}/TT/main.swift | xcrun swift-demangle > ./main.sil && open main.sil
// SIL文件 struct Teacher { @_hasStorage var age: Int { get set } @_hasStorage var name: String { get set } init(age: Int, name: String) } ``` 類編譯器默認不會自動提供成員初始化器,但是對於結構體來説編譯器會提供默認的初始化方法(前提是我們自己沒有指定初始化器)!
類初始化器
類必須要提供對應的指定初始化器,同時我們也可以為當前的類提供便捷初始化器
``` class Person { var age: Int var name: String }
報錯: Class 'Person' has no initializers
class Person { var age: Int var name: String
init(_ age: Int, _ name: String) {
self.age = age
self.name = name
}
convenience init(_ age: Int) {
self.init(18, "tony")
self.age = age
}
}
SIL文件 class Person { @hasStorage var age: Int { get set } @_hasStorage var name: String { get set } init( age: Int, _ name: String) convenience init(_ age: Int) @objc deinit } ``` 注意:便捷初始化器必須從相同的類裏調用另一個初始化器。
當我們派生出一個子類SubTeacher,看下它的指定初始化器的寫法
``` class Person { var age: Int var name: String
init(_ age: Int, _ name: String) {
self.age = age
self.name = name
}
convenience init(_ age: Int) {
self.init(18, "tony")
self.age = age
}
}
class Teacher: Person { var subjectName: String
init(_ subjectName: String) {
self.subjectName = subjectName
super.init(18, "mark")
self.age = 17
}
}
SIL文件 class Person { @hasStorage var age: Int { get set } @_hasStorage var name: String { get set } init( age: Int, _ name: String) convenience init(_ age: Int) @objc deinit }
class Teacher : Person { @hasStorage var subjectName: String { get set } init( subjectName: String) override init(_ age: Int, _ name: String) @objc deinit } ```
-
指定初始化器必須保證在向上委託給父類初始化器之前,其所在類引入的所有屬性都要初始化完成。
-
指定初始化器必須先向上委託父類初始化器,然後才能為繼承的屬性設置新值。如果不這樣做,指定初始化器賦予的新值將被父類中的初始化器所覆蓋。
-
便捷初始化器必須先委託同類中的其它初始化器,然後再為任意屬性賦新值(包括同類裏定義的屬性)。如果沒這麼做,便捷構初始化器賦予的新值將被自己類中其它指定初始化器所覆蓋。
-
初始化器在第一階段初始化完成之前,不能調用任何實例方法、不能讀取任何實例屬性的值,也不能引用 self 作為值。
可失敗初始化器
意思就是當前因為參數的不合法或者外部條件的不滿足,存在初始化失敗的情況。
這種 Swift 中可失敗初始化器寫 return nil 語句, 來表明可失敗初始化器在何種情況下會觸發初始化失敗。寫法也非常簡單:
``` class Person { var age: Int var name: String
init?(_ age: Int, _ name: String) {
// 年齡小於 18 認為不是一個合法的成年人,創建失敗
guard age < 18 else { return nil }
self.age = age
self.name = name
}
convenience init?(_ age: Int) {
self.init(18, "tony")
self.age = age
}
} ```
必要初始化器
在類的初始化器前添加required修飾符來表明所有該類的子類都必須實現該初始化器
如果子類沒有實現該必要初始化器,就會報錯。
類的生命週期
1.Swift編譯
iOS開發的語言不管是OC還是Swift後端都是通過LLVM進行編譯的,如下圖所示:
- OC通過clang編譯器,編譯成IR,然後再生成可執行文件.o
- Swift則是通過Swift編譯器編譯成IR,然後在生成可執行文件。
詳細看下Swift的編譯過程: ``` // 分析輸出AST swiftc main.swift -dump-parse
// 分析並且檢查類型輸出AST swiftc main.swift -dump-ast
// 生成中間體語言(SIL),未優化 swiftc main.swift -emit-silgen
// 生成中間體語言(SIL),優化後的 swiftc main.swift -emit-sil
// 生成LLVM中間體語言 (.ll文件) swiftc main.swift -emit-ir
// 生成LLVM中間體語言 (.bc文件) swiftc main.swift -emit-bc
// 生成彙編 swiftc main.swift -emit-assembly
// 編譯生成可執行.out文件 swiftc -o main.o main.swift ```
2.sil文件分析
在main.swift中寫入下面的代碼:
class Person {
var age: Int = 18
var name: String = "tony"
}
var p = Person()
運行腳本,將main.swift編譯成main.sil文件
``` class Person { @_hasStorage @_hasInitialValue var age: Int { get set } @_hasStorage @_hasInitialValue var name: String { get set } @objc deinit init() }
// main
sil @main : [email protected](c) (Int32, UnsafeMutablePointer
// 去堆區申請內存, 調用關鍵函數__allocating_init()
// Person.__allocating_init()
sil hidden [exact_self_class] @main.Person.__allocating_init() -> main.Person : [email protected](method) (@thick Person.Type) -> @owned Person {
// %0 "$metatype"
bb0(%0 : [email protected] Person.Type):
; 去堆區申請內存空間
%1 = alloc_ref $Person // user: %3
// function_ref Person.init()
%2 = function_ref @main.Person.init() -> main.Person : [email protected](method) (@owned Person) -> @owned Person // user: %3
%3 = apply %2(%1) : [email protected](method) (@owned Person) -> @owned Person // user: %4
return %3 : $Person // id: %4
} // end sil function 'main.Person.__allocating_init() -> main.Person'
```
- @main:入口函數
- %0:寄存器,虛擬的
- sil語法
3.斷點彙編分析
class Person {
var age: Int = 18
var name: String = "tony"
}
var p = Person() // 斷點
按住Control + 點擊Step info
SwiftTest`Person.__allocating_init():
-> 0x100003a48 <+0>: stp x20, x19, [sp, #-0x20]!
0x100003a4c <+4>: stp x29, x30, [sp, #0x10]
0x100003a50 <+8>: add x29, sp, #0x10 ; =0x10
0x100003a54 <+12>: mov x0, x20
0x100003a58 <+16>: mov w8, #0x28
0x100003a5c <+20>: mov x1, x8
0x100003a60 <+24>: mov w8, #0x7
0x100003a64 <+28>: mov x2, x8
0x100003a68 <+32>: bl 0x100003c58 ; symbol stub for: swift_allocObject
0x100003a6c <+36>: mov x20, x0
0x100003a70 <+40>: bl 0x100003aa8 ; SwiftTest.Person.init() -> SwiftTest.Person at main.swift:10
0x100003a74 <+44>: ldp x29, x30, [sp, #0x10]
0x100003a78 <+48>: ldp x20, x19, [sp], #0x20
0x100003a7c <+52>: ret
我們看到了swift_allocObject
下面我們找到關鍵函數swift_allocObject,進行源碼的分析。
4.Swift源碼分析
搜索swift_allocObject, 找到HeapObject.cpp文件
``` #define CALL_IMPL(name, args) do { \ void fptr; \ memcpy(&fptr, (void )& ## name, sizeof(fptr)); \ extern char _ ## name ## _as_char asm("__" #name ""); \ fptr = __ptrauth_swift_runtime_function_entry_strip(fptr); \ if (SWIFT_UNLIKELY(fptr != &_ ## name ## _as_char)) \ return _ ## name args; \ return _ ## name ## _ args; \ } while(0)
HeapObject swift::swift_allocObject(HeapMetadata const metadata, size_t requiredSize, size_t requiredAlignmentMask) { CALL_IMPL(swift_allocObject, (metadata, requiredSize, requiredAlignmentMask)); } ```
swift_allocObject中調用了_swift_allocObject_函數:
```
static HeapObject swift_allocObject(HeapMetadata const metadata,
size_t requiredSize,
size_t requiredAlignmentMask) {
assert(isAlignmentMask(requiredAlignmentMask));
auto object = reinterpret_cast
// 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:
``` 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
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; } ``` 可以看到 Swift 中也調用了 malloc 函數。
由此我們可以得到 Swift 對象的內存分配:
- __allocating_init -> swift_allocObject -> swift_allocObject -> swift_slowAlloc -> Malloc
- Swift對象的內存結構HeapObject(OC objc_object),有兩個屬性: 一個是Metadata,一個是RefCount,默認佔用16字節大小。
``` #define SWIFT_HEAPOBJECT_NON_OBJC_MEMBERS \ InlineRefCounts refCounts
/// The Swift heap-object header. /// This must match RefCountedStructTy in IRGen. struct HeapObject { /// This is always a valid pointer to a metadata object. HeapMetadata const *__ptrauth_objc_isa_pointer 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) { }
ifndef NDEBUG
void dump() const SWIFT_USED;
endif
endif // swift
};
簡化: struct HeapObject { HeapMetadata const * metadata; InlineRefCounts refCounts; } ``` objc_object 只有一個 isa,HeapObject 缺有兩個屬性,接下來我們對 Metadata 進行探索
5.類的結構探索
- Metadata源碼分析
Metadata是HeapMetadata類型,查看HeapMetadata: ``` struct InProcess;
template
這是一個類似別名的定義,下面查看TargetHeapMetadata
```
template
TargetHeapMetadata() = default;
constexpr TargetHeapMetadata(MetadataKind kind)
: TargetMetadata
if SWIFT_OBJC_INTEROP
constexpr TargetHeapMetadata(TargetAnyClassMetadata
endif
};
using HeapMetadata = TargetHeapMetadata
- TargetHeapMetadata繼承了TargetMetadata
-
在初始化方法中,當是純Swift類時傳入了MetadataKind,如果和OC交互時,它就傳了一個isa
-
MetadataKind 源碼分析
``` enum class MetadataKind : uint32_t {
define METADATAKIND(name, value) name = value,
#define ABSTRACTMETADATAKIND(name, start, end) \ name##_Start = start, name##_End = end,
include "MetadataKind.def"
LastEnumerated = 0x7FF, }; ``` MetadataKind可以看到它是一個uint32_t類型
和Swift中的類型關聯表如下:
``` /// A class type. NOMINALTYPEMETADATAKIND(Class, 0)
/// A struct type. NOMINALTYPEMETADATAKIND(Struct, 0 | MetadataKindIsNonHeap)
/// An enum type. /// If we add reference enums, that needs to go here. NOMINALTYPEMETADATAKIND(Enum, 1 | MetadataKindIsNonHeap)
/// An optional type. NOMINALTYPEMETADATAKIND(Optional, 2 | MetadataKindIsNonHeap)
/// A foreign class, such as a Core Foundation class. METADATAKIND(ForeignClass, 3 | MetadataKindIsNonHeap)
/// A type whose value is not exposed in the metadata system. METADATAKIND(Opaque, 0 | MetadataKindIsRuntimePrivate | MetadataKindIsNonHeap)
/// A tuple. METADATAKIND(Tuple, 1 | MetadataKindIsRuntimePrivate | MetadataKindIsNonHeap)
/// A monomorphic function. METADATAKIND(Function, 2 | MetadataKindIsRuntimePrivate | MetadataKindIsNonHeap)
/// An existential type. METADATAKIND(Existential, 3 | MetadataKindIsRuntimePrivate | MetadataKindIsNonHeap)
/// A metatype. METADATAKIND(Metatype, 4 | MetadataKindIsRuntimePrivate | MetadataKindIsNonHeap)
/// An ObjC class wrapper. METADATAKIND(ObjCClassWrapper, 5 | MetadataKindIsRuntimePrivate | MetadataKindIsNonHeap)
/// An existential metatype. METADATAKIND(ExistentialMetatype, 6 | MetadataKindIsRuntimePrivate | MetadataKindIsNonHeap)
/// A heap-allocated local variable using statically-generated metadata. METADATAKIND(HeapLocalVariable, 0 | MetadataKindIsNonType)
/// A heap-allocated local variable using runtime-instantiated metadata. METADATAKIND(HeapGenericLocalVariable, 0 | MetadataKindIsNonType | MetadataKindIsRuntimePrivate)
/// A native error object. METADATAKIND(ErrorObject, 1 | MetadataKindIsNonType | MetadataKindIsRuntimePrivate)
/// A heap-allocated task. METADATAKIND(Task, 2 | MetadataKindIsNonType | MetadataKindIsRuntimePrivate)
/// A non-task async job. METADATAKIND(Job, 3 | MetadataKindIsNonType | MetadataKindIsRuntimePrivate) ```
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|
- 類結構源碼分析
查看TargetHeapMetadata的父類TargetMetadata:
```
template
/// The basic header type.
typedef TargetTypeMetadataHeader
constexpr TargetMetadata()
: Kind(static_cast
if SWIFT_OBJC_INTEROP
protected:
constexpr TargetMetadata(TargetAnyClassMetadata
endif
private: /// The kind. Only valid for non-class metadata; getKind() must be used to get /// the kind value. StoredPointer Kind; public: /// Get the metadata kind. MetadataKind getKind() const { return getEnumeratedMetadataKind(Kind); }
/// Set the metadata kind.
void setKind(MetadataKind kind) {
Kind = static_cast
......
ConstTargetMetadataPointer
查看TargetClassMetadata
```
struct TargetClassMetadata : public TargetAnyClassMetadata
struct TargetAnyClassMetadata : public TargetHeapMetadata
TargetSignedPointer<Runtime, const TargetClassMetadata<Runtime> * __ptrauth_swift_objc_superclass> Superclass;
if SWIFT_OBJC_INTEROP
TargetPointer
StoredSize Data;
static constexpr StoredPointer offsetToData() { return offsetof(TargetAnyClassMetadata, Data); }
endif
} ```
在TargetAnyClassMetadata中我們可以找到superclass、Data、CacheData等成員變量,經過上面一系列的分析,我們可以得到swift類的數據結構如下
struct Metadata {
var kind: Int
var superClass: Any.Type
var cacheData: (Int, Int)
var data: Int
var classFlags: Int32
var instanceAddressPoint: UInt32 var instanceSize: UInt32
var instanceAlignmentMask: UInt16
var reserved: UInt16
var classSize: UInt32
var classAddressPoint: UInt32
var typeDescriptor: UnsafeMutableRawPointer var iVarDestroyer: UnsafeRawPointer
}
運行代碼 ``` struct HeapObject{ var metadata: UnsafeRawPointer var refcounted1: UInt32 var refcounted2: UInt32 }
struct Metadata{ var kind: Int var superClass: Any.Type var cacheData: (Int, Int) var data: Int var classFlags: Int32 var instanceAddressPoint: UInt32 var instanceSize: UInt32 var instanceAlignmentMask: UInt16 var reserved: UInt16 var classSize: UInt32 var classAddressPoint: UInt32 var typeDescriptor: UnsafeMutableRawPointer var iVarDestroyer: UnsafeRawPointer }
class Person { var age: Int = 18 var name: String = "tony"
init(age: Int, metadataname: String) {
self.age = age
self.name = metadataname
}
} var p = Person(age: 18, metadataname: "tony")
let objcRawPtr = Unmanaged.passUnretained(p as AnyObject).toOpaque() let objcPtr = objcRawPtr.bindMemory(to: HeapObject.self, capacity: 1) print(objcPtr.pointee)
let metadata = objcPtr.pointee.metadata.bindMemory(to: Metadata.self, capacity: MemoryLayout
HeapObject(metadata: 0x00000001000082b0, refcounted1: 3, refcounted2: 0) Metadata(kind: 4295000696, superClass: _TtCs12_SwiftObject, cacheData: (6598086240, 140943646785536), data: 4302323938, classFlags: 2, instanceAddressPoint: 0, instanceSize: 40, instanceAlignmentMask: 7, reserved: 0, classSize: 168, classAddressPoint: 16, typeDescriptor: 0x0000000100003c78, iVarDestroyer: 0x0000000000000000) end ```
----------------------------------------分割線-------------------------------------
值類型
前提:需要了解內存五大區,如下所示
- 棧空間 比 堆空間 大
- 棧是從高地址->低地址,向下延伸,由系統自動管理,是一片連續的內存空間
- 堆是從低地址->高地址,向上延伸,由程序員管理,堆空間結構類似於鏈表,是不連續的
- 日常開發中的溢出是指堆棧溢出,可以理解為棧區與堆區邊界碰撞的情況
- 全局區、常量區都存儲在Mach-O中的__TEXT cString段
我們通過一個例子來引入什麼是值類型 ``` func test(){ //棧區聲明一個地址,用來存儲age變量 var age = 18 //傳遞的值 var age2 = age //age、age2是修改獨立內存中的值 age = 30 age2 = 45
print("age=\(age),age2=\(age2)")
} test() ```
從例子中可以得出,age存儲在棧區
- 查看age的內存情況,從圖中可以看出,棧區直接存儲的是值 ``` 獲取age的棧區地址: po withUnsafePointer(to: &age) { print($0) } 查看age內存情況: (lldb) x/8g 0x000000016fdff3b8
(lldb) 0x000000016fdff3b8 0 elements
(lldb) x/8g 0x000000016fdff3b8 0x16fdff3b8: 0x0000000000000012 0x0000000000000000 0x16fdff3c8: 0x0000000000000000 0x000000016fdff3e0 0x16fdff3d8: 0x0000000100003b98 0x000000016fdff400 0x16fdff3e8: 0x0000000183fcd430 0x0000000183fcd430 ```
- 查看age2的情況,從下圖中可以看出,age2的賦值相當於將age中的值拿出來,賦值給了age2。其中age 與 age2 的地址 相差了8字節,從這裏可以説明棧空間是連續的、且是從高到低的 ``` (lldb) po withUnsafePointer(to: &age2) { print($0) } 0x000000016fdff3b0 0 elements
(lldb) x/8g 0x000000016fdff3b0 0x16fdff3b0: 0x0000000000000012 0x0000000000000012 0x16fdff3c0: 0x0000000000000000 0x0000000000000000 0x16fdff3d0: 0x000000016fdff3e0 0x0000000100003b98 0x16fdff3e0: 0x000000016fdff400 0x0000000183fcd430 ```
所以,從上面可以説明,age就是值類型
值類型特點 1. 地址中存儲的是值 2. 值類型的傳遞過程中,相當於傳遞了一個副本,也就是所謂的深拷貝 3. 值傳遞過程中,並不共享狀態
結構體
結構體的常用寫法 ``` //* 寫法一 *** struct JXTeacher { var age: Int = 18
func teach(){
print("teach")
}
} var t1 = JXTeacher()
//* 寫法二 *** struct JXTTeacher { var age: Int
func teach(){
print("teach")
}
} var t2 = JXTTeacher(age: 18) ```
- 在結構體中,如果不給屬性默認值,編譯是不會報錯的。即在結構體中屬性可以賦值,也可以不賦值
- init方法可以重寫,也可以使用系統默認的
結構體的SIL分析
-
如果沒有init,系統會提供不同的默認初始化方法
struct JXTeacher { @_hasStorage @_hasInitialValue var age: Int { get set } func teach() init() init(age: Int = 18) }
-
如果提供了自定義的init,就只有自定義的
struct JXTTeacher { @_hasStorage var age: Int { get set } func teach() init(age: Int) }
為什麼結構體是值類型?
定義一個結構體,並進行分析
struct JXTeacher {
var age: Int = 18
var age2: Int = 20
}
var t = JXTeacher()
print("end") //斷點
- 打印t:po t,從下圖中可以發現,t的打印直接就是值,沒有任何與地址有關的信息 ``` (lldb) po t ▿ JXTeacher
- age : 18
-
age2 : 20 ```
-
獲取t的內存地址,並查看其內存情況 ``` 獲取age的棧區地址: po withUnsafePointer(to: &t) { print($0) } 查看age內存情況: (lldb) x/8g 0x0000000100008030
(lldb) po withUnsafePointer(to: &t) { print($0) } 0x0000000100008030 0 elements
(lldb) x/8g 0x0000000100008030 0x100008030: 0x0000000000000012 0x0000000000000014 0x100008040: 0x0000000000000000 0x0000000000000000 0x100008050: 0x0000000000000000 0x0000000000000000 0x100008060: 0x0000000000000000 0x0000000000000000 ```
問題:此時將t賦值給t1,如果修改了t1,t會發生改變嗎? - 直接打印t及t1,可以發現t並沒有因為t1的改變而改變,主要是因為因為t1和t之間是值傳遞,即t1和t是不同內存空間,是直接將t中的值拷貝至t1中。t1修改的內存空間,是不會影響t的內存空間的 ``` struct JXTeacher { var age: Int = 18 var age2: Int = 20 } var t = JXTeacher() var t1 = t t1.age = 30 print("end") //斷點
(lldb) po t ▿ JXTeacher - age : 18 - age2 : 20
(lldb) po t1 ▿ JXTeacher - age : 30 - age2 : 20 ```
SIL驗證 同樣的,我們也可以通過分析SIL來驗證結構體是值類型
- 在SIL文件中,我們查看結構體的初始化方法,可以發現只有init,而沒有malloc,在其中看不到任何關於堆區的分配
```
// main
sil @main : [email protected](c) (Int32, UnsafeMutablePointer
>>) -> Int32 { bb0(%0 : $Int32, %1 : $UnsafeMutablePointer >>): alloc_global @main.t : main.JXTeacher // id: %2 %3 = global_addr @main.t : main.JXTeacher : $JXTeacher // users: %7, %10 %4 = metatype [email protected] JXTeacher.Type // user: %6 // function_ref JXTeacher.init() // 只有init方法,沒有malloc %5 = function_ref @main.JXTeacher.init() -> main.JXTeacher : [email protected](method) (@thin JXTeacher.Type) -> JXTeacher // user: %6 %6 = apply %5(%4) : [email protected](method) (@thin JXTeacher.Type) -> JXTeacher // user: %7 store %6 to %3 : $JXTeacher // id: %7 ...... %44 = integer_literal $Builtin.Int32, 0 // user: %45 %45 = struct $Int32 (%44 : $Builtin.Int32) // user: %46 return %45 : $Int32 // id: %46 } // end sil function 'main'
// JXTeacher.init() sil hidden @main.JXTeacher.init() -> main.JXTeacher : [email protected](method) (@thin JXTeacher.Type) -> JXTeacher { // %0 "$metatype" bb0(%0 : [email protected] JXTeacher.Type): // 默認alloc一個self,即結構體自身,其內存在棧區 %1 = alloc_stack $JXTeacher, let, name "self" // users: %6, %2, %11 // 根據結構體首地址,拿到age的地址 %2 = struct_element_addr %1 : $JXTeacher, #JXTeacher.age // user: %5 %3 = integer_literal $Builtin.Int64, 18 // user: %4 %4 = struct $Int (%3 : $Builtin.Int64) // users: %5, %10 // 將值存儲地址中 store %4 to %2 : $Int // id: %5 %6 = struct_element_addr %1 : $JXTeacher, #JXTeacher.age2 // user: %9 %7 = integer_literal $Builtin.Int64, 20 // user: %8 %8 = struct $Int (%7 : $Builtin.Int64) // users: %9, %10 store %8 to %6 : $Int // id: %9 %10 = struct $JXTeacher (%4 : $Int, %8 : $Int) // user: %12 dealloc_stack %1 : $*JXTeacher // id: %11 // 返回結構體自身 return %10 : $JXTeacher // id: %12 } // end sil function 'main.JXTeacher.init() -> main.JXTeacher' ```
總結
- 結構體是值類型,且結構體的地址就是第一個成員的內存地址
- 值類型
- 在內存中直接存儲值
- 值類型的賦值,是一個值傳遞的過程,即相當於拷貝了一個副本,存入不同的內存空間,兩個空間彼此間並不共享狀態
- 值傳遞其實就是深拷貝
引用類型
類
類的常用寫法 ``` //** 寫法一 * class JXTeacher { var age: Int = 18
func teach(){
print("teach")
}
init(_ age: Int) {
self.age = age
}
} var t1 = JXTeacher.init(20)
//** 寫法二 * class JXTTeacher { var age: Int?
func teach(){
print("teach")
}
init(_ age: Int) {
self.age = age
}
} var t2 = JXTTeacher.init(20) ``` - 在類中,如果屬性沒有賦值,也不是可選項,編譯會報錯 - 需要自己實現init方法
為什麼類是引用類型?
定義一個類,通過一個例子來説明
``` struct JXTeacher { var age: Int = 18 var age2: Int = 20 } class JXTTeacher { var age: Int = 18 var age2: Int = 20 } var t1 = JXTeacher() var t2 = JXTTeacher() print("end")
(lldb) po t1 ▿ JXTeacher - age : 18 - age2 : 20
(lldb) po t2
類初始化的對象t1,存儲在全局區 - 打印t1、t2: 從圖中可以看出,t2內存空間中存放的是地址,t1中存儲的是值 - 獲取t2變量的地址,並查看其內存情況 ``` 獲取t2指針地址 po withUnsafePointer(to: &t2) { print($0) } 查看t2全局區地址內存情況: x/8g 0x0000000100008220 查看t2地址中存儲的堆區地址內存情況: x/8g 0x00000001006072f0
(lldb) po withUnsafePointer(to: &t2) { print($0) } 0x0000000100008220 0 elements
(lldb) x/8g 0x0000000100008220 0x100008220: 0x00000001006072f0 0x0000000000000000 0x100008230: 0x0000000000000000 0x0000000000000000 0x100008240: 0x0000000000000000 0x0000000000000000 0x100008250: 0x0000000000000000 0x0000000000000000 (lldb) x/8g 0x00000001006072f0 0x1006072f0: 0x0000000100008178 (metadata) 0x0000000200000003 (refCounts) 0x100607300: 0x0000000000000012 (age的值18) 0x0000000000000014 (age2的值20) 0x100607310: 0x0000000000000000 0x0000000000000000 0x100607320: 0x0000000189570002 0x000200018a5bd650 ```
引用類型特點 1. 地址中存儲的是堆區地址 2. 堆區地址中存儲的是值
問題1:此時將t2賦值給t3,如果修改了t3,會導致t2修改嗎? - 通過lldb調試得知,修改了t3,會導致t3改變,主要是因為t2、t3地址中都存儲的是 同一個堆區地址,如果修改,修改是同一個堆區地址,所以修改t2會導致t1一起修改,即淺拷貝
``` class JXTTeacher { var age: Int = 18 var age2: Int = 20 } var t2 = JXTTeacher() var t3 = t2 t3.age = 30 print("end")
(lldb) po withUnsafePointer(to: &t2) { print($0) } 0x0000000100008220 0 elements
(lldb) po withUnsafePointer(to: &t3) { print($0) } 0x0000000100008228 0 elements
(lldb) x/8g 0x0000000100008220 0x100008220: 0x000000010603e2a0(堆區地址) 0x000000010603e2a0 0x100008230: 0x0000000000000000 0x0000000000000000 0x100008240: 0x0000000000000000 0x0000000000000000 0x100008250: 0x0000000000000000 0x0000000000000000 (lldb) x/8g 0x0000000100008228 0x100008228: 0x000000010603e2a0(堆區地址, 跟上面是同一個) 0x0000000000000000 0x100008238: 0x0000000000000000 0x0000000000000000 0x100008248: 0x0000000000000000 0x0000000000000000 0x100008258: 0x0000000000000000 0x0000000000000000 (lldb) x/8g 0x000000010603e2a0 0x10603e2a0: 0x0000000100008178 0x0000000200000003 0x10603e2b0: 0x000000000000001e 0x0000000000000014 0x10603e2c0: 0x00000009a0080001 0x00000001e36b3d48 0x10603e2d0: 0x0000000000000000 0x00000001ef3e7bc0 (lldb) po t2.age 30
(lldb) po t3.age 30 ```
問題2:如果結構體中包含類對象,此時如果修改t2中的實例對象屬性,t2會改變嗎?
代碼如下所示
``` struct JXTeacher { var age: Int = 18 var age2: Int = 20 var teacher: JXTTeacher = JXTTeacher() } class JXTTeacher { var age: Int = 18 var age2: Int = 20 }
var t = JXTeacher() var t1 = t t1.teacher.age = 30 print("end")
(lldb) po t1.teacher.age 30
(lldb) po t.teacher.age 30 ```
從打印結果中可以看出,如果修改t1中的實例對象屬性,會導致t中實例對象屬性的改變。雖然在結構體中是值傳遞,但是對於teacher,由於是引用類型,所以傳遞的依然是地址
同樣可以通過lldb調試驗證 ``` (lldb) po withUnsafePointer(to: &t) { print($0) } 0x0000000100008210 0 elements
(lldb) x/8g 0x0000000100008210 0x100008210: 0x0000000000000012(age:18) 0x0000000000000014(age2:20) 0x100008220: 0x000000010058c920(teacher地址) 0x0000000000000012 0x100008230: 0x0000000000000014 0x000000010058c920 0x100008240: 0x0000000000000000 0x0000000000000000 (lldb) x/8g 0x000000010058c920 (打印teacher內存情況) 0x10058c920: 0x0000000100008178 0x0000000200000003 0x10058c930: 0x000000000000001e(age:30) 0x0000000000000014(age2:20) 0x10058c940: 0x0000000000000000 0x0000000000000000 0x10058c950: 0x000000006fdf0006 0x000000016fdff0d0 ```
注意 在編寫代碼過程中,應該儘量避免值類型包含引用類型
查看當前的SIL文件,儘管JXTTeacher是放在值類型中的,在傳遞的過程中,不管是傳遞還是賦值,teacher都是按照引用計數進行管理的
```
// main
sil @main : [email protected](c) (Int32, UnsafeMutablePointer
// 對JXTTeacher的引用計數+1 %14 = struct_element_addr %13 : $JXTeacher, #JXTeacher.teacher // user: %15 %15 = load %14 : $JXTTeacher // users: %22, %20, %21, %16 strong_retain %15 : $JXTTeacher // id: %16
end_access %13 : $*JXTeacher // id: %17 ...... } // end sil function 'main'
// JXTeacher.teacher.getter sil hidden [transparent] @main.JXTeacher.teacher.getter : main.JXTTeacher : [email protected](method) (@guaranteed JXTeacher) -> @owned JXTTeacher { // %0 "self" // users: %2, %1 bb0(%0 : $JXTeacher): debug_value %0 : $JXTeacher, let, name "self", argno 1 // id: %1 %2 = struct_extract %0 : $JXTeacher, #JXTeacher.teacher // users: %4, %3 // 引用計數+1 strong_retain %2 : $JXTTeacher // id: %3 return %2 : $JXTTeacher // id: %4 } // end sil function 'main.JXTeacher.teacher.getter : main.JXTTeacher'
// JXTeacher.teacher.setter sil hidden [transparent] @main.JXTeacher.teacher.setter : main.JXTTeacher : [email protected](method) (@owned JXTTeacher, @inout JXTeacher) -> () { // %0 "value" // users: %11, %8, %4, %2 // %1 "self" // users: %5, %3 bb0(%0 : $JXTTeacher, %1 : $JXTeacher): debug_value %0 : $JXTTeacher, let, name "value", argno 1 // id: %2 debug_value_addr %1 : $JXTeacher, var, name "self", argno 2 // id: %3 // 引用計數+1 strong_retain %0 : $JXTTeacher // id: %4 %5 = begin_access [modify] [static] %1 : $JXTeacher // users: %10, %6 %6 = struct_element_addr %5 : $JXTeacher, #JXTeacher.teacher // users: %8, %7 %7 = load %6 : $JXTTeacher // user: %9 store %0 to %6 : $JXTTeacher // id: %8 strong_release %7 : $JXTTeacher // id: %9 end_access %5 : $*JXTeacher // id: %10 strong_release %0 : $JXTTeacher // id: %11 %12 = tuple () // user: %13 return %12 : $() // id: %13 } // end sil function 'main.JXTeacher.teacher.setter : main.JXTTeacher' ```
可以通過打印teacher的引用計數來驗證我們的説法,其中teacher的引用計數為3
(lldb) po CFGetRetainCount(t.teacher)
3
主要是是因為: - main中retain一次 - teacher.getter方法中retain一次 - teacher.setter方法中retain一次
mutaing
通過結構體定義一個棧,主要有push、pop方法,此時我們需要動態修改棧中的數組
- 如果是以下這種寫法,會直接報錯,原因是值類型本身是不允許修改屬性的
struct JXStack { var items: [Int] = [] func push(_ item: Int) { items.append(item) } } 報錯: Cannot use mutating member on immutable value: 'self' is immutable
- 將push方法改成下面的方式,查看SIL文件中的push函數 ``` struct JXStack { var items: [Int] = [] func push(_ item: Int) { print(item) } }
// JXStack.push(_:) sil hidden @main.JXStack.push(Swift.Int) -> () : [email protected](method) (Int, @guaranteed JXStack) -> () { // %0 "item" // users: %11, %2 // %1 "self" // user: %3 bb0(%0 : $Int, %1 : $JXStack): // 此時的self是let類型,即是不允許修改的 debug_value %0 : $Int, let, name "item", argno 1 // id: %2 debug_value %1 : $JXStack, let, name "self", argno 2 // id: %3 %4 = integer_literal $Builtin.Word, 1 // user: %6 ...... } ``` 從圖中可以看出,push函數除了item,還有一個默認參數self,self是let類型,表示不允許修改
- 嘗試1:如果將push函數修改成下面這樣,可以添加進去嗎? ``` struct JXStack { var items: [Int] = [] func push(_ item: Int) { var s = self s.items.append(item) } } var s = JXStack() s.push(1) print(s.items)
打印: [] ``` 以得出上面的代碼並不能將item添加進去,因為s是另一個結構體對象,相當於值拷貝,此時調用push是將item添加到s的數組中了
- 根據前文中的錯誤提示,給push添加mutaing,發現可以添加到數組了 ``` struct JXStack { var items: [Int] = [] mutating func push(_ item: Int) { items.append(item) } } var s = JXStack() s.push(1) print(s.items)
打印: [1] ```
查看其SIL文件,找到push函數,發現與之前有所不同,push添加mutaing(只用於值類型)後,本質上是給值類型函數添加了inout關鍵字,相當於在值傳遞的過程中,傳遞的是引用(即地址)
// JXStack.push(_:)
// mutaing的本質:添加了inout輸入輸出
sil hidden @main.JXStack.push(Swift.Int) -> () : [email protected](method) (Int, @inout JXStack) -> () {
// %0 "item" // users: %5, %2
// %1 "self" // users: %6, %3
bb0(%0 : $Int, %1 : $*JXStack):
debug_value %0 : $Int, let, name "item", argno 1 // id: %2
// self是var類型,可以修改,而且這裏訪問的地址,並不是原始的值
debug_value_addr %1 : $*JXStack, var, name "self", argno 2 // id: %3
%4 = alloc_stack $Int // users: %5, %11, %9
store %0 to %4 : $*Int // id: %5
%6 = begin_access [modify] [static] %1 : $*JXStack // users: %10, %7
%7 = struct_element_addr %6 : $*JXStack, #JXStack.items // user: %9
// function_ref Array.append(_:)
%8 = function_ref @Swift.Array.append(__owned A) -> () : [email protected](method) <τ_0_0> (@in τ_0_0, @inout Array<τ_0_0>) -> () // user: %9
%9 = apply %8<Int>(%4, %7) : [email protected](method) <τ_0_0> (@in τ_0_0, @inout Array<τ_0_0>) -> ()
end_access %6 : $*JXStack // id: %10
dealloc_stack %4 : $*Int // id: %11
%12 = tuple () // user: %13
return %12 : $() // id: %13
} // end sil function 'main.JXStack.push(Swift.Int) -> ()'
inout關鍵字
一般情況下,在函數的聲明中,默認的參數都是不可變的,如果想要直接修改,需要給參數加上inout關鍵字
- 未加inout關鍵字,給參數賦值,編譯報錯 ``` func swap(_ a: Int, _ b: Int) { let temp = a a = b b = temp }
Cannot assign to value: 'a' is a 'let' constant Cannot assign to value: 'b' is a 'let' constant ```
- 添加inout關鍵字,可以給參數賦值 ``` func swap(_ a: inout Int, _ b: inout Int) { let temp = a a = b b = temp } var a = 8 var b = 9 swap(&a, &b) print("end")
(lldb) po a 9
(lldb) po b 8 ```
總結
- 結構體中的函數如果想修改其中的屬性,需要在函數前加上mutating,而類則不用
- mutating本質也是加一個 inout修飾的self
- inout相當於取地址,可以理解為地址傳遞,即引用
- mutating修飾方法,而inout 修飾參數
總結
通過上述LLDB查看結構體 & 類的內存模型,有以下總結:
- 值類型,相當於一個本地excel,當我們通過QQ傳給你一個excel時,就相當於一個值類型,你修改了什麼我們這邊是不知道的
- 引用類型,相當於一個在線表格,當我們和你共同編輯一個在先表格時,就相當於一個引用類型,兩邊都會看到修改的內容
- 結構體中函數修改屬性, 需要在函數前添加mutating關鍵字,本質是給函數的默認參數self添加了inout關鍵字,將self從let常量改成了var變量
方法調度
通過上面的分析,我們有以下疑問:結構體和類的方法存儲在哪裏?下面來一一進行分析
靜態派發
值類型對象的函數的調用方式是靜態調用,即直接地址調用,調用函數指針,這個函數指針在編譯、鏈接完成後就已經確定了,存放在代碼段,而結構體內部並不存放方法。因此可以直接通過地址直接調用
``` struct JXTeacher { var age: Int = 18
func teach(){
print("teach")
}
} var t1 = JXTeacher()
print("end") ```
- 結構體函數調試如下所示
`` TT
main: 0x100003c00 <+0>: sub sp, sp, #0x40 ; =0x40 0x100003c04 <+4>: stp x29, x30, [sp, #0x30] 0x100003c08 <+8>: add x29, sp, #0x30 ; =0x30 0x100003c0c <+12>: bl 0x100003e10 ; TT.JXTeacher.init() -> TT.JXTeacher at main.swift:11 0x100003c10 <+16>: mov x8, x0 0x100003c14 <+20>: adrp x9, 5 0x100003c18 <+24>: str x9, [sp] 0x100003c1c <+28>: adrp x0, 5 0x100003c20 <+32>: add x0, x0, #0x40 ; =0x40 0x100003c24 <+36>: str x8, [x9, #0x40] 0x100003c28 <+40>: add x1, sp, #0x18 ; =0x18 0x100003c2c <+44>: str x1, [sp, #0x8] 0x100003c30 <+48>: mov w8, #0x20 0x100003c34 <+52>: mov x2, x8 0x100003c38 <+56>: mov x3, #0x0 0x100003c3c <+60>: bl 0x100003e80 ; symbol stub for: swift_beginAccess 0x100003c40 <+64>: ldr x8, [sp] 0x100003c44 <+68>: ldr x0, [sp, #0x8] 0x100003c48 <+72>: ldr x8, [x8, #0x40] 0x100003c4c <+76>: str x8, [sp, #0x10] 0x100003c50 <+80>: bl 0x100003e98 ; symbol stub for: swift_endAccess 0x100003c54 <+84>: ldr x0, [sp, #0x10] 直接地址調用,即靜態派發 -> 0x100003c58 <+88>: bl 0x100003c98 ; TT.JXTeacher.teach() -> () at main.swift:14 0x100003c5c <+92>: mov w0, #0x0 0x100003c60 <+96>: ldp x29, x30, [sp, #0x30] 0x100003c64 <+100>: add sp, sp, #0x40 ; =0x40 0x100003c68 <+104>: ret
```
- 打開打開demo的Mach-O可執行文件,其中的__text段,就是所謂的代碼段,需要執行的彙編指令都在這裏 Section64 (__TEXT, __text) -> Assembly
對於上面的分析,還有個疑問:直接地址調用後面是符號,這個符號哪裏來的?
是從Mach-O文件中的符號表Symbol Tables,但是符號表中並不存儲字符串,字符串存儲在String Table(字符串表,存放了所有的變量名和函數名,以字符串形式存儲),然後根據符號表中的偏移值到字符串中查找對應的字符,然後進行命名重整:工程名+類名+函數名,如下所示
- Symbol Table:存儲符號位於字符串表的位置
- Dynamic Symbol Table:動態庫函數位於符號表的偏移信息
還可以通過終端命令nm,獲取項目中的符號表
- 查看符號表: ``` nm mach-o文件路徑
// 獲取所有符號
nm /Users/jxwbjmac0003/Library/Developer/Xcode/DerivedData/TT-gdrpjudaexihasfhedwqvhmxjtal/Build/Products/Debug/TT
- 通過命令還原符號名稱:
xcrun swift-demangle 符號
// 還原第一個符號 xcrun swift-demangle s2TT9JXTeacherV5teachyyF $s2TT9JXTeacherV5teachyyF ---> TT.JXTeacher.teach() -> () ```
-
如果將edit scheme -> run中的debug改成release,編譯後查看,在可執行文件目錄下,多一個後綴為dSYM的文件,此時,再去Mach-O文件中查找teach,發現是找不到,其主要原因是因為靜態鏈接的函數,實際上是不需要符號的,一旦編譯完成,其地址確定後,當前的符號表就會刪除當前函數對應的符號,在release環境下,符號表中存儲的只是不能確定地址的符號
-
對於不能確定地址的符號,是在運行時確定的,即函數第一次調用時(相當於懶加載),例如print,是通過dyld_stub_bind確定地址的(這個在最新版的12.2中通過斷點調試並未找到,後續待繼續驗證,有不同見解的,歡迎留言指出)
0x100003d10: adr x17, #0x4478 ; _dyld_private
0x100003d14: nop
0x100003d18: stp x16, x17, [sp, #-0x10]!
0x100003d1c: nop
0x100003d20: ldr x16, #0x2f0 ; (void *)0x00000001895cbbd8: dyld_stub_binder(懶加載)
0x100003d24: br x16
0x100003d28: ldr w16, 0x100003d30
0x100003d2c: b 0x100003d10
函數符號命名規則
- 對於C函數來説,命名的重整規則就是在函數名之前加_(注意:C中不允許函數重載,因為沒有辦法區分) ```
include
void test(){} ```
- 對於OC來説,也不支持函數重載,其符號命名規則是-[類名 函數名]
@interface OCTest : NSObject
- (void)ocTest;
@end
- 對於Swift來説,是函數重載,主要是因為swift中的重整命名規則比較複雜,可以確保函數符號的唯一性
補充:ASLR
下面是針對函數地址的一個驗證
- 通過運行發現,Mach-O中的地址與調試時直接獲取的地址是由一定偏差的,其主要原因是實際調用時地址多了一個ASLR(地址空間佈局隨機化 address space layout randomizes)
0x1000039bc <+100>: ldr x0, [sp, #0x18]
0x1000039c0 <+104>: bl 0x100003aa8 ; SwiftTest.JXTeacher.teach() -> () at main.swift:15
0x1000039c4 <+108>: ldr x1, [sp, #0x30]
0x1000039c8 <+112>: mov w8, #0x1
0x1000039cc <+116>: mov x0, x8
0x1000039d0 <+120>: bl 0x100003d48 ; symbol stub for: Swift._allocateUninitializedArray<τ_0_0>(Builtin.Word) -> (Swift.Array<τ_0_0>, Builtin.RawPointer)
-
可以通過image list查看,其中0x0000000100000000是程序運行的首地址,後8位是隨機偏移00000000(即ASLR)
(lldb) image list [ 0] 2D7B9C51-B077-3AAA-8D1C-28C9404C814F 0x0000000100000000 /Users/jxwbjmac0003/Library/Developer/Xcode/DerivedData/SwiftTest-ezgreflbigvpqjcibzqxeikfdqsy/Build/Products/Debug/SwiftTest [ 1] 38657979-1ABE-3C9A-BF64-EF3B746216AB 0x0000000100014000 /usr/lib/dyld [ 2] A23D1D3A-AD28-3AC2-AEAF-53F4B7A5B2F5 0x000000018a3f2000 /System/Library/Frameworks/Foundation.framework/Versions/C/Foundation [ 3] 252C93CC-2D39-3C15-87F6-1336658B2F49 0x0000000189441000 /usr/lib/libobjc.A.dylib [ 4] 1E75FCDF-2357-30FE-AAAD-5290BA722464 0x000000019304a000 /usr/lib/libSystem.B.dylib [ 5] 1FC1BD60-DC83-3CC7-89AC-D734DC18473A 0x000000018962a000 /System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation [ 6] 9CA57826-61D7-3095-9476-AE0FE45A3805 0x0000000195244000 /usr/lib/swift/libswiftCore.dylib
-
將Mach-O中的文件地址0x0000000100003C98 + 0x00000000 = 0x100003C98,正好對應上面調用的地址
動態派發
彙編指令補充
- blr:帶返回的跳轉指令,跳轉到指令後邊跟隨寄存器中保存的地址
- mov:將某一寄存器的值複製到另一寄存器(只能用於寄存器與起存起或者 寄存器與常量之間 傳值,不能用於內存地址)
- mov x1, x0 將寄存器x0的值複製到寄存器x1中
- ldr:將內存中的值讀取到寄存器中
- ldr x0, [x1, x2] 將寄存器x1和寄存器x2 相加作為地址,取該內存地址的值翻入寄存器x0中
- str:將寄存器中的值寫入到內存中
- str x0, [x0, x8] 將寄存器x0的值保存到內存[x0 + x8]處
- bl:跳轉到某地址
探索class的調度方式
首先介紹下V_Table在SIL文件中的格式 ``` VTables ~~~~~~~ :: // 聲明sil vtable關鍵字 decl ::= sil-vtable // sil vtable中包含 關鍵字、標識(即類名)、所有的方法 sil-vtable ::= 'sil_vtable' identifier '{' sil-vtable-entry* '}'
// 方法中包含了聲明以及函數名稱 sil-vtable-entry ::= sil-decl-ref ':' sil-linkage? sil-function-name ```
例如,以CJLTacher為例,其SIL中的v-table如下所示
class JXTeacher{
func teach(){}
func teach2(){}
func teach3(){}
@objc deinit{}
init(){}
}
- sil_vtable:關鍵字
- JXTeacher:表示是JXTeacher類的函數表
- 其次就是當前方法的聲明對應着方法的名稱
- 函數表 可以理解為 數組,聲明在 class內部的方法在不加任何關鍵字修飾的過程中,是連續存放在我們當前的地址空間中的。這一點,可以通過斷點來印證,
- register read x20,此時的地址和 實例對象的地址是相同的,其中x8 實例對象地址,即首地址
觀察這幾個方法的偏移地址,可以發現方法是連續存放的,正好對應V-Table函數表中的排放順序,即是按照定義順序排放在函數表中
函數表源碼探索
- 源碼中搜索initClassVTable,並加上斷點,然後寫上源碼進行調試
```
/// Using the information in the class context descriptor, fill in in the
/// immediate vtable entries for the class and install overrides of any
/// superclass vtable entries.
static void initClassVTable(ClassMetadata self) {
const auto description = self->getDescription();
auto *classWords = reinterpret_cast
(self);
if (description->hasVTable()) { auto *vtable = description->getVTableDescriptor(); auto vtableOffset = vtable->getVTableOffset(description); auto descriptors = description->getMethodDescriptors(); for (unsigned i = 0, e = vtable->VTableSize; i < e; ++i) { auto &methodDescription = descriptors[i]; swift_ptrauth_init_code_or_data( &classWords[vtableOffset + i], methodDescription.Impl.get(), methodDescription.Flags.getExtraDiscriminator(), !methodDescription.Flags.isAsync()); } }
if (description->hasOverrideTable()) { auto *overrideTable = description->getOverrideTable(); auto overrideDescriptors = description->getMethodOverrideDescriptors();
for (unsigned i = 0, e = overrideTable->NumEntries; i < e; ++i) {
auto &descriptor = overrideDescriptors[i];
// Get the base class and method.
auto *baseClass = cast_or_null<ClassDescriptor>(descriptor.Class.get());
auto *baseMethod = descriptor.Method.get();
// If the base method is null, it's an unavailable weak-linked
// symbol.
if (baseClass == nullptr || baseMethod == nullptr)
continue;
// Calculate the base method's vtable offset from the
// base method descriptor. The offset will be relative
// to the base class's vtable start offset.
auto baseClassMethods = baseClass->getMethodDescriptors();
// If the method descriptor doesn't land within the bounds of the
// method table, abort.
if (baseMethod < baseClassMethods.begin() ||
baseMethod >= baseClassMethods.end()) {
fatalError(0, "resilient vtable at %p contains out-of-bounds "
"method descriptor %p\n",
overrideTable, baseMethod);
}
// Install the method override in our vtable.
auto baseVTable = baseClass->getVTableDescriptor();
auto offset = (baseVTable->getVTableOffset(baseClass) +
(baseMethod - baseClassMethods.data()));
swift_ptrauth_init_code_or_data(&classWords[offset],
descriptor.Impl.get(),
baseMethod->Flags.getExtraDiscriminator(),
!baseMethod->Flags.isAsync());
}
} } ``` 其內部是通過for循環編碼,然後offset+index偏移,然後獲取method,將其存入到偏移後的內存中,從這裏可以印證函數是連續存放的
對於class中函數來説,類的方法調度是通過V-Table,其本質就是一個連續的內存空間(數組結構)
問題:如果更改方法聲明的位置呢?例如extension中的函數,此時的函數調度方式還是函數表調度嗎?
通過以下代碼驗證
-
定義一個JXTeacher的extension
extension JXTeacher { func teach4() {print("teach4")} }
-
在定義一個子類JXStudent繼承自JXTeacher,查看SIL中的V-Table
class JXStudent: JXTeacher{ }
-
查看SIL文件,發現子類只繼承了class中定義的函數,即函數表中的函數 ``` sil_vtable JXTeacher { #JXTeacher.teach: (JXTeacher) -> () -> () : @main.JXTeacher.teach() -> () // JXTeacher.teach() #JXTeacher.teach2: (JXTeacher) -> () -> () : @main.JXTeacher.teach2() -> () // JXTeacher.teach2() #JXTeacher.teach3: (JXTeacher) -> () -> () : @main.JXTeacher.teach3() -> () // JXTeacher.teach3() #JXTeacher.init!allocator: (JXTeacher.Type) -> () -> JXTeacher : @main.JXTeacher.__allocating_init() -> main.JXTeacher // JXTeacher.__allocating_init() #JXTeacher.deinit!deallocator: @main.JXTeacher.__deallocating_deinit // JXTeacher.__deallocating_deinit }
sil_vtable JXStudent { #JXTeacher.teach: (JXTeacher) -> () -> () : @main.JXTeacher.teach() -> () [inherited] // JXTeacher.teach() #JXTeacher.teach2: (JXTeacher) -> () -> () : @main.JXTeacher.teach2() -> () [inherited] // JXTeacher.teach2() #JXTeacher.teach3: (JXTeacher) -> () -> () : @main.JXTeacher.teach3() -> () [inherited] // JXTeacher.teach3() #JXTeacher.init!allocator: (JXTeacher.Type) -> () -> JXTeacher : @main.JXStudent.__allocating_init() -> main.JXStudent [override] // JXStudent.__allocating_init() #JXStudent.deinit!deallocator: @main.JXStudent.__deallocating_deinit // JXStudent.__deallocating_deinit } ```
其原因是因為子類將父類的函數表全部繼承了,如果此時子類增加函數,會繼續在連續的地址中插入,假設extension函數也是在函數表中,則意味着子類也有,但是子類無法並沒有相關的指針記錄函數 是父類方法 還是 子類方法,所以不知道方法該從哪裏插入,導致extension中的函數無法安全的放入子類中。所以在這裏可以側面證明extension中的方法是直接調用的,且只屬於類,子類是無法繼承的
開發注意點:
- 繼承方法和屬性,不能寫extension中。
- 而extension中創建的函數,一定是隻屬於自己類,但是其子類也有其訪問權限,只是不能繼承和重寫,如下所示 ``` class JXTeacher{ func teach2(){print("teach2")} func study() {} @objc deinit{} init(){} }
extension JXTeacher{ var age: Int{ get{ return 18 } } func teach(){ print("teach") } }
class JXMiddleTeacher: JXTeacher{ override func study() { print("JXMiddleTeacher study") } }
var t = JXMiddleTeacher() //子類有父類extension中方法的訪問權限,只是不能繼承和重寫 t.teach() t.study() print(t.age)
teach JXMiddleTeacher study 18 ```
final、@objc、dynamic修飾函數
final 修飾
- final 修飾的方法是 直接調度的,可以通過SIL驗證 + 斷點驗證
``` class JXTeacher { final func teach(){ print("teach") } func teach2(){ print("teach2") } func teach3(){ print("teach3") } func teach4(){ print("teach4") } @objc deinit{} init(){} }
class ViewController: UIViewController {
override func viewDidLoad() { //斷點
super.viewDidLoad()
let t1 = JXTeacher()
t1.teach()
t1.teach2()
t1.teach3()
t1.teach4()
}
}
```
SIL驗證
sil_vtable JXTeacher {
#JXTeacher.teach2: (JXTeacher) -> () -> () : @main.JXTeacher.teach2() -> () // JXTeacher.teach2()
#JXTeacher.teach3: (JXTeacher) -> () -> () : @main.JXTeacher.teach3() -> () // JXTeacher.teach3()
#JXTeacher.teach4: (JXTeacher) -> () -> () : @main.JXTeacher.teach4() -> () // JXTeacher.teach4()
#JXTeacher.init!allocator: (JXTeacher.Type) -> () -> JXTeacher : @main.JXTeacher.__allocating_init() -> main.JXTeacher // JXTeacher.__allocating_init()
#JXTeacher.deinit!deallocator: @main.JXTeacher.__deallocating_deinit // JXTeacher.__deallocating_deinit
}
@objc 修飾
使用@objc關鍵字是將swift中的方法暴露給OC
``` // JXTeacher.swift class JXTeacher: NSObject { @objc func teach(){ print("teach") } func teach2(){ print("teach2") } func teach3(){ print("teach3") } @objc func teach4(){ print("teach4") let person: JXPerson = JXPerson() person.teach5(); } @objc deinit{} override init(){} }
// JXPerson.h @interface JXPerson : NSObject - (void)teach5; @end
// JXPerson.m
import "JXPerson.h"
@implementation JXPerson - (void)teach5 { NSLog(@"%s", func); } @end
// OCDemo-Bridging-Header.h
import "JXPerson.h"
import "OCDemo-Swift.h"
- (void)viewDidLoad { [super viewDidLoad]; JXTeacher *t = [[JXTeacher alloc] init]; [t teach]; }
輸出: teach teach4 2021-11-25 17:04:07.519649+0800 OCDemo[79235:1700332] -[JXPerson teach5] ```
查看SIL文件發現被@objc修飾的函數聲明有兩個:swift + OC(內部調用的swift中的teach函數)
swiftc -emit-sil JXTeacher.swift | xcrun swift-demangle >> ./jxteacher.sil && open jxteacher.sil
``` // JXTeacher.teach() swift中的函數 sil hidden @JXTeacher.JXTeacher.teach() -> () : [email protected](method) (@guaranteed JXTeacher) -> () { // %0 "self" // user: %1 bb0(%0 : $JXTeacher): ...... }
// @objc JXTeacher.teach() OC中的函數,實際內部調用swift中的函數 sil hidden [thunk] @@objc JXTeacher.JXTeacher.teach() -> () : [email protected](objc_method) (JXTeacher) -> () { // %0 // users: %4, %3, %1 bb0(%0 : $JXTeacher): strong_retain %0 : $JXTeacher // id: %1 // function_ref JXTeacher.teach() %2 = function_ref @JXTeacher.JXTeacher.teach() -> () : [email protected](method) (@guaranteed JXTeacher) -> () // user: %3 %3 = apply %2(%0) : [email protected](method) (@guaranteed JXTeacher) -> () // user: %5 strong_release %0 : $JXTeacher // id: %4 return %3 : $() // id: %5 } // end sil function '@objc JXTeacher.JXTeacher.teach() -> ()' ``` 即在SIL文件中生成了兩個方法 - swift原有的函數 - @objc標記暴露給OC來使用的函數: 內部調用swift的
dynamic 修飾
以下面代碼為例,查看dynamic修飾的函數的調度方式
class JXTeacher: NSObject {
dynamic func teach(){ print("teach") }
func teach2(){ print("teach2") }
func teach3(){ print("teach3") }
func teach4(){ print("teach4") }
@objc deinit{}
override init(){}
}
其中teach函數的調度還是 函數表調度,可以通過斷點調試驗證,使用dynamic的意思是可以動態修改,意味着當類繼承自NSObject時,可以使用method-swizzling
@objc + dynamic
class JXTeacher: NSObject {
@objc dynamic func teach(){ print("teach") }
func teach2(){ print("teach2") }
func teach3(){ print("teach3") }
func teach4(){ print("teach4") }
@objc deinit{}
override init(){}
}
通過斷點調試,走的是objc_msgSend流程,即 動態消息轉發
OCDemo`-[ViewController viewDidLoad]:
0x1023784c4 <+0>: sub sp, sp, #0x40 ; =0x40
0x1023784c8 <+4>: stp x29, x30, [sp, #0x30]
0x1023784cc <+8>: add x29, sp, #0x30 ; =0x30
0x1023784d0 <+12>: stur x0, [x29, #-0x8]
0x1023784d4 <+16>: stur x1, [x29, #-0x10]
0x1023784d8 <+20>: ldur x8, [x29, #-0x8]
0x1023784dc <+24>: add x0, sp, #0x10 ; =0x10
0x1023784e0 <+28>: str x8, [sp, #0x10]
0x1023784e4 <+32>: adrp x8, 9
0x1023784e8 <+36>: ldr x8, [x8, #0x5e8]
0x1023784ec <+40>: str x8, [sp, #0x18]
0x1023784f0 <+44>: adrp x8, 9
0x1023784f4 <+48>: ldr x1, [x8, #0x5a0]
0x1023784f8 <+52>: bl 0x10237a0fc ; symbol stub for: objc_msgSendSuper2
0x1023784fc <+56>: adrp x8, 9
0x102378500 <+60>: ldr x0, [x8, #0x5d0]
0x102378504 <+64>: bl 0x10237a0a8 ; symbol stub for: objc_alloc_init
0x102378508 <+68>: add x8, sp, #0x8 ; =0x8
0x10237850c <+72>: str x8, [sp]
0x102378510 <+76>: str x0, [sp, #0x8]
-> 0x102378514 <+80>: ldr x0, [sp, #0x8]
0x102378518 <+84>: adrp x8, 9
0x10237851c <+88>: ldr x1, [x8, #0x5a8]
0x102378520 <+92>: bl 0x10237a0f0 ; symbol stub for: objc_msgSend
0x102378524 <+96>: ldr x0, [sp]
0x102378528 <+100>: mov x1, #0x0
0x10237852c <+104>: bl 0x10237a144 ; symbol stub for: objc_storeStrong
0x102378530 <+108>: ldp x29, x30, [sp, #0x30]
0x102378534 <+112>: add sp, sp, #0x40 ; =0x40
0x102378538 <+116>: ret
場景:swift中實現方法交換
在swift中的需要交換的函數前,使用dynamic修飾,然後通過:@_dynamicReplacement(for: 函數符號)進行交換,如下所示
``` class JXTeacher: NSObject { dynamic func teach(){ print("teach") } func teach2(){ print("teach2") } func teach3(){ print("teach3") } func teach4(){ print("teach4") } @objc deinit{} override init(){} }
extension JXTeacher { @_dynamicReplacement(for: teach) func teach5() { print("teach5") } }
let t = JXTeacher() t.teach() t.teach2() t.teach3() t.teach4() print("end")
輸出: teach5 teach2 teach3 teach4 end ```
- 如果teach沒有實現 / 如果去掉dynamic修飾符,會報錯
總結
- struct是值類型,其中函數的調度屬於直接調用地址,即靜態調度
- class是引用類型,其中函數的調度是通過V-Table函數表來進行調度的,即動態調度
- extension中的函數調度方式是直接調度
- final修飾的函數調度方式是直接調度
- @objc修飾的函數調度方式是函數表調度,如果OC中需要使用,class還必須繼承NSObject
- dynamic修飾的函數的調度方式是函數表調度,使函數具有動態性
- @objc + dynamic 組合修飾的函數調度,是執行的是objc_msgSend流程,即 動態消息轉發
補充
內存插件libfooplugin
主要補充內存插件libfooplugin.dylib安裝及使用
方法一 在通過lldb調試的時候, ``` 直接輸入 plugin load libfooplugin.dylib路徑 如: plugin load /Users/jxwbjmac0003/Documents/M1_libfooplugin/libLGCatAddress.dylib
(lldb) cat address 0x0000000100008240 ```
方法二 在跟目下創建.lldbinit文件 ``` vim /.lldbinit
然後輸入 plugin load libfooplugin.dylib路徑
(lldb) cat address 地址 ```
方法三
在任意目錄創建文件,然後在文件中輸入
plugin load libfooplugin.dylib路徑
打開你需要使用libfooplugin的工程,然後依次點擊
Product -> Scheme -> Edit Scheme