Swift进阶—— 指针&内存管理

300 阅读11分钟

指针类型

Swift中指针主要有两类,指定数据类型指针和原生指针,指定数据类型指针如unsafePointer<T>(指针及所指向的内容都不可变),unsafeMutablePointer<T>(指针及所指向的内容都可变),原生指针是未指定数据类型的,如unsafeRawPointer(指针指向的内存区域未定),unsafeMutableRawPointer(同上)。

原生指针的使用

我们直接上demo看(清晰明朗),通过allocate(byteCount:32,alignment:8),创建了一个32字节大小,8字节对齐的连续内存空间,然后通过advance移动t,调用storeBytes进行数据存储,读取的时候通过load(fromByteOffset:as:)来读取,接着调用deallocate()释放分配的内存空间。

截屏2022-01-27 下午6.09.15.png

泛型指针的使用

取当前变量age的内存指针:

截屏2022-01-27 下午6.28.10.png 若要修改内存指针,就获取指针指向的具体的数据类型:

截屏2022-01-27 下午6.41.46.png 截屏2022-01-27 下午6.35.07.png

直接分配内存

截屏2022-01-28 上午10.47.01.png

这里我们可以画张图来展示下内存分配的具体操作流程:

截屏2022-01-28 下午1.59.30.png

我们在调用allocate(capacity:)分配一段内存空间后,这个内存空间是还未被初始化的,所以接着要调用initialized(to:)初始化分配的内存空间,当使用完这段内存空间后,要调用deinitialize来把内存空间的值置空,最后调用deallocate回收这段内存空间。接着我们看个示例,如下图:

截屏2022-01-28 下午2.13.02.png 上图中我们注意到一点,如果我们初始化了两次指针内存,那我们内存空间相应的要置空两次。还有一种初始化指针的方式,我们看示例,如下图:

截屏2022-01-28 下午2.21.27.png

指针读取Mach-O中的属性名称

我们之前已经通过MachOView分析过如何获取属性名称在Mach-O文件中的值,接下来我们用代码来实现下,如下图:

截屏2022-01-28 下午2.44.42.png 首先获取Mach-O文件中的内存地址,通过getsectdata()方法获取,可以看到地址是0x3f30,把执行文件放进MachOView观察,内存地址也是0x3F30,如下图:

截屏2022-01-28 下午2.45.23.png 接下来我们还要获取当前程序运行的基地址,而且我们还要减去虚拟基地址,我们通过_dyld_get_image_header(0)来获取下Mach-O文件中header的起始地址,即我们之前讲的当前程序运行的地址:

截屏2022-01-28 下午3.37.07.png 拿到程序运行基地址后,我们先放着,接着获取当前程序的虚拟内存地址和偏移量,那它放在LG_SEGMENT_64(_LINKEDIT)中,我们看下MachOView中查看如下图:

截屏2022-01-28 下午3.53.02.png 我们用代码获取就是通过getsegbyname("__LINKEDIT"),返回的是一个unsafePointer<segment_command_64>泛型指针,它里面就包含了虚拟内存地址和偏移量,如下图:

截屏2022-01-28 下午3.59.43.png 下图我们可以看到unsafePointer<segment_command_64>这个结构体指针: 截屏2022-01-28 下午4.00.31.png 我们接着进入seg_command_64结构体中,就可以清晰看到它的所有成员,包括vmaddrfileoff

截屏2022-01-28 下午3.58.06.png 我们用虚拟内存地址(vmaddr)减去偏移量(fileoff)才是当前程序链接的基地址(虚拟内存基地址),声明一个变量linkBaseAddress来接收计算结果,我们看代码如下图:

截屏2022-01-28 下午4.17.25.png 我们开始通过getsectdata()获取的内存地址是带了虚拟内存基地址的,所以这时候要减去,当然直接减是无法减的,我们需要bitPattern()函数做数据类型转换,如下:

截屏2022-01-28 下午4.41.29.png 我们计算结果是16164,转成16进制也就是0x3F24,我们编译程序打开MachOView查看同样也是0x3F24(再次编译时,这个地址会改变,后面看到的地址可能是下一次编译的结果),如下:

截屏2022-01-28 下午4.44.26.png 接着我们获取DataLO那里4字节的地址,通过当前程序运行的基地址加上刚刚计算到的offset(0x3F24),就是DataLO在程序运行中真正的内存地址,如下:

截屏2022-01-28 下午4.54.40.png 程序运行基地址加offset计算出dataLoAddress内存地址代码如下:

截屏2022-01-28 下午7.59.26.png 接着调用withUnsafePointer(to:)把它转化为指针类型再返回回来当前指针指向的内存信息,如下图示例:

截屏2022-01-28 下午8.32.20.png 可以看到当前计算的结果是4294967136,转换为16进制就是0xFFFFFF60Mach-O文件查看dataLo结果也是这个值,如下图:

截屏2022-01-28 下午8.40.37.png 那程序运行的地址offset加上dataLo的值减去虚拟内存基地址linkBaseAddress就是descriptor在Mach-O文件中的内存偏移地址typeDesOffset,之后偏移地址typeDesOffset加上程序运行的基地址mhHeaderPtr_IntRepresention(headerPtr转换而来方便计算)就是descriptor在程序运行中的真正内存地址,代码实现如下图:

截屏2022-01-28 下午10.39.55.png 找到descriptor真正的内存地址后,它其实指向的就是TargetDescriptor,我们直接把它转换为TargetDescriptor,然后通过poitee拿到具体的数据类型,如下:

截屏2022-01-28 下午10.52.01.png 若我们要获取类名name(CTPerson),首先拿到TargetClassDescriptorname的地址信息,加上descriptor的偏移地址typeDesOffset,再加上8个字节的偏移,就是nameMach-O文件中的偏移地址,然后加上程序运行的基地址mhHeaderPtr_IntRepresention就是name在程序运行中真正的内存地址,最后把nameAddress转换为CChar即字符串类型,打印输出:

截屏2022-01-28 下午11.58.19.png

那么接下来我们也很容易找到属性描述文件fieldDescriptor的位置,当然我们首先通过swift源码复原了下fieldDescriptor的相关的结构类型,如下:

struct FieldDescriptor {

    var mangledTypeName:Int32

    var superClass:Int32

    var kind:UInt16

    var fieldRecordSize:UInt16

    var numberFields:UInt32

    var fieldRecords:Array<FieldRecord>

}

struct FieldRecord {

    var flags:UInt32

    var mangledTypeName:Int32

    var fieldName:Int32

}

根据它的具体数据类型,我们就很容易操作了,代码如下:

// 获取fieldDescriptor的相对地址

let fieldDescriptorRelativeAddress = typeDesOffset + 16 + mhHeaderPtr_IntRepresention

// 获取fieldDescriptor下存储的偏移量

let fieldDescriptorOffset = UnsafePointer<UInt32>.init(bitPattern: Int(exactly: fieldDescriptorRelativeAddress) ?? 0)?.pointee

//fieldDescriptor真正的内存地址

let fieldDescriptorAddress = fieldDescriptorRelativeAddress + UInt64(fieldDescriptorOffset!)

let fieldDescriptor = UnsafePointer<FieldDescriptor>.init(bitPattern: Int(exactly: fieldDescriptorAddress) ?? 0)?.pointee

for i in 0..<fieldDescriptor!.numberFields{

    let stride:UInt64 = UInt64(i * 12)

    let fieldRecordAddress = fieldDescriptorAddress + stride + 16

    let fieldNameRelativeAddress = UInt64(2 * 4) + fieldRecordAddress - linkBaseAddress + mhHeaderPtr_IntRepresention

    let offset = UnsafePointer<UInt32>.init(bitPattern: Int(exactly: fieldNameRelativeAddress) ?? 0)?.pointee

    let fieldNameAddress = fieldNameRelativeAddress + UInt64(offset!) - linkBaseAddress

    if let cChar = UnsafePointer<CChar>.init(bitPattern: Int(fieldNameAddress)){

        print(String(cString: cChar))

    }

}

内存绑定

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

  • assumingMemoryBound(to:) 使用场景:我们需要传入的类型和本身的传入的数据类型相似,不想经过复杂的类型转换,则可以使用该API,它本质是绕过编译器检查,并没有发生实际的类型转换。
  • bindMemory(to:capacity:) 用于更改内存绑定的类型,若当前还没有类型绑定,则首次绑定为该类型;否则重新绑定该类型,并且内存中中所有的值都会变为该类型。

截屏2022-02-11 下午2.44.48.png

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

截屏2022-02-11 下午4.54.44.png

内存管理

swift中使用自动引用计数(ARC)机制来追踪和管理内存。之前我们在分析对象内存布局的时候,已经知道前8个字节是metadata(结构体指针),紧接着的8个字节是refCount,如下图:

截屏2022-02-11 下午4.45.34.png 我们从图中看到refCount的8个字节是0x0000000003,但是我们无法确定这个引用计数是多少,接下来就需要去源码中一探究竟了。首先我们找到refCount的定义,如下图:

截屏2022-02-11 下午5.04.43.png

接着找InlineRefCounts的定义:

截屏2022-02-11 下午5.07.23.png

截屏2022-02-11 下午5.16.17.png

最终找到了RefCountBitsT这个类,我们可以看到它的一个属性BitsType bits

截屏2022-02-11 下午5.23.06.png 进入Type可以看到它最终是个64位的位域信息:

截屏2022-02-11 下午5.28.16.png

那我们创建一个对象的时候,它的引用计数到底是多少呢,我们从对象开始创建的时候看下:

截屏2022-02-11 下午5.32.15.png

截屏2022-02-11 下午5.35.58.png 找到initialized,发现它是个枚举值,最终传值是0,1,而这里的RefCountBits就是我们之前找到RefCountBitsT这个类:

截屏2022-02-11 下午5.41.13.png

接着我们找下RefCountBitsT这个类的初始化函数,如下图:

截屏2022-02-11 下午5.49.57.png

所以我们清楚看到,系统是把strongExtraCount(强引用计数)左移StrongExtraRefCountShift(33)位,unownedCount左移UnownedRefCountShift(1)位之后存储在64位上的。他们位置的存储分布如下图:

截屏2022-02-11 下午6.14.41.png

unownedRefCount无主引用存储的位置,isDeinitingMask是否析构的标识位,strongExtraRefCount强引用计数存储位。那引用计数是如何增加的呢,我们在源码中也可以看到相关操作,如下图:

截屏2022-02-11 下午6.23.42.png 可以看到它是通过inc(1)左移StrongExtraRefCountShift(33)然后进行累加得到的。 截屏2022-02-11 下午6.27.51.png

弱引用

弱引用因不会对引用的实例保持强引用,因而不会阻止ARC释放被引用的失礼了。由于弱引用不会强保持对实例的引用,所以实例被释放后,弱引用仍有可能引用着这个实例,因此ARC会在实例被释放的时候自动地设置弱引用为nil。由于弱引用需要允许他们的值为nil,他们一定得是可选类型。
我们初始化一个弱引用变量后,系统是如何操作的呢,我们汇编调试后,如下:

截屏2022-02-11 下午6.49.07.png 系统会调用一个weakInit方法,我们可以去源码中看下这个方法的调用:

截屏2022-02-11 下午6.48.26.png

从源码中我们可以看到,声明一个weak变量相当于定义了一个WeakRefrence对象:

截屏2022-02-11 下午6.57.11.png

根据nativeInit方法调用找到formWeakReference方法,发现最终它创建了一个散列表SideTable

截屏2022-02-11 下午7.00.52.png

截屏2022-02-11 下午7.02.00.png

截屏2022-02-14 下午1.36.34.png

我们进入HeapObjectSideTableEntry这个类看到如下:

截屏2022-02-14 下午6.13.48.png 看到InlineRefCountBits我们就知道它是RefCountBitsT类型,所以弱引用表跟强引用计数公用的一个类,只不过这里是继承自RefCountBitsT,如下图:

截屏2022-02-14 下午6.05.09.png 所以我们可以清楚知道SideTable是一个类名为HeapObjectSideTableEntry的结构,里面也有RefCounts成员,内部是SideTableRefCountBits,其实就是原来的uint64加上一个存储弱引用的uint32_t。其实源码中有段注释也很明白,我们看下:

截屏2022-02-15 下午2.45.49.png 上图我们可以知道,swift中有两种引用计数,一种是InlineRefCounts,一种是SideTableRefCounts,如果是强引用就是strong Rc + unowned RC + flags,如果是弱引用,就是strong RC + unowned RC + weak RC(32位) + flags。这里我们通过LLDB调试来观察下:

截屏2022-02-15 下午3.12.07.png 图中散列表地址是通过0xc000000020c40810中63位和62标识位置为0,然后左移是3位得到的地址,因为在源码中,我们能够知道散列表的地址是被右移了三位的,如下图:

截屏2022-02-15 下午3.20.45.png

无主引用unowned

和弱引用类似,无主引用不会牢牢保持住引用的实例,但与弱引用不同,无主引用假定是永远有值的,而弱引用是可选值,可以为nil。一般如果强引用的双方生命周期没有任何关联,使用weak,例如delegate,若一个对象销毁,其中一个对象也跟着销毁,则使用owned。当然如果我们自己无法确定对象之间是否关联的时候,我们使用weak一定是没问题的,只不过会有点性能问题。owned性能更好,直接操作64位信息,weak要创建散列表,但是更安全。我们看一个例子:

截屏2022-02-15 下午4.14.17.png 这里testClosurep的声明周期就是关联的,p如果释放了,那自然testClosure就也释放了,所以使用owned是没问题的。

闭包捕获列表

默认情况下,闭包表达式从周围的范围捕获常量和变量,并强引用这些值。你可以使用捕获列表来显示控制如何在闭包中捕获值。 捕获列表中的值是通过找当前上下文中的同名变量,然后用的字面量的值来初始化一个let常量,如下图:

截屏2022-02-15 下午4.27.17.png

上图中捕获列表的age值就是10,下面age值变更为11,并不会影响捕获到的值,打印结果还是10。创建闭包时,内部作用域中的age会用外部作用域中age的值进行初始化,但是他们的值未以任何特殊方式连接,这意味着更改外部作用域中的age的值不会影响内部作用域中的age的值。

截屏2022-02-15 下午4.29.28.png

当然我们如果没有显示声明捕获列表,那么闭包在捕获值类型的时候,是调用的时候进行捕获的,如下:

截屏2022-02-15 下午4.38.06.png

循环引用

一般我们在使用闭包的时候,多会碰到解决循环引用的问题,我们大多时候是把一个对象变为弱引用来打破循环,但是闭包内部我们一般会短暂的延长该对象的生命周期,swift中有两种办法:

截屏2022-02-15 下午4.56.25.png