Swift底层探索(三):指针

iOS内存分区

Swift底层探索(三):指针_第1张图片
image.png

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


(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

Swift底层探索(三):指针_第2张图片
image.png

(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个变量的地址,可以看到ageage2离额比较近。这也就验证了全局区分开存储了已初始化和未初始化比变量。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这个值。

Swift底层探索(三):指针_第3张图片
image.png

那么去掉 static

(lldb) po &age3
0x0000000100003fa0

(lldb) cat address 0x0000000100003fa0
&0x0000000100003fa0,  age3 <+0> CTest.__TEXT.__const

发现age3__TEXT.__const

Swift底层探索(三):指针_第4张图片
image.png

再验证下 *p
Swift底层探索(三):指针_第5张图片
image.png

__text

就是我们当前执行的指令。

swift中,调试的时候全局变量初始化前在__common段,初始化后还是在__common段,按理说应该在__data段的。

总结:分区(人为的笼统的划分)和 macho文件不是一个东西(放在segmentsection中,符号划分更细)。可以简单理解为WindowsMacOS

方法调度

结构体 静态调用(直接调用)。在编译完成后方法的地址就确定了,在执行代码的过程中就直接跳转到地址执行方法。对于类中的方法存储在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: 跳转到某地址

直接断点调试


Swift底层探索(三):指针_第6张图片
image.png

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) 
Swift底层探索(三):指针_第7张图片
image.png
  • hotpot地址偏移x60然后跳转。x60就是test2


    Swift底层探索(三):指针_第8张图片
    image.png

    可以看到test、test1、test2、test3是连续的地址,这里也就验证了class中方法是连续存放的。

源码观察

搜索initClassVTable并打个断点

Swift底层探索(三):指针_第9张图片
image.png

  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")
    }
}

Swift底层探索(三):指针_第10张图片
image.png

可以看出变成了直接调用。
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文件.

Swift底层探索(三):指针_第11张图片
image.png

可以看到HotpotCatChild继承了HotpotCat所有的方法,并且在父类方法后插入了自己的 test5

假如extension也放在V-Table中,那么extension有可能放在任意一个文件中,那编译的时候只有编译到的时候才知道有extension。此时扩展中的方法可以插入到父类,但无法插入到子类V-Table中。因为没有指针记录哪块是父类方法,哪块是子类方法,并且继承层级可能很多,加指针记录也不现实。(OC是通过移动内存地址合成的)。

OC方法列表合成

这也就是OC category方法会"覆盖"同名方法的原因。


Swift底层探索(三):指针_第12张图片
image.png
  • 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底层探索(三):指针_第13张图片
image.png
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文件查看一下这个过程究竟做了什么处理


Swift底层探索(三):指针_第14张图片
image.png

@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()
Swift底层探索(三):指针_第15张图片
image.png

可以看到依然是函数表调度。

  • 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
    https://www.jianshu.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中不能添加存储属性。只能添加计算属性。而要更改属性的行为也就是更改setget方法就好了。所以这接给扩展添加一个存储属性并绑定到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底层探索(三):指针_第16张图片
image.png

可以看到调度方式已经变成了动态转发。

指针

Swift中指针分为raw pointer 未指定数据类型指针和typed pointer指定数据类型指针。

  • raw pointer表示UnsafeRawPointer
  • typed pointer表示UnsafePointer

Swift指针与OC对应关系:

Swift Objective-c 说明
unsafeRawPointer const void * 指针及指向的内存内容(未知)均不可变
unsafeMutableRawPointer void * 指针指向未知内容
unsafePointer const T * 指针及所指内容都不可变
unsafeMutablePointer 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源码

Swift底层探索(三):指针_第17张图片
image.png

BuiltinSwift标准模块,匹配 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.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.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()
  • 对指针的内存管理需要我们手动管理,allocatedeallocate匹配,initializedeinitialize 匹配。

应用

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)

这里tptr的区别:

(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) {
    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) 

这里能够绑定成功是因为元组在内存中也是连续存放数据的,这里的元组实际上在内存上就是连续8字节存储的。应用场景就是某些接口不兼容的时候需要转换。

结构体指针转换

原则是要把第一个int属性的地址传过去。

struct HeapObject {
    var strongRef: Int
    var unownedRef: Int
}

func testPointer(_ p : UnsafePointer) {
    print(p.pointee)
}

var hp = HeapObject(strongRef: 10, unownedRef: 20)

withUnsafeMutablePointer(to: &hp) { (ptr : UnsafeMutablePointer) 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.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){
    print(value.pointee)
}

let ptr = withUnsafePointer(to: &age){$0}
//临时修改ptr类型,只在闭包表达式是UnsafePointer,出了作用域仍然为UnsafePointer。
ptr.withMemoryRebound(to: UInt64.self, capacity: 1) { (ptr: UnsafePointer) in
    testPointer(ptr)
}

临时修改ptr类型,只在闭包表达式是UnsafePointer,出了作用域仍然为UnsafePointer

  • withMemoryRebound:临时更改内存绑定类型。
  • bindMemory(to type: T.Type, capacity count: Int):更改内存绑定的类型,如果之前没有绑定,那么就是首次绑定;如果绑定过了,会被重新绑定为该类型。
  • assumingMemoryBound:假定内存绑定,这里是告诉编译器已经绑定过类型了,不需要再编译检查了。

内存操作都是不安全的,需要我们自己管理负责。

参考:
https://www.jianshu.com/p/358f0cd7d823

你可能感兴趣的:(Swift底层探索(三):指针)