一、 内存办理
跟 OC 相同,Swift 也是采取根据引用计数的 ARC 内存办理方案(针对堆空间)。Swift 的 ARC 中有 3 种引用,分别为强引用,弱引用,无主引用。
在第一篇文章 《结构体与类》 中我们了解到了 Swift 类的实质是一个 HeapObject 结构体指针。HeapObject 结构中有两个成员变量,metadata
和 refCounts
,metadata
是指向元数据方针的指针,里面存储着类的信息,比如特色信息,虚函数表等。而 refCounts
通过称谓可以知道,它是一个引用计数信息相关的东西。接下来我们来看一下 refCounts
具体是怎样一回事。
1. refCounts – 引用计数的信息
在 Swift 源码中找到 HeapObject.h 文件,并在 HeapObject.h 文件中找到 refCounts
的具体定义,如下:
此刻,我们知道 refCounts
的类型为 InlineRefCounts
,在 RefCount.h 文件中找到 InlineRefCounts
的定义:
typedef RefCounts<InlineRefCountBits> InlineRefCounts;
发现它是一个模版类:RefCounts,RefCounts 接纳一个泛型参数,我们来看一下 RefCounts 的结构:
RefCounts 是什么呢,RefCounts 其实是对引用计数的一个包装,而引用计数的具体类型取决于外部传进来的泛型参数。那这个泛型参数 InlineRefCountBits
是什么,它的定义如下:
typedef RefCountBitsT<RefCountIsInline> InlineRefCountBits;
它也是一个模版类,并且也有一个参数 RefCountIsInline
,其实 RefCountIsInline
传的是 false 或许 true,在源码中可以找到它的运用,这儿就不贴出来了,感兴趣的可以去看看。接下来看一下 RefCountBitsT 的结构:
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
传入一个 Initialized,接下来全局查找 Initialized_t ,找到了 Initialized 为 Initialized_t 枚举的一个值。
接下来看到了 constexpr RefCounts(Initialized_t) : refCounts(RefCountBits(0, 1)) {}
,通过注释得知,一个新的方针的引用计数为 1,并且我们可以看到 refCounts
函数的参数传的不就是前面找到的 RefCountBitsT 么。我们回到 RefCountBitsT 类中找到它的初始化方法,如下:
如图,已知外部调用 RefCountBitsT 初始化方法,strongExtraCount
传 0,unownedCount
传 1。那么 Offsets::StrongExtraRefCountShift
= 33,Offsets::PureSwiftDeallocShift
= 0,Offsets::UnownedRefCountShift
= 1,这三个的值又是怎样来的呢。
我们来看下 RefCountBitOffsets 在 64 位的完结:
由此可知,PureSwiftDeallocShift
= 0,毫无疑问,那么 StrongExtraRefCountShift
和 UnownedRefCountShift
呢,我们发现它们都调用同一个方法 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)
果不其然,refCounts
为 0x0000000000000003
,也就是 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 之后的效果。
如图,在引用到 p2 的进程中,refCounts
值的改动为 0x0000000000000003
-> 0x0000000200000003
-> 0x0000000400000003
。翻开核算器,看看它具体的改动:
留心看,从高 32 位初步,当为 0x0000000200000003
的时分,高 33 位为 1。当为 0x0000000400000003
的时分,高 34 位为 1。所以,当对一个实例方针进行引用的时分,其实是一个位移的运算。
上面这张图是 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 = nil
和 print("Hello, World!")
处,格式化输出一下内存结构,如下:
当 p = nil
之后,refCounts
变成了 0x0000000100000003
,我们翻开核算器,如下:
验证效果与分析的一起,那这个强引用是怎样增加的呢,我们可以通过源码看一下,全局查找 _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;
}
在进行强引用的时分,实质上是调用 refCounts
的 increment
方法,也就是引用计数 +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. 弱引用
在实践开发的进程中,我们大多运用的都是强引用,在某些场景下运用强引用,用不好的话会形成循环引用。举个比如,如图:
A 对 B 进行了一个强引用,B 对 A 进行一个强引用,此刻会形成循环引用,循环引用会导致无法独自开释某一类的内存,从而导致内存泄露。处理的方法就是把某一引用改成弱引用,如图,把原本 B 对 A 的强引用,换成弱引用。这种相似的场景不就是开发中经常用到的代理形式么。
在 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
处打一个断点,我们检查汇编代码。
通过汇编,我们可以看到,用 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);
}
在这儿,它调用了 refCounts
的 formWeakReference
函数,形成了弱引用,我们再来看一下 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 步:
- 取出原本的
refCounts
-引用计数的信息。 - 判别原本的
refCounts
是否有散列表,假定有直接回来,假定没有并且正在析构直接回来 nil。 - 创建一个散列表。
- 对原本的散列表以及正在析构的一些处理。
接下来我们来看看这个散列表 – HeapObjectSideTableEntry,全局查找 HeapObjectSideTableEntry 找到了官方的一些注释,如下:
其实在这儿,官方现已奉告我们强引用和弱引用内部完结的区别了。我们接下来看一下 HeapObjectSideTableEntry 的结构。
可以看到,HeapObjectSideTableEntry 中存着方针的指针,并且还有一个 refCounts
,而 refCounts
的类型为 SideTableRefCounts,那这个 SideTableRefCounts 又是什么呢?其实 SideTableRefCounts 就是继承自我们前面学过的 RefCountBitsT 的模版类。
并且,它还多了一个 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
从原本的 0x0000000000000003
变成了 0xc000000020264920
。翻开核算器,如下:
如图,在用 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
修饰的时分,实质上是创建了一个散列表。
4. 无主引用
在 Swift 中可以通过 unowned
定义无主引用,unowned
不会发生强引用,实例毁掉后仍然存储着实例的内存地址(相似于 OC 中的 unsafe_unretained
)。需求留心的是妄图在实例毁掉后访问无主引用,会发生工作时差错(野指针)。
weak
、unowned
都能处理循环引用的问题,unowned
要比 weak
少一些功能耗费,那我们怎样来挑选 weak
和 unowned
呢。
根据苹果的官⽅⽂档的主张。当我们知道两个方针的⽣命周期并不相关,那么我们有必要使⽤ weak
。相反,⾮强引⽤方针具有和强引⽤方针相同或许更⻓的⽣命周期的话,则应该使⽤ unowned
。
简略来说就是:
- 在生命周期中可能会变为 nil 的运用
weak
。 - 初始化赋值后再也不会变为 nil 的运用
unowned
。
5. 闭包的循环引用
5.1. 闭包循环引用的原因及处理方案
闭包表达式默许会对用到的外层方针发生额定的强引用(对外层方针进行了 retain 操作)。
下面代码会发生循环引用,导致 SHPerson 方针无法开释(看不到 SHPerson 的 deinit
被调用)。
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. 泛型指针
相⽐较原⽣指针来说,泛型指针就是指定其时指针现已绑定到了具体的类型。
在进⾏泛型指针访问的进程中,我们并不是使⽤ load
和 store
⽅法来进⾏存储操作。这⾥我们使⽤到其时泛型指针内置的变量 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)
为了愈加清楚的了解,我们来看一张图:
那这张图怎样去了解呢,我们看下面的代码:
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。
下面这段代码是通过 unsafeBitCast
把 SHPerson 类型强制转化成 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
};
TargetMethodDescriptor、TargetClassDescriptor 、FieldDescriptor 和 FieldRecord 在前面的华章都有介绍,这儿不做过多的解说。需求留心的是,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