Swift进阶4.指针和内存管理

482 阅读15分钟

指针与内存管理

本文主要介绍指针类型、内存管理、还原Mach-o文件中的函数表。指针类型主要介绍typed pointerraw pointer,并且介绍了三种不同的内存绑定方式。内存管理主要介绍了强引用、弱引用和无主引用。

为什么说指针不安全

  • 比如我们在创建一个对象的时候,是需要在堆分配内存空间的,但是这个空间的声明周期是有限的,也就意味着如果我们使用指针指向这块内存空间,如果当前内存空间的生命周期到了(引用计数为0),那么我们当前的指针就变成了未定义的行为了。
  • 我们创建的内存空间是有边界的,比如我们创建一个大小为10的数组,这个时候我们通过指针访问到了index=11的位置,这个时候就越界了,访问了一个未知的内存空间。
  • 指针类型与内存的值类型不一致,也是不安全的。

一、指针类型

swift中的指针分为两类,

  • typed pointer: 指定数据类型指针,用UnsafeRawPointer来表示

  • raw pointer: 未指定数据类型的指针(原指针),用UnsafePointer<T>来表示,它是泛型,T是要指定的类型

    可以发现swift指针都带有Unsafe,因为swift指针是对内存直接操作,是不安全的

Swift中的指针和OC中的指针的对应关系如下:

SwiftObjective-C说明
unsafePointer< T >const T *指针及所指向的内容都不可变
unsafeMutablePointerT *指针及所指向的内容均可变
unsafeRawPointerconst void *指针指向的内存区域未知
unsafeMutableRawPointervoid *指针指向的内存区域未知
unsafeBufferPointer< T >
unsafeMutableBufferPointer
unsafeRawBufferPointer
unsafeMutableRawBufferPointer

1.1 原指针(raw pointer)的使用

原指针:是指未指定数据类型的指针,有以下说明

  • 对于指针的内存管理是需要手动管理的
  • 指针在使用完需要手动释放

对于原生指针的操作,我们可以通过以下代码来操作raw pointer

// 分配了32字节大小的空间,8字节对齐
var p = UnsafeMutableRawPointer.allocate(byteCount: 32, alignment: 8)

// 存储字节
for i in 0..<4 {
    // 每次移动i*8, 以整型的格式向该内存中存数据。
    p.storeBytes(of: i, as: Int.self)
}

for i in 0..<4 {
    // 每次偏移i*8,以整型结构读取内存值
    let value = p.load(fromByteOffset: i*8, as: Int.self)
    print("index\(i), value\(value)")
}

// 手动管理内存空间,需要手动释放
p.deallocate()

// 打印结果
index0, value3
index1, value0
index2, value4294967312
index3, value4

通过运行发现,在读取数据时有问题,原因是因为读取时指定了每次读取的大小,但是存储是直接在8字节的p中存储了i,即可以理解为并没有指定存储时的内存大小

  • 修改:通过advanced(by:)指定存储时的步长
/存储
for i in 0..<4 {
    //指定当前移动的步数,即i*8
    p.advanced(by: i * MemoryLayout<Int>.stride).storeBytes(of: i, as: Int.self)
}

// 打印结果
index0, value0
index1, value1
index2, value2
index3, value3

总结原指针使用步骤如下:

  • 调用allocate函数创建一个8字节,并遵循8字节对齐的指针pointer
  • pointer指针首地址处开始存储,之后每平移8字节存储一个数
    • advanced(by:)函数是存储位置,如果不设置,就会存在指针的首地址处
  • 通过内存平移读取内存中的内容
  • 调用deallocate函数对指针进行手动销毁

1060

我们看到alignment:8, 8字节对齐,分别存储了0,1,2,3

1.2 范型指针(typed pointer)的使用

下面介绍两种方式来得到范型指针

1.2.1 withUnsafePointer

取基本数据类型的地址是通过withUnsafePointer(to:)方法获取的

  • 下面来查看下withUnsafePointer(to:)的源码:
@inlinable
public func withUnsafePointer<T, Result>(
  to value: T,
  _ body: (UnsafePointer<T>) throws -> Result
) rethrows -> Result
{
  return try body(UnsafePointer<T>(Builtin.addressOfBorrow(value)))
}

@inlinable
public func withUnsafePointer<T, Result>(
  to value: inout T,
  _ body: (UnsafePointer<T>) throws -> Result
) rethrows -> Result
{
  return try body(UnsafePointer<T>(Builtin.addressof(&value)))
}

查看withUnsafePointer(to:)的定义中,

  1. 该方法有两个,后面这个传入的参数value带有inout标记,则传参时需要传入地址

  2. 然后参数body是一个闭包,它参数的类型的指针,且有返回值

  3. 最后就是throws将闭包body的返回值抛出,然后rethrows将返回值抛给withUnsafePointer函数

  • 获取到指针后,可以使用pointer.pointee来打印指针指向的值
var age = 18
let p = withUnsafePointer(to: &age) { $0 }
print(p.pointee)	// 打印结果: 18

pointer.pointee表示:指针指向内存的值

如何改变age变量值?

改变变量值的方式有两种,一种是间接修改,一种是直接修改

  • 间接修改:需要在闭包中直接通过ptr.pointee修改并返回。类似于char *p = “jx” 中的 *p,因为访问jx通过 *p
var age = 18
age = withUnsafePointer(to: &age, { ptr in
    return ptr.pointee + 12
})
print(age) 	// 打印结果: 30
  • 直接修改方式1:也可以通过withUnsafeMutablePointer方法
var age = 18
withUnsafeMutablePointer(to: &age, { ptr in
    ptr.pointee += 12
})
print(age)	// 打印结果: 30
  • 直接修改方式2:通过allocate创建UnsafeMutablePointer,需要注意的是
    • initialize 与 deinitialize是成对的
    • deinitialize中的count与申请时的capacity需要一致
    • 需要deallocate
var age = 10
let ptr = UnsafeMutablePointer<Int>.allocate(capacity: 1)
ptr.initialize(to: age)
ptr.deinitialize(count: 1)

ptr.pointee += 12
print(ptr.pointee)

ptr.deallocate()

1.2.2 withUnsafeMutablePointer

  • 下面我们查看withUnsafeMutablePointer源码
@inlinable
public func withUnsafeMutablePointer<T, Result>(
  to value: inout T,
  _ body: (UnsafeMutablePointer<T>) throws -> Result
) rethrows -> Result
{
  return try body(UnsafeMutablePointer<T>(Builtin.addressof(&value)))
}

查看withUnsafeMutablePointer(to:)的定义中,

  1. 该方法传入的参数value带有inout标记,则传参时需要传入地址
  2. 然后参数body是一个闭包,它参数的类型的指针,且有返回值
  3. 最后就是throws将闭包body的返回值抛出,然后rethrows将返回值抛给withUnsafePointer函数

实战:访问结构体实例对象

定义一个结构体

struct Person {
    var age = 18
    var height = 185
}
  • 使用UnsafeMutablePointer创建指针,并通过指针访问Person实例对象,有以下三种方式:

    • 方式一:下标访问

    • 方式二:内存平移

    • 方式三:successor

//分配两个Person大小的空间
let ptr = UnsafeMutablePointer<Person>.allocate(capacity: 2)
//初始化第一个空间
ptr.initialize(to: Person())
//移动,初始化第2个空间
ptr.successor().initialize(to: Person(age: 20, height: 175))

//访问方式一
print(ptr[0])   // Person(age: 18, height: 185)
print(ptr[1])   // Person(age: 20, height: 175)
//访问方式二
print(ptr.pointee)
print((ptr+1).pointee)
//访问方式三
print(ptr.pointee)
//successor 往前移动
print(ptr.successor().pointee)

//必须和分配是一致的
ptr.deinitialize(count: 2)
//释放
ptr.deallocate()
  • 代码中主要通过指针内存平移来初始化两个Person实例

  • (ptr+1)是从ptr首地址开始向下平移Person内存

  • 访问Person的实例可以用以下几种方法:

    • ptr[0], ptr[1]

    • ptr.pointee, (ptr+1).pointee

    • (ptr + 1).predecessor().pointee, ptr.successor().pointee

predecessor是上一个连续的Person大小的指针,successor是下一个连续的Person大小的指针,与内存平移道理一样
  • 可以通过ptr + 1或者successor() 或者advanced(by: 1)
ptr.successor().initialize(to: Person(age: 20, height: 175))
(ptr+1).initialize(to: Person(age: 20, height: 175))
ptr.advanced(by: 1).initialize(to: Person(age: 20, height: 175))
  • 需要注意的是,第二个空间的初始化不能通过advanced(by: MemoryLayout.stride)去访问,否则取出结果是有问题
//移动,初始化第2个空间
//ptr.successor().initialize(to: Person(age: 20, height: 175))
ptr.advanced(by: MemoryLayout<Person>.stride).initialize(to: Person(age: 20, height: 175))

// 打印结果:
Person(age: 8652652556, height: 8320505416)

1.3 内存指针的使用

1061

1.3 内存绑定

swift提供了三种不同的API来绑定/重新绑定指针

1.3.1 assumingMemoryBound(to: )

有些时候我们处理代码的过程中,只有原始指针(没有保留指针类型), 但我们知道指针的类型,可以使用assumingMemoryBound(to:)来告诉编译器预期的类型

1062

1063

注意“ 这里只是让编译器绕过类型检查,并没有发生实际的类型转换

1.3.2 bindMemory(to: capacity: )

用于更改内存绑定的类型,如果当前内存还没有绑定类型,则将首次绑定为该类型;否则重新绑定该类型,并且内存中所有的值都会变成该类型。

func testPoint(_ p: UnsafePointer<Int>) {
    print(p[0])
    print(p[1])
}

let tuple = (10, 20)
withUnsafePointer(to: tuple) { (tuplePtr: UnsafePointer<(Int, Int)>) in
    testPoint(UnsafeRawPointer(tuplePtr).bindMemory(to: Int.self, capacity: 1))
}

1.3.3 withMemoryRebound(to: capacity: body:)

当我们在给外部函数传递参数时,不免会有一些数据类型的差距。如果我们进行类型转换,必然要来回复制数据,这个时候我们就可以使用withMemoryRebound(to: capacity: body:)来临时更改内存绑定类型。

如果方法的类型与传入参数的类型不一致,会报错。解决办法:通过withMemoryRebound临时绑定内存类型

func testPoint(_ p: UnsafePointer<Int8>) {
    print(p)
}

let Uint8Ptr = UnsafePointer<UInt8>.init(bitPattern: 10)
Uint8Ptr?.withMemoryRebound(to: Int8.self, capacity: 1) { (int8Ptr: UnsafePointer<Int8>) in
    testPoint(int8Ptr)
}

1.4 总结

  • 指针类型分两种
    • typed pointer 指定数据类型指针,即 UnsafePointer + unsafeMutablePointer
    • raw pointer 未指定数据类型的指针(原生指针) ,即UnsafeRawPointer + unsafeMutableRawPointer
  • withMemoryRebound: 临时更改内存绑定类型
  • bindMemory(to: Capacity:): 更改内存绑定的类型,如果之前没有绑定,那么就是首次绑定,如果绑定过了,会被重新绑定为该类型
  • assumingMemoryBound假定内存绑定,这里就是告诉编译器:我的类型就是这个,你不要检查我了,其实际类型还是原来的类型

二、内存管理

这里主要介绍swift中的内存管理,涉及引用计数、弱引用、强引用、循环引用等

Swift中使用自动引用计数来追踪和管理内存。

2.1 强引用

1064

查看p的内存情况,为什么其中的refCounts是0x0000000000000003?

  • 首先我们先来找到当前的refCounts的定义

1065

  • 搜索InlineRefCounts

1066

进入InlineRefCounts定义,是RefCounts类型的别名,InlineRefCounts定义,是RefCounts类型的别名

1067

而RefCounts是模板类,真正决定的是传入的类型InlineRefCountBits

typedef RefCountBitsT<RefCountIsInline> InlineRefCountBits;
  • 进入RefCountBitsT

1068

我们看到有bits属性

1069

其中bits其实质是将RefCountBitsInt中的type属性取了一个别名,所以bits的真正类型是uint64_t即64位整型数组

这我们任然不知道是如何设置的,我们先来看一下,当我们创建一个实例的时候,当前的引用计数是多少。

swift中对象创建的底层方法swift_allocObject

  • 分析初始化源码swift_allocObject

1070

1071

  • 进入Initialized定义,是一个枚举,其对应的refCounts方法中,

    1072

从这里看出真正干事的是RefCountBits

1073

所以真正的初始化地方是上面这个,实际上是做了一个位域操作,根据的是Offsets

分析RefCountsBit的结构,如下所示

1106

重点关注UnownedRefCount和StrongExtraRefCount

	# define maskForField(name) (((uint64_t(1)<<name##BitCount)-1) << name##Shift)
	# define shiftAfterField(name) (name##Shift + name##BitCount)
  
    static const size_t StrongExtraRefCountShift = shiftAfterField(IsDeiniting)
    = IsDeinitingShift + IsDeinitingBitCount 
    = shiftAfterField(UnownedRefCount) + 1 
    = UnownedRefCountShift + UnownedRefCountBitCount + 1
    = shiftAfterField(PureSwiftDealloc) + 31 + 1
    = PureSwiftDeallocShift + PureSwiftDeallocBitCount + 32
    = 0 + 1 + 32
    = 33
    
  	static const size_t PureSwiftDeallocShift = 0;
  	
    static const size_t UnownedRefCountShift = shiftAfterField(PureSwiftDealloc);
    = PureSwiftDeallocShift + PureSwiftDeallocBitCount
    = 0 + 1
    = 1
    
    RefCountBitsT(uint32_t strongExtraCount, uint32_t unownedCount)
    : bits((BitsType(strongExtraCount) << Offsets::StrongExtraRefCountShift) |
           (BitsType(1)                << Offsets::PureSwiftDeallocShift) |
           (BitsType(unownedCount)     << Offsets::UnownedRefCountShift)) { }
           
    strongExtraCount = 0
  	unownedCount = 1
    0 << 33 | 1 << 0 | 1 << 1 = 0x0000000000000003
    
    strongExtraCount = 1
    1 << 33 | 1 << 0 | 1 << 1 = 0x0000000200000003

1074

分析SIL代码

1075

SIL官方文档中关于copy_addr的解释如下

1107

1076

  • 其中的strong_retain对应的就是swift_retain,其内部是一个宏定义,内部是_swift_retain_,其实现是对object的引用计数作+1操作

这里我们直接去源码搜索一下swift_retain:

1108

1109

  • 本质上是_swift_retain_

1077

  • 进入increment

1078

  • 进入incrementStrongExtraRefCount

1079

回到实例

1080

为什么是0x200000000? 因为1左移33位,其中4位为一组,计算成16进制,剩余的33-32位0x10,转换为10进制为2。其实际增加引用计数就是1

问题

这里我们就了解了强引用,使用强引用就会造成一个问题:循环引用。我们来看一个经典的循环引用案例:

class Teacher {
    var age: Int = 18
    var name: String = "tony"
    var subject: Subject?
}

class Subject {
    var subjectName: String
    var subjectTeacher: Teacher
    
    init(_ subjectName: String, _ subjectTeacher: Teacher) {
        self.subjectName = subjectName
        self.subjectTeacher = subjectTeacher
    }
}
var t = Teacher()
var subject = Subject.init("swift", t)
t.subject = subject

上面做这段代码是不是就产生了两个实例对象之前的强引用,swift提供了两个办法用来解决你在使用类的属性时所遇到的循环强引用问题:弱引用(weak reference)和无主引用(unowed reference)。

2.2 弱引用

弱引用不会对其引用的实例保持强引用,因而不会阻止ARC释放被引用的实例。这个特征阻止了引用变为循环强引用。声明属性或者变量时,在前面加上weak关键字表明这是一个弱引用。

由于弱引用不会强保持对实例的引用,所以说实例被释放了弱引用仍旧引用着这个实例也是有可能的。因此,ARC会在被引用的实例被释放是自动地设置引用位nil。由于弱引用需要允许它们的值为nil,它们一定得是可选类型。

1081

你可以检查弱引用的值是否存在,就像其他可选项的值一样,并且你将永远不会遇到“野指针"

我们断点看一下,这里调用了什么函数

  • 在t处加断点,查看汇编

1082

  • 查看 swift_weakInit函数,这个函数是由WeakReference来调用的,相当于weak字段在编译器声明过程中就自定义了一个WeakReference的对象,其目的在于管理弱引用

1083

  • 进入nativeInit1087

    • 进入formWeakReference

    1085

allocateSideTable创建sideTable, 如果创建成功,则增加弱引用incrementWeak

  • allocateSideTable

1086

  1. 先拿到原本的引用计数
  2. 创建sideTable
  3. 将创建的sideTable地址给InlineRefCountBits,并查看其初始化方法,根据sideTable地址作了偏移操作并存储到内存,相当于将sideTable直接存储到了64位的变量中

1088

	# define maskForField(name) (((uint64_t(1)<<name##BitCount)-1) << name##Shift)
	# define shiftAfterField(name) (name##Shift + name##BitCount)
  
   static const size_t UseSlowRCShift = shiftAfterField(StrongExtraRefCount);
   = shiftAfterField(IsDeiniting) + StrongExtraRefCountBitCount
   = shiftAfterField(UnownedRefCount) + IsDeinitingBitCount + 30
   = shiftAfterField(PureSwiftDealloc) + UnownedRefCountBitCount + 1 + 30
   = PureSwiftDeallocShift + PureSwiftDeallocBitCount + 31 + 31
   = 0 + 1 + 32
   = 63
   
   static const size_t SideTableMarkShift = SideTableBitCount;
   = 62

可以看到这里的本质也是用64位的指针,把当前的side位位域中,并设置一些标记位。(62、63位是1)

  • 进入HeapObjectSideTableEntry

1089

  • 搜索SideTableRefCounts
typedef RefCounts<SideTableRefCountBits> SideTableRefCounts;
  • 搜索SideTableRefCountBits

1090

Side Table是一种类名为HeapObjectSideTableEntry的结构,里面也有RefCounts成员,实际做事的是SideTableRefCountBits,存的是原来的uint64_t加上一个存储弱引用数的uint32_t

  • 64位uint64_t用于记录原有引用计数
  • 32位uint32_t用于记录弱引用计数

回到实例

1091

我们把这个地址放到计算器中看一下

1092

以0xc000000020c20812为例,将62、63位清零,变成0x20c20812, 同时左移3位(即InlineRefCountBits初始化方法),拿到当前的地址0x106104090(即HeapObjectSideTableEntry对象地址, 即散列表地址)

1093

强引用计数为3,弱引用计数为2

那如果此时我们在来增加一个强引用会发生什么?

1094

  • 进入incrementSlow

1095

  • 进入incrementStrong

1096

这里我们来总结一下我们当前的引用计数,一个对象在初始化的时候后是没有SideTable的,当我们创建一个弱引用的时候,系统会创建一个Side Table

所有对于一个HeapObject来说他有两种引用计数的布局方式。

  HeapObject {
    isa
    InlineRefCounts {
      atomic<InlineRefCountBits> {
        strong RC + unowned RC + flags
        OR
        HeapObjectSideTableEntry*
      }
    }
  }

  HeapObjectSideTableEntry {
    SideTableRefCounts {
      object pointer
      atomic<SideTableRefCountBits> {
        strong RC + unowned RC + weak RC + flags
      }
    }   
  }

其中InlineRefCountsSideTableRefCounts共享当前模版类RefCounts<T>.的实现

InlineRefCountBitsSideTableRefCountBits共享当前模版类RefCountsBits<bool>

其实刚才我们也可以看到当前Weak修饰的变量是一个可选值,ARC会在被引用的实例被释放是自动地设置弱引用为nil。你可以检查弱引用的值是否存在,就像其他可选项的值一样,并且你将永远不会遇到“野指针”。

1097

上面我们讲了两种RefCounts,一种是inline,用在HeapObject中,它其实是一个uint64_t,可以当引用计数也可以当Side Table的指针。

Side Table是一种类名为HeapObjectSideTableEntry的结构,里面也有RefCounts成员,其内部是SideTableRefCountBits,其实就是原来的uint64_t加上一个存储弱引用数的uint32_t

2.3 Unowned

和弱引用类似,无主引用不会牢牢保持住引用的实例。但是不像弱引用,总之,无主引用假定是永远有值的。

1098

根据苹果的官方文档的建议。当我们知道两个对象的生命周期并不相关,那么我们必须使用weak。相反,非强引用对象拥有和强引用对象同样或者更长的生命周期的话,则应该使用unowned

class Teacher {
    var age: Int = 18
    var name: String = "tony"
    var subject: Subject?
}

// 如果老师不在了,那么在这个过程中所讲授的课程也就不存在了
// 也就意味着当前的声明周期Teacher更长,所以给它加上unowned
class Subject {
    var subjectName: String
    unowned var subjectTeacher: Teacher

    init(_ subjectName: String, _ subjectTeacher: Teacher) {
        self.subjectName = subjectName
        self.subjectTeacher = subjectTeacher
    }
}
var t = Teacher()
var subject = Subject.init("swift", t)
t.subject = subject

Weak VC Unowned

  • 如果两个对象的生命周期完全和对方没有关系(其中一方什么时候赋值为nil,对对方都没有影响),请用Weak
  • 如果你的代码能确保:其中一个对象销毁,另一个对象也要跟着销毁,这时候可以(谨慎)用Unowned

2.4 闭包循环引用

首先我们的闭包会一般默认捕获我们外部的变量

var age = 18

let closure = {
    age += 1
}
closure()
print(age)	// 19

从打印结果可以看出来:闭包内部对变量的修改将会改变外部原始变量的值

那同时就会有一个问题,如果我们在class的内部定义一个闭包,当前闭包访问属性的过程中,就会对我们当前的实例对象进行捕获:

1099

1100

1101

运行结果发现,闭包对t并没有强引用

1102

从运行结果发现,没有执行deinit方法,即没有打印Teacher deinit,所以这里有循环引用

1103

那么这里我们如何解决这里的循环引用了

循环引用解决方法

有两种方式可以解决swift中的循环引用

  • 使用weak修饰闭包传入的参数,其中参数的类型是optional
func test() {
    let t = Teacher()
    
    t.complateCallBack = { [weak t] in
        t?.age += 1
    }
    
    print("end")
}
test()
  • 使用unowned修饰闭包参数,与weak的区别在于unowned不允许被设置为nil,即总是假定有值的
func test() {
    let t = Teacher()
    
    t.complateCallBack = { [unowned t] in
        t.age += 1
    }
    
    print("end")
}
test()

什么是捕获列表

默认情况下,闭包表达式从其周围的范围捕获常量和变量,并强引用这些值。您可以使用捕获列表来显示控制如何在闭包中捕获值。

在参数列表之前,捕获列表被写为用逗号括起来的表达式列表,并用方括号括起来。如果使用捕获列表,则即使省略参数名称,参数类型和返回类型,也必须使用in关键词。

1104

所以从结果中可以得出:对于捕获列表中的每个常量,闭包会利用周围范围内具有相同名称的常量/变量,来初始化捕获列表中定义的常量。有以下几点说明:

  • 捕获列表中的常量是值拷贝,而不是引用
  • 捕获列表中的常量的相当于复制了变量age的值
  • 捕获列表中的常量是只读的,即不可修改

创建闭包时,内部作用域中的age会用外部作用域中age的值进行初始化,但它们的值未以任何特殊方式连接。这意味着更改外部作用域中的a的值不会影响内部作用域中age的值,也不会更改封闭内部的值,也不会更改封闭外部的值。相比之下,只有一个名为height的变量,外部作用域中的height,在闭包内部或者外部进行的更改在两个地方均可见。

三、还原Mach-o文件中的函数表

class Person  {
   var age:Int = 18
    
    init() {
        self.speak()
        print("init")
    }
   
   func speak() {
       print("speak")
   }

   func run() {
       print("run")
   }
}

class Student : NSObject {
    @objc func fire() {
        let person = Person()
        person.speak()
    }
}

class Teacher: Person {
    override func run() {
        print("Teacher run")
    }
    
    func teach() {
        print("teach")
    }
}
  • 获取__swift5_types 数据
var size: UInt = 0
var ptr = getsectdata("__TEXT", "__swift5_types", &size)
print(ptr) 
  • 当前运行APP的起始地址
var mhHeaderPtr = _dyld_get_image_header(0)
let mhHeaderPtr_IntRepresentation = UInt64(bitPattern: Int64(Int(bitPattern: mhHeaderPtr)))
  • 计算linkBase
var setCommonadPtr = getsegbyname("__LINKEDIT")
var linkBaseAddress: UInt64 = 0

// VM Address: 段的虚拟内存地址,在内存中的位置
// VM Size: 段的虚拟内存大小,占用多少内存
// File Offset: 段在文件中的偏移量
// File Size: 段在文件中的大小
// 计算linkBase =  VM Address - File Offset
if let vmaddr = setCommonadPtr?.pointee.vmaddr, let fileOff = setCommonadPtr?.pointee.fileoff {
    linkBaseAddress = vmaddr - fileOff
}
  • TargetClassDescriptor

    struct SwiftType {
        var flag: UInt32
        var parent: UInt32
    };
    
    struct TargetClassDescriptor {
        var flags: UInt32
        var parent: UInt32
        var name: Int32
        var accessFunctionPointer: Int32
        var fieldDescriptor: Int32
        var superClassType: Int32
        var metadataNegativeSizeInWords: UInt32
        var metadataPositiveSizeInWords: UInt32
        var numImmediateMembers: UInt32
        var numFields: UInt32
        var fieldOffsetVectorOffset: UInt32
        var VTableOffset: UInt32    // uint32_t VTableOffset;
        var VTableSize: UInt32      // uint32_t VTableSize;
        //V-Table VTableDescriptorFlags[VTableSize];
    }
    
    struct VTableDescriptorFlags {
        var kind: UInt32
        var Offset: UInt32
    }
    
    // OverrideTable结构如下,紧随VTable后4字节为OverrideTable数量,再其后为此结构数组
    struct SwiftInheritMethod {
        var classDescriptor: TargetClassDescriptor
        var method: VTableDescriptorFlags
        var OverrideMethod: VTableDescriptorFlags
    };
    
  • 遍历__swift5_types, 找到函数方法的地址

    if let unwrappedPtr = ptr {
        var location: UInt64 = 0
        let uint64Representation: UInt64 = UInt64(bitPattern: Int64(Int(bitPattern: unwrappedPtr)))
        let UInt64Size: UInt64 = UInt64(Int64(MemoryLayout<UInt32>.size))
        // 遍历__swift5_types,内部包含class、struct、enum
        for _ in 0..<uint64Representation/UInt64Size {
            let offset: UInt64 = uint64Representation  + location - linkBaseAddress
            // 计算出当前正在遍历的4字节在Mach-O文件中的偏移
            let dataLoAddress = mhHeaderPtr_IntRepresentation + offset
            // 从内存中获取那4字节的内容
            let dataLoContent = UnsafePointer<UInt32>.init(bitPattern: Int(exactly: dataLoAddress) ?? 0)?.pointee
            // mach-O中记录的是虚拟地址,content + offset 是Swift的相对寻址方式,得到的是虚拟地址,因此需要 - linkBase即为类描述的偏移地址
            let typeDescOffset = UInt64(dataLoContent!) + offset - linkBaseAddress
            // 计算出类的描述在内存中的位置
            let typeDescAddress = typeDescOffset + mhHeaderPtr_IntRepresentation        
            
            //按SwiftType结构去解析内存
            let swiftType = UnsafePointer<SwiftType>.init(bitPattern: Int(exactly: typeDescAddress) ?? 0)?.pointee
            if let swiftFlag = swiftType?.flag, (swiftFlag & 0x80000050) == 0x80000050 {
                // 按TargetClassDescriptor结构去解析内存
                let classDescriptor = UnsafePointer<TargetClassDescriptor>.init(bitPattern: Int(exactly: typeDescAddress) ?? 0)?.pointee
                
                if let name = classDescriptor?.name, let numVTables = classDescriptor?.VTableSize {
                    let nameOffset = Int64(name) + Int64(typeDescOffset) + 8
                    let nameAddress = nameOffset + Int64(mhHeaderPtr_IntRepresentation)
                    if let cChar = UnsafePointer<CChar>.init(bitPattern: Int(nameAddress)) {
                        print("类名:\(String(cString: cChar)), 有\(numVTables)个函数") // 输出类名: Person
                    }
                    
                    var vTableLocation: UInt64 = 0
                    for _ in 0..<numVTables {
                        // vTable的相对地址
                        let vTableOffset = typeDescOffset + vTableLocation + UInt64(Int64(MemoryLayout<TargetClassDescriptor>.size))
                        // 计算出vTable在内存中的位置
                        let vTableAddress = vTableOffset + mhHeaderPtr_IntRepresentation
                        // 按VTableDescriptorFlags结构去解析内存
                        let vTableDescriptorFlags = UnsafePointer<VTableDescriptorFlags>.init(bitPattern: Int(exactly: vTableAddress) ?? 0)?.pointee
                        // 只调用我们自己写的方法
                        if let kind = vTableDescriptorFlags?.kind, (kind == 0x10) {
                            let impAddress = vTableAddress + UInt64(Int64(MemoryLayout<UInt32>.size)) + UInt64(vTableDescriptorFlags!.Offset) - linkBaseAddress
                            ImpTest.callImp(IMP(bitPattern: UInt(impAddress))!)
                        }
                        
                        vTableLocation += UInt64(Int64(MemoryLayout<VTableDescriptorFlags>.size))
                    }
                }
                location += UInt64(Int64(MemoryLayout<UInt32>.size))
            }
        }
    }
    

1105