iOS內存分區
Stack
棧區就是存放當前:局部變量和函數運行過程中的上下文。
func test() {
var age: Int = 10
print(age)
}
test()
(lldb) po withUnsafePointer(to: &age){print($0)}
0x00007ffeefbff3d8
0 elements
(lldb) cat address 0x00007ffeefbff3d8
&0x00007ffeefbff3d8, stack address (SP: 0x7ffeefbff3b0 FP: 0x7ffeefbff3e0) SwiftPointer.test() -> ()
(lldb)
直接定義一個局部變量,然后lldb
查看一下,可以看到是一個Stack address
。
Heap
對于堆空間,通過new & malloc
關鍵字來申請內存空間,不連續,類似鏈表的數據結構。
class HotpotCat {
var age: Int = 18
}
func test() {
var hotpot = HotpotCat()
print(hotpot)
}
test()
(lldb) po hotpot
<HotpotCat: 0x101157190>
(lldb) cat address 0x101157190
&0x101157190, (char *) $4 = 0x00007ffeefbff250 "0x101157190 heap pointer, (0x20 bytes), zone: 0x7fff88aa8000"
- hotpot變量存放在棧區
- hotpot變量中存放的地址位于堆區
全局區(靜態區)
//全局已初始化變量
int age = 10;
//全局未初始化變量
int age1;
//全局靜態變量
static int age2 = 30;
int main(int argc, const char * argv[]) {
// insert code here...
char *p = "Hotpot";
printf("%d",age);
printf("%d",age1);
printf("%d",age2);
return 0;
}
(lldb) po &age
0x0000000100008010
(lldb) cat address 0x0000000100008010
&0x0000000100008010, age <+0> CTest.__DATA.__data
(lldb)
age
位于可執行文件__DATA.__data
中
(lldb) po &age1
0x0000000100008018
(lldb) cat address 0x0000000100008018
&0x0000000100008018, age1 <+0> CTest.__DATA.__common
age1
位于__DATA.__common
中。這里將未初始化和已初始化變量分開存放(劃分更細),便于定位符號。
//如果不調用age2,po &會找不到地址。
(lldb) po &age2
0x0000000100008014
(lldb) cat address 0x0000000100008014
age2
位于__DATA.__data
中。
仔細觀察3個變量的地址,可以看到age
和age2
離額比較近。這也就驗證了全局區分開存儲了已初始化和未初始化比變量。age1
地址比較大,所以
在全局區中,已初始化變量位于未初始化變量下方(按地址大小)。
&age
0x0000000100008010
&age1
0x0000000100008018
age2
0x0000000100008014
常量區
//全局靜態常量
static const int age3 = 30;
int main(int argc, const char * argv[]) {
int age4 = age3;
return 0;
}
(lldb) po &age3
0x00000001004f8440
(lldb) cat address 0x00000001004f8440
Couldn't find address, reverting to "image lookup -a 0x00000001004f8440"
(lldb)
接著上面的例子,定義一個static const
修飾的age3
,發現查找不到地址,斷點調試發現直接賦值的是30
。也就是沒有age3
這個符號,直接存儲的是30
這個值。
那么去掉
static
(lldb) po &age3
0x0000000100003fa0
(lldb) cat address 0x0000000100003fa0
&0x0000000100003fa0, age3 <+0> CTest.__TEXT.__const
發現age3
在__TEXT.__const
再驗證下
*p
。__text
就是我們當前執行的指令。
在swift
中,調試的時候全局變量初始化前在__common
段,初始化后還是在__common
段,按理說應該在__data
段的。
總結:分區(人為的籠統的劃分)和 macho
文件不是一個東西(放在segment
的section
中,符號劃分更細)。可以簡單理解為Windows
和 MacOS
方法調度
結構體 靜態調用(直接調用)。在編譯完成后方法的地址就確定了,在執行代碼的過程中就直接跳轉到地址執行方法。對于類中的方法存儲在V-Table
中,執行的時候去V-table
中找。
V-Table(函數表)
V-table
是一個數組結構擴展中是直接調用在SIL
中是這樣表示的:
decl ::= sil-vtable
sil-vtable ::= 'sil_vtable' identifier '{' sil-vtable-entry* '}'
sil-vtable-entry ::= sil-decl-ref ':' sil-linkage? sil-function-name
identifier
標識就是類, sil-decl-ref
聲明,sil-function-name
方法名稱。
直接看一個SIL
文件
class HotpotCat {
func test()
func test1()
func test2()
func test3()
init()
@objc deinit
}
sil_vtable HotpotCat {
#HotpotCat.test: (HotpotCat) -> () -> () : @main.HotpotCat.test() -> () // HotpotCat.test()
#HotpotCat.test1: (HotpotCat) -> () -> () : @main.HotpotCat.test1() -> () // HotpotCat.test1()
#HotpotCat.test2: (HotpotCat) -> () -> () : @main.HotpotCat.test2() -> () // HotpotCat.test2()
#HotpotCat.test3: (HotpotCat) -> () -> () : @main.HotpotCat.test3() -> () // HotpotCat.test3()
#HotpotCat.init!allocator: (HotpotCat.Type) -> () -> HotpotCat : @main.HotpotCat.__allocating_init() -> main.HotpotCat // HotpotCat.__allocating_init()
#HotpotCat.deinit!deallocator: @main.HotpotCat.__deallocating_deinit // HotpotCat.__deallocating_deinit
}
sil_vtable
是關鍵字,HotpotCat
代表是HotpotCat class
的函數表。這張表本質就是數組,在聲明class
內部的方法時不加任何關鍵字修飾的時候就連續存放在我們當前的地址空間中。
斷點觀察
斷點直觀觀察一下,先熟悉一下匯編指令:
ARM64匯編指令
- blr:帶返回的跳轉指令,跳轉到指令后邊跟隨寄存器中保存的地址
- mov:將某一寄存器的值復制到另一寄存器(只能用于
寄存器與寄存器
/寄存器與常量
之間傳值,不能用于內存地址)
如:mov x1,x0
將寄存器x0
的值復制到寄存器x1
中。 - ldr:將內存中的值讀取到寄存器
如:ldr x0, [x1, x2]
將寄存器x1
和寄存器x2
相加作為地址,取該內存地址的值放入x0
中。 - str:將寄存器中的值寫入到內存中
如:str x0, [x0, x8]
將寄存器x0
的值保存到內存[x0 + x8]
處。 - bl: 跳轉到某地址
直接斷點調試
x9地址是從x8偏移0x60獲取的,讀取下x8內容,x8就是hotpot
(lldb) register read x8
x8 = 0x00000002837ad610
(lldb) x/8g 0x00000002837ad610
0x2837ad610: 0x0000000104b4dca8 0x0000000200000003
0x2837ad620: 0x000098b3fd5dd620 0x00000002097b0164
0x2837ad630: 0x000098b3fd5dd630 0x0000000209770165
0x2837ad640: 0x000098b3fd5dd640 0x0000000209770167
(lldb)
-
hotpot地址偏移x60然后跳轉。x60就是test2
image.png
可以看到test、test1、test2、test3是連續的地址,這里也就驗證了class中方法是連續存放的。
源碼觀察
搜索initClassVTable
并打個斷點
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(&classWords[vtableOffset + i],
methodDescription.Impl.get(),
methodDescription.Flags.getExtraDiscriminator());
}
}
從源碼中可以看到直接是一個for循環把V-Table寫到內存中。大小為vtable->VTableSize
,偏移量是vtableOffset
。從description->getMethodDescriptors()
獲取放到classWords
中。
extension中方法調用
extension HotpotCat {
func test4() {
print("test4")
}
}
可以看出變成了直接調用。
why?
class HotpotCat {
func test() {
print("test")
}
func test1() {
print("test1")
}
func test2() {
print("test2")
}
func test3() {
print("test3")
}
}
extension HotpotCat {
func test4() {
print("test4")
}
}
class HotpotCatChild: HotpotCat {
func test5() {
print("test5")
}
}
HotpotCatChild繼承自HotpotCat 我們直接看一下SIL文件.
可以看到HotpotCatChild繼承了HotpotCat所有的方法,并且在父類方法后插入了自己的
test5
。
假如extension也放在V-Table中,那么extension有可能放在任意一個文件中,那編譯的時候只有編譯到的時候才知道有extension。此時擴展中的方法可以插入到父類,但無法插入到子類V-Table中。因為沒有指針記錄哪塊是父類方法,哪塊是子類方法,并且繼承層級可能很多,加指針記錄也不現實。(OC是通過移動內存地址合成的)。
OC方法列表合成
這也就是OC category方法會"覆蓋"同名方法的原因。
- V-Table是一個數組的結構
- extension中方法變成了靜態調用
靜態調用與V-Table區別
- 靜態調用直接調用地址。
- V-Table需要先找到基地址,然后基地址根據偏移量找到函數地址。
調用速度:靜態調用>V-Table>動態調用
SIL
使用 class_method, super_method, objc_method, 和 objc_super_method 操作來實現類方法的動態分派.
還有OverrideTable、PWT(Protocol Witness Table)、VWT(Value Witness Table)。VWT和PWT通過分工,去管理Protocol Type實例的內存管理(初始化,拷貝,銷毀)和方法調用。
final
final修飾方法就變成直接調用了。
- final 修飾類(只能修飾類),這個類就不能被繼承;
- final 修飾方法,方法不能被重寫(override);
- static 和 final 不能一起使用, static修飾的就已經是final了
@objc
標記給OC調用,并沒有修改調度方式。
由于Swift是靜態語言,OC是動態語言。在混合項目中為了保證安全將swift中可能被OC調用的方法加上@objc標記。
- @objc標記
- 繼承自NSObject。(swift3的時候編譯器默認給繼承自NSObject的類所有方法添加@objc,swift4之后只會給實現OC接口和重寫OC方法的函數添加)
這樣就可以優化刪除沒有用到的swift函數,減少包大小。
https://www.hangge.com/blog/cache/detail_1839.html
編譯器會自己生成轉換文件
SWIFT_CLASS("_TtC7SwiftOC9HotpotCat")
@interface HotpotCat : NSObject
- (void)test1;
- (nonnull instancetype)init OBJC_DESIGNATED_INITIALIZER;
@end
class HotpotCat {
@objc func test(){
print("test")
}
}
var hotpot = HotpotCat()
hotpot.test()
SIL文件查看一下這個過程究竟做了什么處理
@objc標記的方法生成了兩個方法,一個是swift原有的方法,另一個是生成供oc調用的方法,oc調用的這個方法內部調用了swift的方法
更詳細資料:https://blog.csdn.net/weixin_33816300/article/details/91370145
dynamic
class HotpotCat {
dynamic func test(){
print("test")
}
}
var hotpot = HotpotCat()
hotpot.test()
可以看到依然是函數表調度。
- dynamic不改變調度方式
- dynamic使屬性啟用Objc的動態轉發功能;
- dynamic只用于類,不能用于結構體和枚舉,因為它們沒有繼承機制,而Objc的動態轉發就是根據繼承關系來實現轉發。
- Swift 中我們也是可以使用 KVO 的,但是僅限于在 NSObject 的子類中。這是可以理解的,因為 KVO 是基于 KVC (Key-Value Coding) 以及動態派發技術實現的,而這些東西都是 Objective-C 運行時的概念。另外由于 Swift 為了效率,默認禁用了動態派發,因此想用 Swift 來實現 KVO,我們還需要做額外的工作,那就是將想要觀測的對象標記為 dynamic。
- OC中@dynamic關鍵字告訴編譯器不要為屬性合成getter和setter方法。
- Swift中dynamic關鍵字,可以用于修飾變量或函數,意思與OC完全不同。它告訴編譯器使用動態分發而不是靜態分發。
- 標記為dynamic的變量/函數會隱式的加上@objc關鍵字,它會使用OC的runtime機制。
https://www.cnblogs.com/feng9exe/p/9084788.html
http://www.lxweimin.com/p/49b8e6f6a51d
Swift替換
class HotpotCat {
dynamic func test(){
print("test")
}
}
extension HotpotCat {
@_dynamicReplacement(for: test)
func test1(){
print("test1")
}
}
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
var hotpot = HotpotCat()
hotpot.test()
}
}
lldb
test1
這里調用test實際上調用到了test1。
既然可以替換方法,那么能不能替換屬性呢?
首先要清楚extension
中不能添加存儲屬性。只能添加計算屬性。而要更改屬性的行為也就是更改set
和get
方法就好了。所以這接給擴展添加一個存儲屬性并綁定到age
上:
class HotpotCat {
dynamic var age: Int = 18
dynamic func test() {
print("test")
}
}
extension HotpotCat {
@_dynamicReplacement(for: age)
var age2: Int {
set {
age = newValue
}
get {
10
}
}
@_dynamicReplacement(for: test)
func test1() {
print("test1")
}
}
var hotpot = HotpotCat()
hotpot.test()
print(hotpot.age)
輸出
test1
10
(lldb)
@objc+dynamic
class HotpotCat {
@objc dynamic func test(){
print("test")
}
}
可以看到調度方式已經變成了動態轉發。
指針
Swift
中指針分為raw pointer
未指定數據類型指針和typed pointer
指定數據類型指針。
-
raw pointer
表示UnsafeRawPointer
-
typed pointer
表示UnsafePointer<T>
Swift指針與OC對應關系:
Swift | Objective-c | 說明 |
---|---|---|
unsafeRawPointer | const void * | 指針及指向的內存內容(未知)均不可變 |
unsafeMutableRawPointer | void * | 指針指向未知內容 |
unsafePointer<T> | const T * | 指針及所指內容都不可變 |
unsafeMutablePointer<T> | T * | 指針及所指向的內存內容均可變 |
raw pointer(原始指針)
假如我們想在內存中存儲連續4個整形的數據,用raw pointer
來處理
//手動管理內存,allocate要與deallocate匹配
let p = UnsafeMutableRawPointer.allocate(byteCount: 32, alignment: 8)
for i in 0 ..< 4 {
//advanced 步長,相當于p前進 i*8 存儲 i + 1
p.advanced(by: i * 8).storeBytes(of: i + 1, as: Int.self)
}
for i in 0 ..< 4 {
let value = p.load(fromByteOffset: i * 8, as: Int.self)
print("index:\(i),value:\(value)")
}
p.deallocate()
UnsafeMutableRawPointer
源碼
Builtin
Swift標準模塊,匹配LLVM
的類型和方法減少swift運行時負擔。
typed pointer(指定數據類型指針)
typed pointer
簡單用法
方式一
var age = 18
//p的值由閉包表達式返回值決定
let p = withUnsafePointer(to: &age) {$0}
//pointee 相當于 *p = age
print(p.pointee)
//UnsafePointer中不能修改pointee
age = withUnsafePointer(to: &age) { p in
p.pointee + 10
}
print(age)
//UnsafeMutablePointer中可以修改pointee
withUnsafeMutablePointer(to: &age) { p in
p.pointee += 10
}
print(age)
方式二
//容量為1也就意味著8字節
let ptr = UnsafeMutablePointer<Int>.allocate(capacity: 1)
//initialize 與 deinitialize 對應,allocate 與 deallocate 對應
ptr.initialize(to: age)
ptr.deinitialize(count: 1)
ptr.pointee += 10
print(ptr.pointee)
ptr.deallocate()
取值或者創建兩種方式都可以,只不過第一種方式age
的值被改變了,第二種方式age
值并沒有被改變。
指針創建結構體
struct HPTeacher {
var age = 10
var height = 1.85
}
//指針創建HPTeacher
let ptr = UnsafeMutablePointer<HPTeacher>.allocate(capacity: 2)
ptr.initialize(to: HPTeacher())
//這里advanced為1是因為這里ptr是type pointer。知道類型,不需要我們傳大小了。只需要傳步數就好了。
ptr.advanced(by: 1).initialize(to: HPTeacher(age: 18, height: 1.80))
print(ptr[0])
print(ptr[1])
print(ptr.pointee)
print((ptr + 1).pointee)
//successor 本質目的往前移動等效(ptr + 1).pointee
print(ptr.successor().pointee)
ptr.deinitialize(count: 2)
ptr.deallocate()
- 對指針的內存管理需要我們手動管理,
allocate
與deallocate
匹配,initialize
與deinitialize
匹配。
應用
HeapObject(實例對象)結構
struct HeapObject {
var kind: UnsafeRawPointer
var strongRef: UInt32
var unownedRef: uint32
}
class HPTeacher {
var age = 18
}
//實例變量內存地址
var t = HPTeacher()
//let ptr1 = withUnsafePointer(to: &t){$0}
//print(t)//SwiftPointer.HPteacher
//print(ptr1)//0x00000001000081e0
//print(ptr.pointee)//SwiftPointer.HPteacher
//這里拿到的就是實例對象的值。因為Raw pointer 和 type pointer都需要我們自己管理內存。 Unmanaged可以托管指針。
//passUnretained 這里有點類似OC和C的交互 __bridge 所有權的轉換。
//toOpaque返回不透明的指針UnsafeMutableRawPointer
let ptr = Unmanaged.passUnretained(t as AnyObject).toOpaque()
//Raw pointer 重新綁定為 HeapObject 的 UnsafeMutablePointer
//bindMemory更改ptr的指針類型綁定到HeapObject具體的內存指針。如果ptr沒有綁定則首次綁定到HeapObject,如果綁定過了會重新綁定。
let heapObject = ptr.bindMemory(to: HeapObject.self, capacity: 1)
print(heapObject.pointee.kind)
print(heapObject.pointee.strongRef)
print(heapObject.pointee.unownedRef)
這里t
和ptr
的區別:
(lldb) po withUnsafePointer(to: &t){print($0)}
0x00000001000081e0
0 elements
(lldb) po ptr
? 0x0000000101817860
- pointerValue : 4320229472
(lldb) x/8g 0x00000001000081e0
0x1000081e0: 0x0000000101817860 0x0000000101817860
0x1000081f0: 0x0000000101817860 0x0000000000000000
0x100008200: 0x0000000000000000 0x0000000000000000
0x100008210: 0x0000000000000000 0x0000000000000000
(lldb)
相當于ptr
指向t
的內存地址的metadata
地址。
類結構
struct HeapObject {
var kind: UnsafeRawPointer
var strongRef: UInt32
var unownedRef: uint32
}
struct hp_swift_class {
var kind: UnsafeRawPointer
var superClass:UnsafeRawPointer
var cacheData1:UnsafeRawPointer
var cacheData2:UnsafeRawPointer
var data:UnsafeRawPointer
var flags:UInt32
var instanceAddressOffset:UInt32
var instanceSize:UInt32
var instanceAlignMask:UInt16
var reserved:UInt16
var classSize:UInt32
var classAddressOffset:UInt32
var description:UnsafeRawPointer
}
class HPTeacher {
var age = 18
}
var t = HPTeacher()
//獲取HPTeacher的指針
let ptr = Unmanaged.passUnretained(t as AnyObject).toOpaque()
//綁定 ptr 到 HeapObject(實例對象)
let heapObject = ptr.bindMemory(to: HeapObject.self, capacity: 1)
//綁定 heapObject 的 kind(metadata) 到 hp_swift_class(類)。這里本質上內存結構一致,所以能正確拿到數據。
let metaPtr = heapObject.pointee.kind.bindMemory(to: hp_swift_class.self, capacity: 1)
print(metaPtr.pointee)
可以看到打印信息如下:
hp_swift_class(kind: 0x0000000100008140, superClass: 0x00007fff88a6f6f8, cacheData1: 0x00007fff201d3af0, cacheData2: 0x0000802000000000, data: 0x000000010054b672, flags: 2, instanceAddressOffset: 0, instanceSize: 24, instanceAlignMask: 7, reserved: 0, classSize: 136, classAddressOffset: 16, description: 0x0000000100003c3c)
指針轉換
元組指針轉換
var tul = (10,20)
func testPointer(_ p : UnsafePointer<Int>) {
print(p.pointee)
print(p[1])
}
withUnsafePointer(to: &tul) { (tulPtr : UnsafePointer<(Int,Int)>) in
//assumingMemoryBound 假定內存綁定,告訴編譯器 tulPtr 已經綁定過 Int.self 類型了,不需要再編譯檢查了。
testPointer(UnsafeRawPointer(tulPtr).assumingMemoryBound(to: Int.self))
}
10
20
(lldb)
這里能夠綁定成功是因為元組在內存中也是連續存放數據的,這里的<Int,Int>元組實際上在內存上就是連續8字節存儲的。應用場景就是某些接口不兼容的時候需要轉換。
結構體指針轉換
原則是要把第一個int屬性的地址傳過去。
struct HeapObject {
var strongRef: Int
var unownedRef: Int
}
func testPointer(_ p : UnsafePointer<Int>) {
print(p.pointee)
}
var hp = HeapObject(strongRef: 10, unownedRef: 20)
withUnsafeMutablePointer(to: &hp) { (ptr : UnsafeMutablePointer<HeapObject>) in
//1.這里通過取strongRef的地址,其實ptr和strongRefPtr是同一個。需要注意的一點是外層要改為withUnsafeMutablePointer,否則無法對 ptr.pointee.strongRef &
// let strongRefPtr = withUnsafePointer(to: &ptr.pointee.strongRef){$0}
// testPointer(strongRefPtr)
//2.通過 advance 步長 advance 0,由于 ptr 步長 1 的話就跳過 hp 了,需要轉換為 UnsafeRawPointer 然后 advanced
// let strongRefPtr = UnsafeRawPointer(ptr).advanced(by: 0)
// testPointer(strongRefPtr.assumingMemoryBound(to: Int.self))
//3. ptr 的地址 + 偏移量,這里可以直接傳ptr。因為HeapObject結構體地址首個屬性就是strongRef
//offset 的參數是keypath,如果path改為 unownedRef,那輸出的就是unownedRef的值。
// let strongRefPtr = UnsafeRawPointer(ptr) + MemoryLayout<HeapObject>.offset(of: \HeapObject.strongRef)!
// testPointer(strongRefPtr.assumingMemoryBound(to: Int.self))//在這里等效4
//4.直接傳ptr地址
testPointer(UnsafeRawPointer(ptr).assumingMemoryBound(to: Int.self))
}
withMemoryRebound
withMemoryRebound
用于臨時更改指針類型,用于我們不希望修改指針類型的情況下。
var age = 10
func testPointer(_ value: UnsafePointer<UInt64>){
print(value.pointee)
}
let ptr = withUnsafePointer(to: &age){$0}
//臨時修改ptr類型,只在閉包表達式是UnsafePointer<UInt64>,出了作用域仍然為UnsafePointer<Int>。
ptr.withMemoryRebound(to: UInt64.self, capacity: 1) { (ptr: UnsafePointer<UInt64>) in
testPointer(ptr)
}
臨時修改ptr類型,只在閉包表達式是UnsafePointer<UInt64>,出了作用域仍然為UnsafePointer<Int>。
-
withMemoryRebound
:臨時更改內存綁定類型。 -
bindMemory<T>(to type: T.Type, capacity count: Int)
:更改內存綁定的類型,如果之前沒有綁定,那么就是首次綁定;如果綁定過了,會被重新綁定為該類型。 -
assumingMemoryBound
:假定內存綁定,這里是告訴編譯器已經綁定過類型了,不需要再編譯檢查了。
內存操作都是不安全的,需要我們自己管理負責。