指针类型
Swift中指针主要有两类,指定数据类型指针和原生指针,指定数据类型指针如unsafePointer<T>(指针及所指向的内容都不可变),unsafeMutablePointer<T>(指针及所指向的内容都可变),原生指针是未指定数据类型的,如unsafeRawPointer(指针指向的内存区域未定),unsafeMutableRawPointer(同上)。
原生指针的使用
我们直接上demo看(清晰明朗),通过allocate(byteCount:32,alignment:8),创建了一个32字节大小,8字节对齐的连续内存空间,然后通过advance移动t,调用storeBytes进行数据存储,读取的时候通过load(fromByteOffset:as:)来读取,接着调用deallocate()释放分配的内存空间。
泛型指针的使用
取当前变量age的内存指针:
若要修改内存指针,就获取指针指向的具体的数据类型:
直接分配内存
这里我们可以画张图来展示下内存分配的具体操作流程:
我们在调用allocate(capacity:)分配一段内存空间后,这个内存空间是还未被初始化的,所以接着要调用initialized(to:)初始化分配的内存空间,当使用完这段内存空间后,要调用deinitialize来把内存空间的值置空,最后调用deallocate回收这段内存空间。接着我们看个示例,如下图:
上图中我们注意到一点,如果我们初始化了两次指针内存,那我们内存空间相应的要置空两次。还有一种初始化指针的方式,我们看示例,如下图:
指针读取Mach-O中的属性名称
我们之前已经通过MachOView分析过如何获取属性名称在Mach-O文件中的值,接下来我们用代码来实现下,如下图:
首先获取
Mach-O文件中的内存地址,通过getsectdata()方法获取,可以看到地址是0x3f30,把执行文件放进MachOView观察,内存地址也是0x3F30,如下图:
接下来我们还要获取当前程序运行的基地址,而且我们还要减去虚拟基地址,我们通过
_dyld_get_image_header(0)来获取下Mach-O文件中header的起始地址,即我们之前讲的当前程序运行的地址:
拿到程序运行基地址后,我们先放着,接着获取当前程序的虚拟内存地址和偏移量,那它放在
LG_SEGMENT_64(_LINKEDIT)中,我们看下MachOView中查看如下图:
我们用代码获取就是通过
getsegbyname("__LINKEDIT"),返回的是一个unsafePointer<segment_command_64>泛型指针,它里面就包含了虚拟内存地址和偏移量,如下图:
下图我们可以看到
unsafePointer<segment_command_64>这个结构体指针:
我们接着进入
seg_command_64结构体中,就可以清晰看到它的所有成员,包括vmaddr和fileoff:
我们用虚拟内存地址(vmaddr)减去偏移量(fileoff)才是当前程序链接的基地址(虚拟内存基地址),声明一个变量
linkBaseAddress来接收计算结果,我们看代码如下图:
我们开始通过
getsectdata()获取的内存地址是带了虚拟内存基地址的,所以这时候要减去,当然直接减是无法减的,我们需要bitPattern()函数做数据类型转换,如下:
我们计算结果是
16164,转成16进制也就是0x3F24,我们编译程序打开MachOView查看同样也是0x3F24(再次编译时,这个地址会改变,后面看到的地址可能是下一次编译的结果),如下:
接着我们获取DataLO那里4字节的地址,通过当前程序运行的基地址加上刚刚计算到的
offset(0x3F24),就是DataLO在程序运行中真正的内存地址,如下:
程序运行基地址加
offset计算出dataLoAddress内存地址代码如下:
接着调用
withUnsafePointer(to:)把它转化为指针类型再返回回来当前指针指向的内存信息,如下图示例:
可以看到当前计算的结果是
4294967136,转换为16进制就是0xFFFFFF60,Mach-O文件查看dataLo结果也是这个值,如下图:
那程序运行的地址
offset加上dataLo的值减去虚拟内存基地址linkBaseAddress就是descriptor在Mach-O文件中的内存偏移地址typeDesOffset,之后偏移地址typeDesOffset加上程序运行的基地址mhHeaderPtr_IntRepresention(headerPtr转换而来方便计算)就是descriptor在程序运行中的真正内存地址,代码实现如下图:
找到
descriptor真正的内存地址后,它其实指向的就是TargetDescriptor,我们直接把它转换为TargetDescriptor,然后通过poitee拿到具体的数据类型,如下:
若我们要获取类名
name(CTPerson),首先拿到TargetClassDescriptor下name的地址信息,加上descriptor的偏移地址typeDesOffset,再加上8个字节的偏移,就是name在Mach-O文件中的偏移地址,然后加上程序运行的基地址mhHeaderPtr_IntRepresention就是name在程序运行中真正的内存地址,最后把nameAddress转换为CChar即字符串类型,打印输出:
那么接下来我们也很容易找到属性描述文件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:)用于更改内存绑定的类型,若当前还没有类型绑定,则首次绑定为该类型;否则重新绑定该类型,并且内存中中所有的值都会变为该类型。
withMemoryRebound(to:capacity:body:)当我们给外部函数传递参数时,不免会有些数据类型上的差距。如果我们进行类型转换,必然要来回复制数据,这个时候我们可以使用这个API来临时更改内存绑定类型。
内存管理
swift中使用自动引用计数(ARC)机制来追踪和管理内存。之前我们在分析对象内存布局的时候,已经知道前8个字节是metadata(结构体指针),紧接着的8个字节是refCount,如下图:
我们从图中看到
refCount的8个字节是0x0000000003,但是我们无法确定这个引用计数是多少,接下来就需要去源码中一探究竟了。首先我们找到refCount的定义,如下图:
接着找InlineRefCounts的定义:
最终找到了RefCountBitsT这个类,我们可以看到它的一个属性BitsType bits:
进入
Type可以看到它最终是个64位的位域信息:
那我们创建一个对象的时候,它的引用计数到底是多少呢,我们从对象开始创建的时候看下:
找到
initialized,发现它是个枚举值,最终传值是0,1,而这里的RefCountBits就是我们之前找到RefCountBitsT这个类:
接着我们找下RefCountBitsT这个类的初始化函数,如下图:
所以我们清楚看到,系统是把strongExtraCount(强引用计数)左移StrongExtraRefCountShift(33)位,unownedCount左移UnownedRefCountShift(1)位之后存储在64位上的。他们位置的存储分布如下图:
unownedRefCount无主引用存储的位置,isDeinitingMask是否析构的标识位,strongExtraRefCount强引用计数存储位。那引用计数是如何增加的呢,我们在源码中也可以看到相关操作,如下图:
可以看到它是通过
inc(1)左移StrongExtraRefCountShift(33)然后进行累加得到的。
弱引用
弱引用因不会对引用的实例保持强引用,因而不会阻止ARC释放被引用的失礼了。由于弱引用不会强保持对实例的引用,所以实例被释放后,弱引用仍有可能引用着这个实例,因此ARC会在实例被释放的时候自动地设置弱引用为nil。由于弱引用需要允许他们的值为nil,他们一定得是可选类型。
我们初始化一个弱引用变量后,系统是如何操作的呢,我们汇编调试后,如下:
系统会调用一个
weakInit方法,我们可以去源码中看下这个方法的调用:
从源码中我们可以看到,声明一个weak变量相当于定义了一个WeakRefrence对象:
根据nativeInit方法调用找到formWeakReference方法,发现最终它创建了一个散列表SideTable:
我们进入HeapObjectSideTableEntry这个类看到如下:
看到
InlineRefCountBits我们就知道它是RefCountBitsT类型,所以弱引用表跟强引用计数公用的一个类,只不过这里是继承自RefCountBitsT,如下图:
所以我们可以清楚知道
SideTable是一个类名为HeapObjectSideTableEntry的结构,里面也有RefCounts成员,内部是SideTableRefCountBits,其实就是原来的uint64加上一个存储弱引用的uint32_t。其实源码中有段注释也很明白,我们看下:
上图我们可以知道,swift中有两种引用计数,一种是
InlineRefCounts,一种是SideTableRefCounts,如果是强引用就是strong Rc + unowned RC + flags,如果是弱引用,就是strong RC + unowned RC + weak RC(32位) + flags。这里我们通过LLDB调试来观察下:
图中散列表地址是通过
0xc000000020c40810中63位和62标识位置为0,然后左移是3位得到的地址,因为在源码中,我们能够知道散列表的地址是被右移了三位的,如下图:
无主引用unowned
和弱引用类似,无主引用不会牢牢保持住引用的实例,但与弱引用不同,无主引用假定是永远有值的,而弱引用是可选值,可以为nil。一般如果强引用的双方生命周期没有任何关联,使用weak,例如delegate,若一个对象销毁,其中一个对象也跟着销毁,则使用owned。当然如果我们自己无法确定对象之间是否关联的时候,我们使用weak一定是没问题的,只不过会有点性能问题。owned性能更好,直接操作64位信息,weak要创建散列表,但是更安全。我们看一个例子:
这里
testClosure和p的声明周期就是关联的,p如果释放了,那自然testClosure就也释放了,所以使用owned是没问题的。
闭包捕获列表
默认情况下,闭包表达式从周围的范围捕获常量和变量,并强引用这些值。你可以使用捕获列表来显示控制如何在闭包中捕获值。 捕获列表中的值是通过找当前上下文中的同名变量,然后用的字面量的值来初始化一个let常量,如下图:
上图中捕获列表的age值就是10,下面age值变更为11,并不会影响捕获到的值,打印结果还是10。创建闭包时,内部作用域中的age会用外部作用域中age的值进行初始化,但是他们的值未以任何特殊方式连接,这意味着更改外部作用域中的age的值不会影响内部作用域中的age的值。
当然我们如果没有显示声明捕获列表,那么闭包在捕获值类型的时候,是调用的时候进行捕获的,如下:
循环引用
一般我们在使用闭包的时候,多会碰到解决循环引用的问题,我们大多时候是把一个对象变为弱引用来打破循环,但是闭包内部我们一般会短暂的延长该对象的生命周期,swift中有两种办法: