iOS-Swift 独孤九剑:四、内存管理和指针

1,514 阅读18分钟

一、 内存管理

跟 OC 一样,Swift 也是采取基于引用计数的 ARC 内存管理方案(针对堆空间)。Swift 的 ARC 中有 3 种引用,分别为强引用弱引用无主引用

在第一篇文章 《结构体与类》 中我们了解到了 Swift 类的本质是一个 HeapObject 结构体指针。HeapObject 结构中有两个成员变量,metadatarefCountsmetadata 是指向元数据对象的指针,里面存储着类的信息,比如属性信息,虚函数表等。而 refCounts 通过名称可以知道,它是一个引用计数信息相关的东西。接下来我们来看一下 refCounts 具体是怎么一回事。

1. refCounts - 引用计数的信息

在 Swift 源码中找到 HeapObject.h 文件,并在 HeapObject.h 文件中找到 refCounts 的具体定义,如下:

refCounts 的定义.png

此时,我们知道 refCounts 的类型为 InlineRefCounts,在 RefCount.h 文件中找到 InlineRefCounts 的定义:

typedef RefCounts<InlineRefCountBits> InlineRefCounts;

发现它是一个模版类:RefCountsRefCounts 接收一个泛型参数,我们来看一下 RefCounts 的结构:

RefCounts 的结构.png

RefCounts 是什么呢,RefCounts 其实是对引用计数的一个包装,而引用计数的具体类型取决于外部传进来的泛型参数。那这个泛型参数 InlineRefCountBits 是什么,它的定义如下:

typedef RefCountBitsT<RefCountIsInline> InlineRefCountBits;

它也是一个模版类,并且也有一个参数 RefCountIsInline,其实 RefCountIsInline 传的是 false 或者 true,在源码中可以找到它的使用,这里就不贴出来了,感兴趣的可以去看看。接下来看一下 RefCountBitsT 的结构:

RefCountBitsT 的结构.png

RefCountBitsT 类中只有一个属性信息 bits,如图,bits 的类型为 BitsType,并且由 RefCountBitsInt 中的 Type 来定义,我们来看一下 RefCountBitsInt 的结构:

// 64-bit inline
// 64-bit out of line
template <RefCountInlinedness refcountIsInline>
struct RefCountBitsInt<refcountIsInline, 8> {
    typedef uint64_t Type;
    typedef int64_t SignedType;
};

可以看到,Type 的类型是一个 uint64_t 的位域信息,在这个 uint64_t 的位域信息中存储着运行生命周期的相关引用计数

到这儿,我们仍然不知道是如何设置的,我们先来看⼀下,当我们创建⼀个实例对象的时候,当前的引⽤计数是多少?我们找到 HeapObject 的定义,在 HeapObject 的初始化方法中我们看到了 refCounts 的初始化赋值,如下:

refCounts 的初始化赋值.png

我们看到 refCounts 传入一个 Initialized,接下来全局搜索 Initialized_t ,找到了 InitializedInitialized_t 枚举的一个值。

Initialized_t 的定义.png

接下来看到了 constexpr RefCounts(Initialized_t) : refCounts(RefCountBits(0, 1)) {} ,通过注释得知,一个新的对象的引用计数为 1,并且我们可以看到 refCounts 函数的参数传的不就是前面找到的 RefCountBitsT 么。我们回到 RefCountBitsT 类中找到它的初始化方法,如下:

RefCountBitsT 的初始化方法.png

如图,已知外部调用 RefCountBitsT 初始化方法,strongExtraCount 传 0,unownedCount 传 1。那么 Offsets::StrongExtraRefCountShift = 33,Offsets::PureSwiftDeallocShift = 0,Offsets::UnownedRefCountShift = 1,这三个的值又是怎么来的呢。

我们来看下 RefCountBitOffsets 在 64 位的实现:

RefCountBitOffsets 的实现.png

由此可知,PureSwiftDeallocShift = 0,毫无疑问,那么 StrongExtraRefCountShiftUnownedRefCountShift 呢,我们发现它们都调用同一个方法 shiftAfterField,找到它的实现,如下:

# define shiftAfterField(name) (name##Shift + name##BitCount)

这是一个宏定义实现,传一个参数 name,内部做相加的操作。注意看,## 运算符在 C++ 中是用来粘合的,比如一个名字,或者一个值,所以:

  • UnownedRefCountShift 传的是 PureSwiftDealloc,那么内部的实现为:
PureSwiftDeallocShift + PureSwiftDeallocBitCount)-> (0 + 1= 1
  • StrongExtraRefCountShift 传的是 IsDeiniting,那么内部的实现:
(IsDeinitingShift + IsDeinitingBitCount)

IsDeinitingShift 传的是 UnownedRefCount,所以应该是 。

( (UnownedRefCountShift + UnownedRefCountBitCount) + IsDeinitingBitCount)-> ((1 + 31 + 1))= 33

知道了这三个值的由来之后,我们开始计算 RefCountBitsT 的初始化方法调用 bits 的值:

0 << 33 | 1 << 0 | 1 << 1;
0 | 1 | 2 = 3;

我们最终算出来的结果为 3,其实整个流程下来我们都在干什么,在求 refCounts 的值。

所以在创建一个对象并第一次引用对象的时候,refCounts = 3。我们来验证一下,代码如下:

class SHPerson {
    var age = 18
    var name = "Coder_张三"
}

let p = SHPerson()
// 打印当前 p 实例的内存指针地址
print(Unmanaged.passUnretained(p as AnyObject).toOpaque())
print("Hello, World!")

print("Hello, World!") 处打一个断点,程序运行起来之后,拿到 p 实例的内存指针地址,通过 x/4gx 打印内存结构,结果如下:

打印结果:
0x0000000100720ce0
(lldb) x/4gx 0x0000000100720ce0
0x100720ce0: 0x0000000100008198 0x0000000000000003
0x100720cf0: 0x0000000000000012 0xbce55f7265646f43
(lldb)

果不其然,refCounts0x0000000000000003,也就是 3。

2. 强引用

默认情况下,引用都是强引用。通过 refCounts 我们了解到它是一个引用计数信息相关的东西,在创建一个对象之后它的初始值为 0x0000000000000003,如果我对这个实例对象进行多个引用,refCounts 会不会变呢。

代码如下:

class SHPerson {
    var age = 18
    var name = "Coder_张三"
}

let p = SHPerson()

// 打印当前 p 实例的内存指针
print(Unmanaged.passUnretained(p as AnyObject).toOpaque())

let p1 = p
let p2 = p

print("Hello, World!")

把断点分别打在 p1,p2,print("Hello, World!") 处,在控制台中通过 x/4gx 打印出 p 指针的内存结构,过一个断点打印一次,分别打印三次,对应赋值 p1 之前,p2 之前,p2 之后的结果。

0x000000010071d120 打印比较.png

如图,在引用到 p2 的过程中,refCounts 值的变化为 0x0000000000000003 -> 0x0000000200000003 -> 0x0000000400000003。打开计算器,看看它具体的变化:

0x0000000000000003.png

0x0000000200000003.png

0x0000000400000003.png

注意看,从高 32 位开始,当为 0x0000000200000003 的时候,高 33 位为 1。当为 0x0000000400000003 的时候,高 34 位为 1。所以,当对一个实例对象进行引用的时候,其实是一个位移的运算。

64位位域信息 refCounts 的存储.png

上面这张图是 64 位位域信息下 refCounts 的存储。我们来测试一下 isDeinitingMask,如果没有对实例进行一个强引用,这个实例会被释放掉,也就是 32 位会变成 1。代码如下:

class SHPerson {
    var age = 18
    var name = "Coder_张三"
}

var p: SHPerson? = SHPerson()
// 打印当前 p 实例的内存指针
print(Unmanaged.passUnretained(p as AnyObject).toOpaque())

p = nil

print("Hello, World!")

把断点分别打在 p = nilprint("Hello, World!") 处,格式化输出一下内存结构,如下:

0x0000000100000003打印.png

p = nil 之后,refCounts 变成了 0x0000000100000003,我们打开计算器,如下:

0x0000000100000003.png

验证结果与分析的一致,那这个强引用是如何添加的呢,我们可以通过源码看一下,全局搜索 _swift_retain_,在 HeapObject.cpp 文件中找到它的实现,如下:

static HeapObject *_swift_retain_(HeapObject *object) {
    SWIFT_RT_TRACK_INVOCATION(object, swift_retain);
    if (isValidPointerForNativeRetain(object)) object->refCounts.increment(1);
    return object;
}

在进行强引用的时候,本质上是调用 refCountsincrement 方法,也就是引用计数 +1。我们来看一下 increment 的实现:

void increment(uint32_t inc = 1) {
    auto oldbits = refCounts.load(SWIFT_MEMORY_ORDER_CONSUME);

    // constant propagation will remove this in swift_retain, it should only
    // be present in swift_retain_n
    if (inc != 1 && oldbits.isImmortal(true)) {
        return;
    }

    RefCountBits newbits;
    do {
        newbits = oldbits;
        bool fast = newbits.incrementStrongExtraRefCount(inc);
        if (SWIFT_UNLIKELY(!fast)) {
            if (oldbits.isImmortal(false)) return;
            return incrementSlow(oldbits, inc);
        }
    } while (!refCounts.compare_exchange_weak(oldbits, newbits, std::memory_order_relaxed));
}

看到关键的代码,在 increment 中调用了 incrementStrongExtraRefCount,我们再去看看 incrementStrongExtraRefCount 的实现:

// Returns true if the increment is a fast-path result.
// Returns false if the increment should fall back to some slow path
// (for example, because UseSlowRC is set or because the refcount overflowed).
SWIFT_NODISCARD SWIFT_ALWAYS_INLINE bool
incrementStrongExtraRefCount(uint32_t inc) {
    // This deliberately overflows into the UseSlowRC field.
    bits += BitsType(inc) << Offsets::StrongExtraRefCountShift;
    return (SignedBitsType(bits) >= 0);
}

注意看,在前面我们已经知道 StrongExtraRefCountShift = 33,外部传进来的 inc = 1。假设此时 bits = 0,那此时就是 1 << 33 = 0x2。如果还有变量对其进行强引用,就是 1 += 1 << 33 --> 2 << 33 = 0x4。

到这里, incrementStrongExtraRefCount 的实现就对应了前面讲的赋值 p1,p2 后,refCounts 高 32 位开始的变化。

3. 弱引用

在实际开发的过程中,我们大多使用的都是强引用,在某些场景下使用强引用,用不好的话会造成循环引用。举个例子,如图:

强引用和弱引用.png

AB 进行了一个强引用,BA 进行一个强引用,此时会造成循环引用,循环引用会导致无法单独释放某一类的内存,从而导致内存泄漏。解决的办法就是把某一引用改成弱引用,如图,把原本 BA 的强引用,换成弱引用。这种类似的场景不就是开发中经常用到的代理模式么。

在 Swift 中可以通过 weak 定义弱引用,定义必须用 var,在用 weak 定义一个属性的时候,该属性默认是一个可选值,因为实例销毁之后 ARC 会自动将弱引用设置为 nil。需要注意的是 ARC 自动给弱引用设置为 nil 时,不会触发属性观察器。

接下来我们看一下用 weak 修饰的本质是什么,代码如下:

class SHPerson {
    var age = 18
    var name = "Coder_张三"
}

weak var p = SHPerson()
print("Hello, World!")

print 处打一个断点,我们查看汇编代码。

swift_weakInit 引入.png

通过汇编,我们可以看到,用 weak 修饰之后,p 变成了一个可选项,并且,之后会调用一个 swift_weakInit 函数,紧接着调用 swift_release 函数,将 p 的实例释放掉了。

我们来看一下 swift_weakInit 函数在源码中是怎么实现的,在 HeapObject.cpp 文件中,swift_weakInit 的实现如下:

WeakReference *swift::swift_weakInit(WeakReference *ref, HeapObject *value) {
    ref->nativeInit(value);
    return ref;
}

通过源码,可以知道用 weak 修饰之后,在内部会生成 WeakReference 类型的变量,并在 swift_weakInit 中调用 nativeInit 函数。nativeInit 的实现如下:

void nativeInit(HeapObject *object) {
    auto side = object ? object->refCounts.formWeakReference() : nullptr;
    nativeValue.store(WeakReferenceBits(side), std::memory_order_relaxed);
}

在这里,它调用了 refCountsformWeakReference 函数,形成了弱引用,我们再来看一下 formWeakReference 的实现:

// SideTableRefCountBits specialization intentionally does not exist.
template <>
HeapObjectSideTableEntry* RefCounts<InlineRefCountBits>::formWeakReference()
{
    auto side = allocateSideTable(true);
    if (side)
    return side->incrementWeak();
    else
    return nullptr;
}

可以发现,它本质上就是创建了一个散列表,我们接下来看一下散列表的创建:

// Return an object's side table, allocating it if necessary.
// Returns null if the object is deiniting.
// SideTableRefCountBits specialization intentionally does not exist.
template <>
HeapObjectSideTableEntry* RefCounts<InlineRefCountBits>::allocateSideTable(bool failIfDeiniting)
{
    // 1. 取出原来的 refCounts-引用计数的信息
    auto oldbits = refCounts.load(SWIFT_MEMORY_ORDER_CONSUME);

    // Preflight failures before allocating a new side table.
    // 2. 判断原来的 refCounts 是否有当前的引用计数
    if (oldbits.hasSideTable()) {
        // Already have a side table. Return it.
        // 如果有直接返回
        return oldbits.getSideTable();
    }
    else if (failIfDeiniting && oldbits.getIsDeiniting()) {
        // Already past the start of deinit. Do nothing.
        // 如果没有并且正在析构直接返回 nil
        return nullptr;
    }

    // Preflight passed. Allocate a side table.

    // FIXME: custom side table allocator
    // 3. 创建一个散列表
    HeapObjectSideTableEntry *side = new HeapObjectSideTableEntry(getHeapObject());

    auto newbits = InlineRefCountBits(side);

    // 4. 对原来的散列表以及正在析构的一些处理
    do {
    if (oldbits.hasSideTable()) {
        // Already have a side table. Return it and delete ours.
        // Read before delete to streamline barriers.
        auto result = oldbits.getSideTable();
        delete side;
        return result;
    }
    else if (failIfDeiniting && oldbits.getIsDeiniting()) {
        // Already past the start of deinit. Do nothing.
        return nullptr;
    }

    side->initRefCounts(oldbits);

    } while (! refCounts.compare_exchange_weak(oldbits, newbits, std::memory_order_release, std::memory_order_relaxed));
    return side;
}

散列表的创建可分为 4 步:

  1. 取出原来的 refCounts-引用计数的信息。
  2. 判断原来的 refCounts 是否有散列表,如果有直接返回,如果没有并且正在析构直接返回 nil。
  3. 创建一个散列表。
  4. 对原来的散列表以及正在析构的一些处理。

接下来我们来看看这个散列表 - HeapObjectSideTableEntry,全局搜索 HeapObjectSideTableEntry 找到了官方的一些注释,如下:

官方注释,强引用和弱引用的区别.png

其实在这里,官方已经告诉我们强引用和弱引用内部实现的区别了。我们接下来看一下 HeapObjectSideTableEntry 的结构。

HeapObjectSideTableEntry结构.png

可以看到,HeapObjectSideTableEntry 中存着对象的指针,并且还有一个 refCounts,而 refCounts 的类型为 SideTableRefCounts,那这个 SideTableRefCounts 又是什么呢?其实 SideTableRefCounts 就是继承自我们前面学过的 RefCountBitsT 的模版类。

SideTableRefCountBits 的结构.png

并且,它还多了一个 weakBits

到这里,当我们用 weak 修饰之后,这个散列表就会存储对象的指针和引用计数信息相关的东西。我们来验证一下是否存储了对象的指针,代码如下:

class SHPerson {
    var age = 18
    var name = "Coder_张三"
}

var p = SHPerson()
// 打印当前 p 实例的内存指针
print(Unmanaged.passUnretained(p as AnyObject).toOpaque())
weak var p1 = p
print("Hello, World!")

打上断点了之后我们来看下用 weak 修饰之后的变化。

weak 修饰后的 refCounts.png

如图,用 weak 修饰后,refCounts 从原来的 0x0000000000000003 变成了 0xc000000020264920。打开计算器,如下:

0xc000000020264920.png

如图,在用 weak 修饰之后变成的 0xc000000020264920 ,在 62 位 和 63 位会变成 1,此时需要还原,将 1 还原成 0,还原之后的内存地址变成了 0x20264920

我们接下来看一下这个散列表的生成 - InlineRefCountBits

typedef RefCountBitsT<RefCountIsInline> InlineRefCountBits;

可以看到,InlineRefCountBits 也是一个 RefCountBitsT 的模版类。它对应的初始化方法如下:

RefCountBitsT(HeapObjectSideTableEntry* side)
: bits((reinterpret_cast<BitsType>(side) >> Offsets::SideTableUnusedLowBits) 
       | (BitsType(1) << Offsets::UseSlowRCShift) 
       | (BitsType(1) << Offsets::SideTableMarkShift))
{
    assert(refcountIsInline);
}

SideTableUnusedLowBits = 3,所以,在这个过程中,传进来的散列表往右移了 3 位,下面的两个是 62 位和 63 位标记成 1。所以,我们回到计算器,它既然是右移 3 位,那么我左移 3 位把它还原。

0x20264920 左移 3 位的结果等于 0x101324900。接下来在 Xcode 中我们格式化输出 0x101324900,如下:

验证 weak 修饰后存储的信息.png

如图,验证的结果与分析一致。所以,当用 weak 修饰的时候,本质上是创建了一个散列表。

4. 无主引用

在 Swift 中可以通过 unowned 定义无主引用,unowned 不会产生强引用,实例销毁后仍然存储着实例的内存地址(类似于 OC 中的 unsafe_unretained)。需要注意的是试图在实例销毁后访问无主引用,会产生运行时错误(野指针)。

weakunowned 都能解决循环引用的问题,unowned 要比 weak 少一些性能消耗,那我们如何来选择 weakunowned 呢。

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

简单来说就是:

  • 在生命周期中可能会变为 nil 的使用 weak
  • 初始化赋值后再也不会变为 nil 的使用 unowned

5. 闭包的循环引用

5.1. 闭包循环引用的原因及解决方案

闭包表达式默认会对用到的外层对象产生额外的强引用(对外层对象进行了 retain 操作)。

下面代码会产生循环引用,导致 SHPerson 对象无法释放(看不到 SHPersondeinit 被调用)。

class SHPerson {
    var closure: (()->())?

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

    deinit { print("deinit") }
}

func test() {
    let p = SHPerson()
    p.closure = {
    p.run()
    }
    p.closure()
}

test()

在闭包表达式的捕获列表声明 weak 或者 unowned 引用,可以解决循环引用的问题。代码如下:

p.closure = { [weak p] in
    p?.run()
}
p.closure = { [unowned p] in
    p.run()
}

我们还可以在捕获列表里定义新的名称, 甚至还可以添加别的常量,如下:

p.closure = { [weak wp = p, unowned up = p, a = 10 + 20] in
    wp?.run()
}

如果想在定义闭包属性的同时引用 self,这个闭包必须是 lazy 的(因为在实例初始化完毕之后才能引用 self )。如下代码,closure 内部如果用到了实例成员(属性,方法),编译器会强制要求明确的写出 self

class SHPerson {
    lazy var closure: (()->()) = { [weak self] in
        self?.run()
    }

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

    deinit { print("deinit") }
}

func test() {
    let p = SHPerson()
    p.closure()
}

test()

如果 lazy 是闭包调用的结果,那么可以不用考虑循环引用的问题,因为闭包调用后,闭包的声明周期就结束了。代码如下:

class SHPerson {
    var age = 18
    lazy var getAge: Int = {
        self.age
    }()

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

    deinit { print("deinit") }
}

5.2. 捕获列表

那什么是捕获列表呢?

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

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

创建闭包时,将初始化捕获列表中的条⽬。对于捕获列表中的每个条⽬,将常量初始化为在周围范围内具有相同名称的常量或变量的值。

例如,在下⾯的代码中,捕获列表中包含 age,但捕获列表中未包含 height,这使它们具有不同的⾏为。

var age = 0
var height = 0.0

let closure = { [age] in
    print("age: ", age)
    print("height: ", height)
}

age = 10
height = 180

closure() // 输出结果:age:  0,height:  180.0
  • 创建闭包时,内部作⽤域中的 age 会⽤外部作⽤域中的 age 的值进⾏初始化,但它们的值未以任何特殊⽅式连接。

  • 这意味着更改外部作⽤域中的a的值不会影响内部作⽤域中的 age 的值,也不会更改封闭内部的值,也不会影响封闭外部的值。

  • 相⽐之下,只有⼀个名为 height 的变量 - 外部作⽤域中的 height,因此,在闭包内部或外部进⾏的更改在两个地⽅均可⻅。

二、指针

Swift 中的指针分为两类, typed pointer 指定数据类型指针, raw pointer 未指定数据类型的指针(原⽣指针)。这些都被定性为“Unsafe”(不安全的),常见的有以下4种类型:

  • UnsafePointer<Pointee> 类似于 const Pointee *

  • UnsafeMutablePointer<Pointee> 类似于 Pointee *

  • UnsafeRawPointer 类似于 const void *

  • UnsafeMutableRawPointer 类似于 void *

为什么说指针不安全。

  • ⽐如我们在创建⼀个对象的时候,是需要在堆分配内存空间的。但是这个内存空间的声明周期是有 限的。如果我们使⽤指针指向这块内容空间,当前内存空间的⽣命周期到了(引⽤计数为0),那么指针就变成了未定义的⾏为了。

  • 我们创建的内存空间是有边界的,⽐如我们创建⼀个⼤⼩为 10 的数组,这个时候我们通过指针访问 到了 index = 11 的位置,这个时候就越界了。

  • 访问了⼀个未知的内存空间。 指针类型与内存的值类型不⼀致,也是不安全的。

1. 原生指针

我们创建一个可变的原生指针,8 字节大小,8 字节对齐。用 storeBytes 方法存值,load 方法取值,代码如下:

// 创建一个可变的原生指针,8 个字节大小,8 字节对齐
var ptr = UnsafeMutableRawPointer.allocate(byteCount: 8, alignment: 8)
// 存值
ptr.storeBytes(of: 10, as: Int.self)
// 取值
let value = ptr.load(as: Int.self)
print(value)

接下来我们创建一个 32 字节大小, 8 字节对齐的可变原生指针,如下:

// 获取布长,也就是实例大小字节对齐之后的值
let stride = MemoryLayout<Int>.stride

// 创建一个可变的原生指针,32 个字节大小,8 字节对齐
var ptr = UnsafeMutableRawPointer.allocate(byteCount: 4 * stride, alignment: 8)
// 存值
for i in 0..<4 {
    ptr.advanced(by: i * stride).storeBytes(of: i * 2, as: Int.self)
}
// 取值
for i in 0..<4 {
    let value = ptr.load(fromByteOffset: i * 8, as: Int.self)
    print("i: \(i) - value: \(value)")
}

我们将 i * 2 存入指针,需要在调用 storeBytes 方法之前调用 advanced 方法。advanced 传一个值,可以将值偏移指定的距离。需要注意的是 advanced 传的值,这个值不是根据字节对齐来传的,而是根据 stride,系统分配给结构体的内存大小。

因为存的时候是根据索引偏移了指定的距离存储值,所以取值的时候,也需要偏移指定的距离取值。 fromByteOffset 参数指定需要偏移的距离。

2. 泛型指针

相⽐较原⽣指针来说,泛型指针就是指定当前指针已经绑定到了具体的类型。

在进⾏泛型指针访问的过程中,我们并不是使⽤ loadstore ⽅法来进⾏存储操作。这⾥我们使⽤到当前泛型指针内置的变量 pointee 获取 UnsafePointer 的⽅式有两种。

⼀种⽅式就是通过已有变量获取。

可以用泛型指针获得指向某个变量的指针:

var age = 18
let ptr = withUnsafePointer(to: &age) { $0 }
print(ptr)
print(ptr.pointee)

可以用可变的泛型指针修改指针存储的值:

var age = 18
    let ptr = withUnsafeMutablePointer(to: &age) { ptr -> UnsafeMutablePointer<Int> in
    ptr.pointee += 1
    return ptr
}
print(ptr)
print(ptr.pointee)

还有⼀种⽅式就是直接分配内存:

var age = 18
// 分配一块 Int 类型的内存空间
let ptr = UnsafeMutablePointer<Int>.allocate(capacity: 1)
// 初始化分配的内存空间
ptr.initialize(to: age)
print(ptr)
print(ptr.pointee)

为了更加清楚的理解,我们来看一张图:

指针的初始化流程.png

那这张图怎么去理解呢,我们看下面的代码:

struct SHPerson {
    var age: Int
    var height: Double
}
// 分配一块 SHPerson 类型的内存空间
let ptr = UnsafeMutablePointer<SHPerson>.allocate(capacity: 2)
ptr[0] = SHPerson(age: 18, height: 180)
ptr[1] = SHPerson(age: 20, height: 190)
print(ptr[0])
print(ptr[1])
ptr.deinitialize(count: 2)
ptr.deallocate()

除了通过下标访问之外,我们也可以通过这种方式去初始化泛型指针:

let ptr = UnsafeMutablePointer<SHPerson>.allocate(capacity: 2)
ptr.initialize(to: SHPerson(age: 18, height: 180))
ptr.advanced(by: MemoryLayout<SHPerson>.stride).initialize(to: SHPerson(age: 20, height: 190))
print(ptr.advanced(by: 0).pointee)
print(ptr.advanced(by: MemoryLayout<SHPerson>.stride).pointee)
ptr.deinitialize(count: 2)
ptr.deallocate()

3. 获取指向堆空间实例的指针

class SHPerson {}
var p = SHPerson()
var ptr = withUnsafePointer(to: &p) { UnsafeRawPointer($0) }
var heapPtr = UnsafeRawPointer(bitPattern: ptr.load(as: UInt.self))
print(heapPtr ?? "")

4. 内存绑定

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

4.1. assumingMemoryBound(to:)

有些时候我们处理代码的过程中,只有原始指针(没有保留指针类型),但此刻对于处理代码的我们来说,明确知道指针的类型,我们就可以使⽤ assumingMemoryBound(to:) 来告诉编译器预期的类型。 (注意:这⾥只是让编译器绕过类型检查,并没有发⽣实际类型的转换)。

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

// 元组是值类型,里面存储 10 和 20,那本质上这块内存空间存储的就是 Int 类型的数据
let tuple = (10, 20)
withUnsafePointer(to: tuple) { (tuplePtr: UnsafePointer<(Int, Int)>) in
    // 先转换成原生指针,然后调用 assumingMemoryBound(to:) 方法,告诉编译器当前内存已经绑定过 Int 类型了,这个时候编译器不会检查。
    testPointer(p: UnsafeRawPointer(tuplePtr).assumingMemoryBound(to: Int.self))
}

4.2. bindMemory(to: capacity:)

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

withUnsafePointer(to: tuple) { (tuplePtr: UnsafePointer<(Int, Int)>) in
    testPointer(p: UnsafeRawPointer(tuplePtr).bindMemory(to: Int.self, capacity: 2))
}

4.3. withMemoryRebound(to: capacity: body:)

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

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

let uint8Ptr = UnsafePointer<UInt8>.init(bitPattern: 10)

uint8Ptr?.withMemoryRebound(to: Int8.self, capacity: 1, { (int8Ptr: UnsafePointer<Int8>) in
    testPointer(int8Ptr)
})

5. 指针之间的转换

unsafeBitCast 函数是忽略数据类型的强制转换,不会因为数据类型的变化而改变原来的内存数据,类似于 C++ 中的 reinterpret_cast

var ptr = UnsafeMutableRawPointer.allocate(byteCount: 16, alignment: 1)
ptr.assumingMemoryBound(to: Int.self).pointee = 11
(ptr + 8).assumingMemoryBound(to: Double.self).pointee = 22.0
print(unsafeBitCast(ptr, to: UnsafePointer<Int>.self).pointee) // 11
print(unsafeBitCast(ptr + 8, to: UnsafePointer<Double>.self).pointee) // 22.0
ptr.deallocate()

上面这段代码,我把 Int 类型的 11 和 Double 类型的 22.0 分别存储到 ptr 指针的内存中。在取值的时候,我们可以通过 unsafeBitCast 强制转换,分别取出 Int 类型的 11 和 Double 类型的 22.0。

下面这段代码是通过 unsafeBitCastSHPerson 类型强制转换成 UnsafeRawPointer 类型。

class SHPerson {}
var p = SHPerson()
var ptr = unsafeBitCast(p, to: UnsafeRawPointer.self)
print(ptr)

6. 指针加强的练习

在前面所学的《方法》《属性》中,我们通过源码 + 汇编分析在 Mach-O 文件中找到了方法和属性相关的信息。接下来我们通过指针来获取 Mach-O 文件中的方法和属性相关的信息。

准备工作:

class SHPerson {
    var age: Int = 18
    var name: String = "Coder_张三"
}

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 Offset: UInt32
    var size: UInt32
    //V-Table
}

struct FieldDescriptor {
    var MangledTypeName: UInt32
    var Superclass: UInt32
    var Kind: UInt16
    var FieldRecordSize: UInt16
    var NumFields: UInt32
    var FieldRecords: FieldRecord
}

struct FieldRecord {
    var Flags: UInt32
    var MangledTypeName: UInt32
    var FieldName: UInt32
}

// VTable 的结构
struct TargetMethodDescriptor {
    // 占 4 字节,Flags 标识是什么方法。
    var Flags: UInt32
    // 不是真正的 imp,这里存储的是相对指针,offset。
    var Impl: UInt32
};

TargetMethodDescriptorTargetClassDescriptorFieldDescriptorFieldRecord 在前面的篇章都有介绍,这里不做过多的解释。需要注意的是,SHPerson 必须放在第一位,否则打印的将不是 SHPerson 的属性信息或者方法信息。

6.1. 通过指针获取 Mach-O 文件的属性信息

// 1. 获取 __swift5_types 中正确的内存地址
var size: UInt = 0
// __swift5_types
let types_ptr = getsectdata("__TEXT", "__swift5_types", &size)
//print(types_ptr)

// 获取 Mach-O 文件中 __LINKEDIT 的信息
var segment_command_linkedit = getsegbyname("__LINKEDIT")
// 获取该段的文件内存的地址
let vmaddr = segment_command_linkedit?.pointee.vmaddr
// 获取该段的文件偏移量
let fileoff = segment_command_linkedit?.pointee.fileoff
// 计算出链接的基地址(也就是虚拟内存的基地址)
var link_base_address = (vmaddr ?? 0) - (fileoff ?? 0)

// 前面拿到的 __swift5_types 的内存地址,很明显是加了虚拟内存的基地址的。
// 所以要拿到 Swift 类的信息正确的内存地址,需要用 __swift5_types 的内存地址 减去 虚拟内存的基地址
var offset: UInt64 = 0
if let unwrapped_ptr = types_ptr {
    // 把 types_ptr 转换成整型,进行计算
    let types_int_representation = UInt64(bitPattern: Int64(Int(bitPattern: unwrapped_ptr)))
    offset = types_int_representation - link_base_address
    print("offset: ", offset)
}

// 2. 获取 __swift5_types 中那四个字节的内容
// 获取当前程序运行的基地址,这里需要注意!我的 Xcode 版本是 13.2.1,macOS 的版本是 12.1,经测试,在调用 _dyld_get_image_header 函数的时候,index 传 3 才能拿到程序运行的基地址。
// 这个和 Xcode 的版本还有 macOS 的版本有关系,正常来讲传 0 应该就能拿到,如果不能,传 3 或者传别的试试。
var app_base_address = _dyld_get_image_header(3)
//print(app_base_ptr)

// 把 app_base_address 转换成整型,进行计算
let app_base_address_int_representation = UInt64(bitPattern: Int64(Int(bitPattern: app_base_address)))
// 计算出 __swift5_types 中四个字节在程序内存中存放的地址
var data_load_address = app_base_address_int_representation + offset
// 接下来需要拿到这四个字节指向的内容
// 将 data_load_address 转成指针类型
let data_load_address_ptr = withUnsafePointer(to: data_load_address) { $0 }
let data_load_content = UnsafePointer<UInt32>.init(bitPattern: Int(exactly: data_load_address) ?? 0)?.pointee
print("data_load_content: ",data_load_content)

// 3. 获取 Description 的地址
// 获取 Description 在 Mach-O 文件的信息
let description_offset = offset + UInt64(data_load_content ?? 0) - link_base_address
print("description_offset: ", description_offset)
// 获取 Description 在内存中的指针地址
let description_address = description_offset + app_base_address_int_representation

// 将 Description 的指针地址指向 TargetClassDescriptor
let class_description = UnsafePointer<TargetClassDescriptor>.init(bitPattern: Int(exactly: description_address) ?? 0)?.pointee
print("class_description: ", class_description)
// 4. 获取属性信息 - FieldDescriptor
// 16 为 fieldDescriptor 前面四个成员变量的大小,4 个 4 字节,所以为 16
let fieldDescriptor_address_int_representation = description_offset + 16 + app_base_address_int_representation
print("fieldDescriptor_address_int_representation: ", fieldDescriptor_address_int_representation)

// 将 fieldDescriptor_address_int_representation 转成指针地址,这里拿到的地址的值为 fieldDescriptor 的偏移信息
let fieldDescriptorOffset = UnsafePointer<UInt32>.init(bitPattern: Int(exactly: fieldDescriptor_address_int_representation) ?? 0)?.pointee
print("fieldDescriptorOffset: ", fieldDescriptorOffset)

// fieldDescriptor_address_int_representation + 偏移信息 = fieldDescriptor 的真正的内存地址
let fieldDescriptorAddress = fieldDescriptor_address_int_representation + UInt64(fieldDescriptorOffset!)
print("fieldDescriptorAddress: ", fieldDescriptorAddress)

//将 fieldDescriptor 内存地址转成 FieldDescriptor
let fieldDescriptor = UnsafePointer<FieldDescriptor>.init(bitPattern: Int(exactly: fieldDescriptorAddress) ?? 0)?.pointee
print("fieldDescriptor: ", fieldDescriptor)

for i in 0..<fieldDescriptor!.NumFields {
    let a = MemoryLayout<FieldRecord>.stride
    let stride: UInt64 = UInt64(i * UInt32(a))
    let fieldRecordAddress = fieldDescriptorAddress + 16 + stride
    //     print(fieldRecordRelactiveAddress)
    //    let fieldRecord = UnsafePointer<FieldRecord>.init(bitPattern: Int(exactly: fieldRecordAddress) ?? 0)?.pointee
    //     print(fieldRecord)
    let fieldNameRelactiveAddress = (fieldRecordAddress + 8 - link_base_address) + app_base_address_int_representation
    let offset = UnsafePointer<UInt32>.init(bitPattern: Int(exactly: fieldNameRelactiveAddress) ?? 0)?.pointee
    //     print(offset)
    let fieldNameAddress = fieldNameRelactiveAddress + UInt64(offset!) - link_base_address
    if let cChar = UnsafePointer<CChar>.init(bitPattern: Int(fieldNameAddress)){
        print(String(cString: cChar))
    }
}

根据前面的文章 属性 我们已经知道了如何找到 Mach-O 文件中的属性信息,结合上面的代码,做个总结:

  • 第一步:获取 __swift5_types 中正确的内存地址
  • 第二步:获取 __swift5_types 中那四个字节的内容
  • 第三步:获取 TargetClassDescriptor
  • 第四步:获取 FieldDescriptor。 具体可以根据代码中的注释进行理解。

6.2. 通过指针获取 Mach-O 文件的方法信息

获取 Mach-O 文件的方法信息和获取 Mach-O 文件的属性信息的步骤和思路基本一致。

还是和获取属性信息一样,前面的三步不变,在拿到 TargetClassDescriptor 的信息之后,我们做一些准备,将 SHPerson 的属性注释掉,添加两个方法,如下:

class SHPerson {
    func test1() {
        print("test1")
    }

    func test2() {
        print("test2")
    }
}

接下来准备 VTable 的结构,如下:

// VTable 的结构
struct TargetMethodDescriptor {
    // 占 4 字节,Flags 标识是什么方法。
    var Flags: UInt32
    // 不是真正的 imp,这里存储的是相对指针,offset。
    var Impl: UInt32
}

根据 《方法》 这篇文章的理解,通过获取方法信息的代码如下:

// 5. 获取方法信息 - FieldDescriptor
let VTable_size = class_description?.size

for i in 0..<(VTable_size ?? 0) {
    // VTable offset
    let VTable_offset = Int(description_offset) + MemoryLayout<TargetClassDescriptor>.size + MemoryLayout<TargetMethodDescriptor>.size * Int(i)
    // 获取 VTable 的地址
    let VTable_address = Int(app_base_address_int_representation) + VTable_offset
    // 将 VTable_address 转成 TargetMethodDescriptor 结构
    let method_descriptor = UnsafePointer<TargetMethodDescriptor>.init(bitPattern: Int(exactly: VTable_address) ?? 0)?.pointee
    // 拿到方法的函数地址
    let imp_address = VTable_address + 4 + Int((method_descriptor?.Impl ?? 0)) - Int(link_base_address)
    // 转成 IMP
    let imp: IMP = IMP(bitPattern: UInt(imp_address))!
    // 通过 OC 的类和语法调用 IMP,打印方法名
    SHCallFunc.callFunc(imp: imp)
}

SHCallFunc 是一个 OC 的类,为了方便打印,通过 OC 的方法直接调用 IMP,可以清楚的知道是否拿到了 Swift 类的方法信息,SHCallFunc 的代码如下:

@interface SHCallFunc : NSObject
+ (void)callFuncWithImp:(IMP)imp;
@end

@implementation SHCallFunc
+ (void)callFuncWithImp:(IMP)imp {
imp();
}
@end