一、 内存办理

跟 OC 相同,Swift 也是采取根据引用计数的 ARC 内存办理方案(针对堆空间)。Swift 的 ARC 中有 3 种引用,分别为强引用弱引用无主引用

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

1. refCounts – 引用计数的信息

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

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

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

typedef RefCounts<InlineRefCountBits> InlineRefCounts;

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

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

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

typedef RefCountBitsT<RefCountIsInline> InlineRefCountBits;

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

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

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 的初始化赋值,如下:

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

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

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

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

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

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

我们来看下 RefCountBitOffsets 在 64 位的完结:

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

由此可知,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 之后的效果。

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

如图,在引用到 p2 的进程中,refCounts 值的改动为 0x0000000000000003 -> 0x0000000200000003 -> 0x0000000400000003。翻开核算器,看看它具体的改动:

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

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

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

留心看,从高 32 位初步,当为 0x0000000200000003 的时分,高 33 位为 1。当为 0x0000000400000003 的时分,高 34 位为 1。所以,当对一个实例方针进行引用的时分,其实是一个位移的运算。

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

上面这张图是 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!") 处,格式化输出一下内存结构,如下:

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

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

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

验证效果与分析的一起,那这个强引用是怎样增加的呢,我们可以通过源码看一下,全局查找 _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. 弱引用

在实践开发的进程中,我们大多运用的都是强引用,在某些场景下运用强引用,用不好的话会形成循环引用。举个比如,如图:

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

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 处打一个断点,我们检查汇编代码。

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

通过汇编,我们可以看到,用 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 找到了官方的一些注释,如下:

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

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

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

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

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

并且,它还多了一个 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 修饰之后的改动。

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

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

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

如图,在用 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,如下:

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

如图,验证的效果与分析一起。所以,当用 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)

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

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

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

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