1、指针
1.1 指针不安全
- 指针从内存的角度来说,是不安全的,当我们创建一个对象的时候,系统会在堆分配内存空间,但这个内存生命周期有限,也就是如果我们使用指针指向这个空间,当内存的生命周期结束(引用计数器为0)时,那么这个指针就成了未定义的行为了。
- 我们创建的内存空间是有边界的,⽐如我们创建⼀个⼤⼩为10的数组,这个时候我们通过指针访问 到了 index = 11 的位置,这个时候是不是就越界了,访问了⼀个未知的内存空间。
- 当指针类型与内存的值类型不一致时,那么这个指针不安全。
1.2 Swift 指针类型
Swift中指针分为两大类,typed pointer 指定数据类型指针,raw pointer 未指定数据类型的指针(原生指针)
| Swift | Object-C | 说明 |
|---|---|---|
| unsafePointer<T.> | const T* | 指针及其所指向的内存内容均不可变 |
| unsafeMutablePointer<T.> | T* | 指针及其所指向的内存内容均可变 |
| unsafeRawPointer | const void* | 指针指向的内存区域未定 |
| unsafeMutableRawPointer | void* | 同上 |
| unsafeBufferPointer<T.> | ||
| unsafeMutableBufferPointer<T.> | ||
| unsafeRawBufferPointer | ||
| unsafeMutableRawBufferPointer |
1.3 原始指针的使用 UnsafeMutableRowPointer
/*
byteCount:当前总的字节大小
alignment:对齐大小
store 方法存储当前的整形数值
load 方法读取内存中的值
MemoryLayout<Int>.size //实际大小
MemoryLayout<Int>.stride //步长大小
MemoryLayout<Int>.alignment //对齐大小
*/
let p = UnsafeMutableRawPointer.allocate(byteCount: 32, alignment: 8)
for i in 0..<4 {
//advanced(by:) 步长
//storeBytes(of: as:) 存储
p.advanced(by: i * MemoryLayout<Int>.stride).storeBytes(of: i, as: Int.self)
}
for i in 0..<4 {
let value = p.load(fromByteOffset: i * 8, as: Int.self)
print("index\(i),value:\(value)")
}
打印:
index0,value:0
index1,value:1
index2,value:2
index3,value:3
1.4 泛型指针(类型指针)的使用
相比起原始指针,它就是已经绑定到了具体的类型。
1.4.1 withUnsafePointer()与withUnsafeMutablePointer()
var age = 18
/*
通过withUnsafePointer方法,创建一个指针ptr指向age
ptr.pointee 访问这个指针引用的实例
*/
withUnsafePointer(to: &age) { ptr in
print(ptr.pointee)
}
/*
可以操作指针所指向的实例
*/
age = withUnsafePointer(to: &age) { ptr in
return ptr.pointee + 12
}
print("age:\(age)")
/*
withUnsafeMutablePointer 可直接修改指针指向的实例
*/
withUnsafeMutablePointer(to: &age, { ptr in
ptr.pointee += 10;
})
print("age:\(age)")
1.4.2 主要几个API
.allocate .initialize .assign .deinitialize .deallocate
var age = 10
//分配一块Int类型的内存空间
let tPtr = UnsafeMutablePointer<Int>.allocate(capacity: 1)
tPtr.initialize(to: age)
//通过pointee属性来访问实例
print(tPtr.pointee)
struct CCStruct{
var age: Int
var height: Double
}
//capacity: 要分配多少个CCStruct 就写多少
var tPtr = UnsafeMutablePointer<CCStruct>.allocate(capacity: 2)
tPtr[0] = CCStruct(age: 18, height: 180)
tPtr[1] = CCStruct(age: 20, height: 170)
//释放内存 分配与释放要成对出现
tPtr.deinitialize(count: 2)
tPtr.deallocate()
1.5 案例 - 指针读取Macho的属性名称
此案例目的是为了理解指针操作
import Foundation
class CTTeacher {
var age: Int = 18
var name:String = "CCT"
}
var size :UInt = 0
//获得maco文件的__swift5_types section 的pFile 也就是程序运行地址 0x0000000100003f24
var ptr = getsectdata("__TEXT", "__swift5_types", &size)
//print(ptr)
//获取当前程序运行地址 0x0000000100000000 macoheader的起始地址
var mhHeaderPtr = _dyld_get_image_header(0)
//计算链接的基地址 = 虚拟内存地址 - 偏移量
//这个里面就存了maco的内容
var setCommond64Ptr = getsegbyname("__LINKEDIT")
var linkBaseAddress: UInt64 = 0
if let vmaddr = setCommond64Ptr?.pointee.vmaddr, let fileOff = setCommond64Ptr?.pointee.fileoff {
linkBaseAddress = vmaddr - fileOff
}
//把程序运行地址ptr 转换成UInt64类型,然后与链接的基地址相减
var offset:UInt64 = 0
if let unwrappedPtr = ptr{
let intRepresentation = UInt64(bitPattern: Int64(Int(bitPattern: unwrappedPtr)))
offset = intRepresentation - linkBaseAddress
print(offset) //16164 -> 16进制0x3F24
}
//当前程序运行地址 转化为UInt64
let mhHeaderPtr_IntRepresentation = UInt64(bitPattern: Int64(Int(bitPattern: mhHeaderPtr)))
//当前程序运行地址 + 偏移量 = 属性的地址
var dataLoAddress = mhHeaderPtr_IntRepresentation + offset
//拿到属性地址指向的内容
//先把 地址 转换为指针类型
var dataLoAddressPtr = withUnsafePointer(to: &dataLoAddress){return $0}
print(dataLoAddressPtr)
//再通过指针拿到pointee,拿到指针指向的内容
var dataLoContent = UnsafePointer<UInt32>.init(bitPattern: Int(exactly: dataLoAddress) ?? 0)?.pointee
print(dataLoContent)
//拿到描述文件 在maco文件中的偏移信息
let typeDescOffset = UInt64(dataLoContent!) + offset - linkBaseAddress
//描述文件在内存中的真实地址 typeDescOffset + 程序运行地址
var typeDescAddress = typeDescOffset + mhHeaderPtr_IntRepresentation
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
}
//直接把 typeDescAddress 强转为 TargetClassDescriptor 这个结构体
let classDescriptor = UnsafePointer<TargetClassDescriptor>.init(bitPattern: Int(exactly: typeDescAddress) ?? 0)?.pointee
if let name = classDescriptor?.name{
let nameOffset = Int64(name) + Int64(typeDescOffset) + 8
print(nameOffset)
let nameAddress = nameOffset + Int64(mhHeaderPtr_IntRepresentation)
print(nameAddress)
if let cChar = UnsafePointer<CChar>.init(bitPattern: Int(nameAddress)){
print(String(cString: cChar))
}
}
let filedDescriptorRelaticveAddress = typeDescOffset + 16 + mhHeaderPtr_IntRepresentation
//print(filedDescriptorAddress)
/*
// 还原vtable
let numVTables = classDescriptor?.numMethods
//print(numVTables)
struct VTable{
var kind: UInt32
var offset: UInt32
}
for i in 0..<numVTables!{
let vTableOffset = Int(typeDescOffset) + MemoryLayout<TargetClassDescriptor>.size + Int(i) * MemoryLayout<VTable>.size
// print(vTableOffset)
//0x3B6C
//0x3B74
let vTableAddress = mhHeaderPtr_IntRepresentation + UInt64(vTableOffset)
let vtable = UnsafePointer<VTable>.init(bitPattern: Int(exactly: vTableAddress) ?? 0)?.pointee
let impAddress = vTableAddress + 4 + UInt64(vtable!.offset) - linkBaseAddress
//方法的名称 -- 符号
LGTest.callImp(IMP(bitPattern: UInt(impAddress))!)
}
// let t = LGTest()
*/
/*
// 还原filedDescriptor
let filedDescriptorRelaticveAddress = typeDescOffset + 16 + mhHeaderPtr_IntRepresentation
//print(filedDescriptorAddress)
struct FieldDescriptor {
var mangledTypeName: Int32
var superclass: Int32
var Kind: UInt16
var fieldRecordSize: UInt16
var numFields: UInt32
// var fieldRecords: [FieldRecord]
}
struct FieldRecord{
var Flags: UInt32
var mangledTypeName: Int32
var fieldName: UInt32
}
//print(filedDescriptorAddress)
//let fieldDescriptor = UnsafePointer<FieldDescriptor>.init(bitPattern: Int(exactly: filedDescriptorAddress) ?? 0)?.pointee
//print("end"
let fieldDescriptorOffset = UnsafePointer<UInt32>.init(bitPattern: Int(exactly: filedDescriptorRelaticveAddress) ?? 0)?.pointee
//print(fieldDescriptorOffset)
let fieldDescriptorAddress = filedDescriptorRelaticveAddress + UInt64(fieldDescriptorOffset!)
let fieldDescriptor = UnsafePointer<FieldDescriptor>.init(bitPattern: Int(exactly: fieldDescriptorAddress) ?? 0)?.pointee
for i in 0..<fieldDescriptor!.numFields{
let stride: UInt64 = UInt64(i * 12)
let fieldRecordAddress = fieldDescriptorAddress + stride + 16
// print(fieldRecordRelactiveAddress)
// let fieldRecord = UnsafePointer<FieldRecord>.init(bitPattern: Int(exactly: fieldRecordAddress) ?? 0)?.pointee
// print(fieldRecord)
let fieldNameRelactiveAddress = UInt64(2 * 4) + fieldRecordAddress - linkBaseAddress + mhHeaderPtr_IntRepresentation
let offset = UnsafePointer<UInt32>.init(bitPattern: Int(exactly: fieldNameRelactiveAddress) ?? 0)?.pointee
// print(offset)
let fieldNameAddress = fieldNameRelactiveAddress + UInt64(offset!) - linkBaseAddress
if let cChar = UnsafePointer<CChar>.init(bitPattern: Int(fieldNameAddress)){
print(String(cString: cChar))
}
}
*/
1.6、内存绑定
Swift提供了三种API来绑定、重绑定指针:
1.6.1、assumingMemoryBound(to:)
有些时候我们处理代码的过程中,只有原始指针(没有保留指针类型),但此刻对于处理代码的我们来
说明确知道指针的类型,我们就可以使⽤ assumingMemoryBound(to:) 来告诉编译器预期的类型。
(注意:这⾥只是让编译器绕过类型检查,并没有发⽣实际类型的转换)
func testPointer(_ p:UnsafePointer<Int>){
print(p[0])
print(p[1])
}
let tuple = (10, 20)
withUnsafePointer(to: tuple) { (tuplePtr: UnsafePointer<(Int, Int)>) in
testPointer(UnsafeRawPointer(tuplePtr).assumingMemoryBound(to: Int.self))
}
1.6.2、bindMemory(to: capacity:)
用于更改内存绑定的类型,如果当前内存还没有类型绑定,则将⾸次绑定为该类型;否则重新绑定该类 型,并且内存中所有的值都会变成该类型。(发生了实际类型转换)
func testPointer(_ p:UnsafePointer<Int>){
print(p[0])
print(p[1])
}
let tuple = (10, 20)
withUnsafePointer(to: tuple) { (tuplePtr: UnsafePointer<(Int, Int)>) in
testPointer(UnsafeRawPointer(tuplePtr).bindMemory(to: Int.self, capacity: 1))
}
1.6.3、withMemoryRebound(to: capacity: body:)
当我们在给外部函数传递参数时,不免会有⼀些数据类型上的差距。如果我们进⾏类型转换,必然要来 回复制数据;这个时候我们就可以使⽤withMemoryRebound(to: capacity: body:)来临时更改内存绑定类型。 目的是减少代码复杂度
func testPointer(_ p:UnsafePointer<Int8>){
}
let Uint8Ptr = UnsafePointer<UInt8>.init(bitPattern: 10)
Uint8Ptr?.withMemoryRebound(to: Int8.self, capacity: 1){ (int8Ptr: UnsafePointer<Int8>) in
testPointer(int8Ptr)
}
2、内存管理
2.1 引用计数源码追踪
Swift中使用自动引用计数ARC机制来追踪和管理内存
对象的内存布局,其中有8个字节是用来存储当前的引用计数的。
来看一下源代码
引用计数refCounts是InlineRefCounts类型,点进去看看:
是一个模板类,接受一个泛型参数InlineRefCountBits
typedef RefCounts<InlineRefCountBits> InlineRefCounts;
点击RefCounts进去看看:
这个RefCounts在操作的时候,本质上都是在操作传进来的泛型参数
所以refCounts是对引用计数的一个包装
再回头追踪一下InlineRefCounts:
它也是个模板函数:
也就是我们当前操作的引用计数是在操作
RefCountBitsT这个类,而这个类里面只有一个属性,就是BitsType bits,这个属性是由typedef typename RefCountBitsInt<refcountIsInline, sizeof(void*)>::Type BitsType定义的
再接着追踪
BitsType bits进去看:
看到这里,就能理解其实Swift与OC的引用计数 都是一个64位的位域信息
2.2 创建实例时,引用计数的变化
我们可以通过追踪实例创建的源码,来了解引用计数是如何设置进去的
从头开始:
进入
HeapObject
初始化赋值了一个
Initialized,点进去看看是啥:
是个枚举类,下面还进行了赋值0、1,这里出现的
RefCountBits,正是前面分析过的RefCountBitsT这个类,那么我们就看看RefCountBitsT的初始化函数:
能看到,传进来的
strongExtraCount是0,unownedCount是1,并且都进行了左移操作(strongExtraCount是左移了33位,unownedCount是左移1位),也就是说通过左移,将“强引用计数”和“无主引用计数”存进了64位当中。
通过一个案例来分析一下:
情况一:仅创建对象
class HTPerson {
var age = 10
var name = "ht"
}
var t = HTPerson()
//打印对象内存地址
print(Unmanaged.passUnretained(t as AnyObject).toOpaque())
print("end")
通过x/4g 对象地址来查看内存内容
**x/4g 0x100737960**
0x100737960: 0x0000000100008180 0x0000000000000002
0x100737970: 0x0000000000000001 0x0000000000544343
得到0x0000000000000002,也就是在十六进制中,unownedCount左移1位
情况二:进行一次强引用
class HTPerson {
var age = 10
var name = "ht"
}
var t = HTPerson()
//打印对象内存地址
var t1 = t
print(Unmanaged.passUnretained(t as AnyObject).toOpaque())
var t2 = t
print(Unmanaged.passUnretained(t as AnyObject).toOpaque())
print("end")
在swift源代码中可以追踪到:
swift_retain(heapObject)->refCounts.increment(1)->incrementStrongExtraRefCount(inc)->强引用计数+1并且左移33位
在汇编层也能看到本质是调用swift_retain进行引用计数操作
在t1处打断点:
t的强引用计数和无主引用计数都是1
在t2处打断点:
t的强引用计数为2,无主引用计数1
解释一下最后面为什么是3:
在16进制中第0位是用来标识delloc的,引用计数是从1开始算的,这个跟xcode版本有关,xcode11会初始化为2
2.3 CGGetRetainCount() 引用计数统计方法
CFGetRetainCount()用于统计对象的引用计数,它在执行前,会对对象进行strong_retain操作,执行后,完成release_value操作。所以在Swift中使用CFGetRetainCount()得到的强引用计数会比实际的引用计数多1
- 在lldb中使用p和po命令打印对象,,会使
引用计数+1,影响CFGetRetainCount的结果(p打印一次或多次,x/4g在内存信息中可看到引用计数的明显变化)
代码试验:
class HTPerson {
var age = 10
var name = "ht"
}
var t = HTPerson()
print("end")
看一下SIL: 没有retain、release
然后来个CGGetRetainCount()打印
class HTPerson {
var age = 10
var name = "ht"
}
var t = HTPerson()
print("end")
再看下SIL:CGGetRetainCount()执行之前 调用了strong_retain,执行结束了调用release_value
3、循环引用
来个循环引用的例子:
class CTTeacher{
var age: Int = 18
var name: String = "Kody"
var subject: LGSubject?
}
class CTSubject{
var subjectName: String
var subjectTeacher: LGTeacher
init(_ subjectName: String, _ subjectTeacher: LGTeacher) {
self.subjectName = subjectName
self.subjectTeacher = subjectTeacher
}
}
var t = CTTeacher()
var subject = CTSubject.init("XXXXX", t)
t.subject = subject
在Swift中有两种方式解决循环引用的问题,一种是弱引用weak,一种是无主引用unowned
3.1 weak 弱引用
-
声明属性或者变量时,由于弱引⽤不会强保持对实例的引⽤,所以实例被释放之后弱引⽤有可能仍旧引⽤着这个实例,因此ARC会在实例被释放后⾃动地设置弱引⽤为nil 。
-
由于弱引⽤需要允许它们的值为 nil ,所以它们⼀定得是
可选类型。 -
weak修饰的对象,不影响强引用计数,不影响对象的释放。
-
允许对象为nil,当对象为nil时,后面的语句不执行
swift中的弱引用与OC不同的是:
-
OC:
弱引用计数是存放在全局维护的散列表中,isa中会记录是否使用了散列表。
在引用计数为0时,自动触发dealloc,会检查并清空当前对象的散列表计数。 -
swift:
弱引用计数也是存放在散列表中,但这个散列表不是全局的。- 如果对象
没有使用weak弱引用,就是单纯的HeapObject对象,没有散列表。 - 如果使用
weak弱引用,会变为WeakReference对象。这是一个Optionl(可空对象)。其结构中自带散列表计数区域。
但swift的散列表与refCount无关联。当强引用计数为0时,不会触发散列表的清空。而是在下次访问发现当前对象不存在(为nil)时,会清空散列表计数。
- 如果对象
看一段代码:
class CTTeacher{
var age: Int = 18
var name: String = "Kody"
}
weak var t = CTTeacher()
从汇编的来看,调用了
swift_weakInit符号
在Swift源码中搜索
swift_weakInit可以看出,声明一个
weak变量,相当于定义了一个weakRefrence对象,本质上就是创建了一个散列表![]()
![]()
来看一下散列表的初始化
allocateSideTable()
初始化的是HeapObjectSideTableEntry这个玩意儿
那就继续看一下
HeapObjectSideTableEntry这是个啥玩意儿全局搜一下,可以找到这个:
在Swift中,存在着两种引用计数,第一种就是一般的引用计数,也就是上面分析过的
InlineRefCounts,第二种是弱引用,也就是HeapObjectSideTableEntry如果是一般的引用计数(强引用)
InlineRefCounts里边儿就是strong RC+unowned RC+flags如果是弱引用 那么
InlineRefCounts里边儿就是HeapObjectSideTableEntry这个sidetable里面存在着
strong RC+unowned RC+weak RC+flags
接着看
HeapObjectSideTableEntry:![]()
object是引用的对象,refCounts是引用计数
refCounts是个SideTableRefCounts类型,接着看:
可以看到
SideTableRefCounts是SideTableRefCountBits类型,而它跟InlineRefCountBits公用一个模板类RefCountBitsT
那既然
SideTableRefCountBits继承自RefCountBitsT,那么RefCountBitsT的64位信息也就被继承下来了。
- 常规对象与弱引用对象的区别:
除了继承下来的信息,它还有一个存储弱引用计数的weakBits,所以当我们声明了weak变量之后,它会存储一下内容,存的啥呢?
来个例子:
class CTTeacher{ var age: Int = 18 var name: String = "Kody" } var t = CTTeacher() print(Unmanaged.passUnretained(t as AnyObject).toOpaque()) //weak修饰 weak var t1 = t print("end")
可以看到,weak修饰的t1,变成了optional可选值
- (weak修饰的对象,不会改变原来对象的引用计数,只是多一层可空的状态)
断点到t和t1,观察
x/4g打印的地址变化t是正常的引用,引用计数是0x3,而t1的断点是weak,引用计数就变了
那我们来研究一下这个内存地址,把它放到计算器里边儿:
回到sidetable生成的代码:
进
InlineRefCountBits(side)初始化方法:
side的地址向右移动了3位,然后
UseSlowRCShift和SideTableMarkShift都标记成1,也就是第62位和63位会标识成1,所以我们手动把62位和63位改为0,然后左移3位:
最终得到的这个地址,用
x/8g打一下:
它的前8个字节,存储了heapObject,第三个8字节和第四个8字节,分别存储了
strongCount和weakCount,所以这就是经过weak修饰之后,引用计数器的变化过程,本质上就是创建了一个SideTable。
3.2 Unowned 无主引用
无主引用假定永远有值,不能nil。
必须确保捕获对象的生命周期可控(当对象被释放时,会存在产生野指针,有crash风险)
weak和unowned选择:
-
如果两个对象的⽣命周期完全和对⽅没关系(其中⼀⽅什么时候赋值为nil,对对⽅都没影响),请⽤ weak
-
如果你的代码能确保:其中⼀个对象销毁,另⼀个对象也要跟着销毁,这时候,可以(谨慎)⽤ unowned
3.3 闭包循环引用
1、闭包内部自动捕获变量,修改它会改变闭包外变量的值
var age = 18
let closure = {
age += 1
}
closure()
print(age)
打印:
自动捕获了
外部变量age,闭包内修改age值,影响了外部age的值
2、手动捕获外界变量、值类型对象,会copy一份,不可修改,也不影响外部值
3、如果闭包直接持有外部对象,容易造成循环引用 例子:
class CTTeacher{
var age = 18
var complateCallBack:(()->())?
deinit{
print("deinit")
}
}
func test(){
let t = CTTeacher()
t.complateCallBack = {
t.age += 1
}
print("end")
}
test()
- 打印结果:
t对象持有complateCallBack属性,complateCallBack闭包中捕获t这个外部变量,并且强引用计数+1,造成循环引用
捕获列表
-
闭包表达式从其周围的范围捕获常量和变量,并强引⽤这些值。
-
创建闭包时,将初始化捕获列表中的条⽬。对于捕获列表中的每个条⽬,将常量初始化为在周围范围内具有相同名称的常量或变量的值。
var age = 0
var height = 0.0
let closure = { [age] in //手动捕获,这是个常量age,初始化为外部同名age的值
print(age)
print(height) //自动捕获 外部变量 内外修改都会产生影响
}
age = 10
height = 1.85
closure() //输出结果为0, 1.85
打破循环引用:
【方式一】weak弱引用
【方式二】unowned无主引用
总结
Swift闭包会自动捕获外界变量,并影响引用计数,有循环引用风险
手动捕获变量(在
[]内写相同的变量名):值类型(var let):会copy一份数据,为只读属性,不可修改,与原始值互不影响,
引用类型(对象):会操作对象地址,会影响引用计数,也会影响该对象的内容,有循环引用的风险。
打破循环引用的方式
1、weak弱引用
2、unowned无主引用