一、为什么说指针不安全
- 比如我们在创建一个对象的时候,是需要在堆分配内存空间的。但是这个内存空间的声明周期是有限的,也就意味这如果我们使用指针指向这块内存空间,如果当前内存空间的生命周期到了(引用计数为0),那么我们当前的指针就变成未定义的行为了。
- 我们创建的内存空间是有边界的,比如我们创建一个大小为10的数组,这个时候我们通过指针访问到了
index = 11
的位置,这个时候就越界了,访问了一个未知的空间。 - 指针类型与内存的值类型不一致,也是不安全的。
二、指针
Swift 中的指针分为两类
typed pointer
: 指定数据类型指针,即UnsafePointer<T>
,其中 T 表示泛型
raw pointer
: 未指定数据类型的指针(原生指针) ,即UnsafeRawPointer
与 OC 中指针的对比:
OC | Swift | 释义 |
---|---|---|
const T * | unsafePointer<T> | 指针及所指向的内容都不可变 |
T * | unsafeMutablePointer | 指针及所指向的内容都可变 |
const void * | unsafeRawPointer | 指针指向的内存区域未定 |
void * | unsafeMutableRawPointer | 同上 |
2.1 原生指针的使用
我们一起来看一下如何使用raw pointer
来存储4个整形的数据,我们这里使用UnsafeMutableRawPointer
:
// 开辟内存,分配32字节大小的空间(Int占8字节),对齐方式是8字节对齐
let p = UnsafeMutableRawPointer.allocate(byteCount: 32, alignment: 8)
// 存值
for i in 0..<4 {
// 指定当前移动的步数,即i * 8
p.advanced(by: i * 8).storeBytes(of: i + 1, as: Int.self)
}
//取值
for i in 0..<4 {
//p是当前内存的首地址,通过内存平移来获取值
let value = p.load(fromByteOffset: i * 8, as: Int.self)
print("index: \(i), value: \(value)")
}
//使用完需要手动释放
p.deallocate()
运行程序,查看执行结果,值可以正常打印:
移动步数时可以使用MemoryLayout
动态获取步幅,修改代码如下:
p.advanced(by: i * MemoryLayout<Int>.stride).storeBytes(of: i + 1, as: Int.self)
MemoryLayout<Int>.size
// size 指当前类型的实际大小
MemoryLayout<Int>.stride
// stride 翻译过来是步幅,这里可以认为是内存对齐之后的大小
MemoryLayout<Int>.alignment
// alignment 指的是内存对齐的方式,是1字节对齐,还是4字节对齐
运行程序,仍然可以正常打印:
2.2 类型指针的使用
我们还是通过示例来介绍type pointer
,我们获取基本数据类型
的地址
可以通过withUnsafePointer(to:)
方法:
var age = 18
let p = withUnsafePointer(to: &age) { ptr in
return ptr
}
print(p)
输出结果:0x0000000100008058
2.2.1 如何访问指针指向的值
我们可以通过指针的pointee属性
访问变量值
:
var age = 18
let p = withUnsafePointer(to: &age){$0}
print(p.pointee)
输出:18
2.2.2 如何修改指针指向的值
-
间接修改
var age = 18 age = withUnsafePointer(to: &age) { ptr in return ptr.pointee + 12 } print(age) 输出结果:30
-
通过
withUnsafeMutablePointer
var age = 18 withUnsafeMutablePointer(to: &age) { ptr in ptr.pointee += 12 } print(age) 输出结果:30
-
通过
allocate
创建UnsafeMutablePointer
var age = 18 //分配容量大小,为8字节 let ptr = UnsafeMutablePointer<Int>.allocate(capacity: 1) //初始化 ptr.initialize(to: age) ptr.deinitialize(count: 1) ptr.pointee += 12 print(ptr.pointee) //释放 ptr.deallocate() 输出结果:30
通过
allocate
创建UnsafeMutablePointer
,需要注意以下几点initialize
与deinitialize
需成对使用deinitialize
中的count
与申请时的capacity
需要一致
- 使用完后必须
deallocate
2.2.3 通过指针访问结构体对象
先定义一个结构体
struct SSLTeacher {
var age = 18
var height = 1.85
}
var t = SSLTeacher()
然后使用UnsafeMutablePointer
创建指针,并通过 3 种方式访问结构体对象 t:
// 分配2个 SSLTeacher 大小的空间
let ptr = UnsafeMutablePointer<SSLTeacher>.allocate(capacity: 2)
// 初始化第一个空间
ptr.initialize(to: SSLTeacher())
// 移动,初始化第2个空间
ptr[1] = SSLTeacher(age: 20, height: 1.75)
//访问方式一 下标访问
print(ptr[0])
print(ptr[1])
//访问方式二 内存平移
print(ptr.pointee)
print((ptr+1).pointee)
//访问方式三 successor()
print(ptr.pointee)
//successor 往前移动
print(ptr.successor().pointee)
//必须和分配是一致的
ptr.deinitialize(count: 2)
//释放
ptr.deallocate()
运行程序,查看执行结果:
2.3 Macho 指针操作案例
接下来通过案例来熟悉指针的操作,我们用指针来读取 Macho 中的属性名称
、类名
、vTable 中的方法
。
2.3.1 获取 classDescriptor 指针
先通过指针操作获取 classDescriptor
指针,代码如下:
class SSLTeacher{
var age: Int = 18
var name: String = "SSL"
}
// 当前程序运行地址
var mhHeaderPtr = _dyld_get_image_header(0)
// 16进制转化为10进制
let mhHeaderPtr_IntRepresentation = UInt64(bitPattern: Int64(Int(bitPattern: mhHeaderPtr)))
// 获取虚拟内存基地址
var setCommond64Ptr = getsegbyname("__LINKEDIT")
var linkBaseAddress: UInt64 = 0
if let vmaddr = setCommond64Ptr?.pointee.vmaddr, let fileOff = setCommond64Ptr?.pointee.fileoff{
linkBaseAddress = vmaddr - fileOff
}
//__swift5_types section 的 pFile 地址
var size: UInt = 0
var ptr = getsectdata("__TEXT", "__swift5_types", &size)
// __swift5_types section 的 pFile 在 Macho 中的地址
var offset: UInt64 = 0
if let unwrappedPtr = ptr{
let intRepresentation = UInt64(bitPattern: Int64(Int(bitPattern: unwrappedPtr)))
offset = intRepresentation - linkBaseAddress
}
// DataLO 的内存地址
var dataLoAddress = mhHeaderPtr_IntRepresentation + offset
// DataLO 指针
var dataLoAddressPtr = withUnsafePointer(to: &dataLoAddress){return $0}
// DataLO 中存储的内容
var dataLoContent = UnsafePointer<UInt32>.init(bitPattern: Int(exactly: dataLoAddress) ?? 0)?.pointee
// descriptor 在Macho 中的地址
let typeDescOffset = UInt64(dataLoContent!) + offset - linkBaseAddress
// descriptor 地址
var typeDescAddress = typeDescOffset + mhHeaderPtr_IntRepresentation
//print(typeDescAddress)
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 methods: UInt32
}
// 得到 descriptor 指针
let classDescriptor = UnsafePointer<TargetClassDescriptor>.init(bitPattern: Int(exactly: typeDescAddress) ?? 0)?.pointee
// 打印
print(classDescriptor)
// ********** 输出结果 **********
Optional(SwiftTest.TargetClassDescriptor(flags: ...))
- VM Address:Virtual Memory Address,段的虚拟内存地址,在内存中的位置
- VM Size:Virtual Memory Size,段的虚拟内存大小,占用多少内存
- File Offset:段在虚拟内存中的偏移量
- File Size:段在虚拟内存中的大小
- Address Space Layout Random,地址空间布局随机化,是一种针对缓冲区溢出的安全保护技术,通过对堆、栈、共享库映射等线性区布局的随机化,通过增加攻击者预测目的地址的难度,防止攻击者指针定位攻击代码位置,达到阻止溢出攻击的一种技术
- 相关 Macho :
2.3.2 获取 类名
接上面代码,添加如下代码打印类名:
if let name = classDescriptor?.name{
let nameOffset = Int64(name) + Int64(typeDescOffset) + 8
let nameAddress = nameOffset + Int64(mhHeaderPtr_IntRepresentation)
if let cChar = UnsafePointer<CChar>.init(bitPattern: Int(nameAddress)){
print(String(cString: cChar))
}
}
// ********** 输出结果 **********
SSLTeacher
2.3.3 获取 属性名
接上面代码,添加如下代码打印属性名:
// fieldDescriptor 属性地址
let filedDescriptorRelaticveAddress = typeDescOffset + 16 + mhHeaderPtr_IntRepresentation
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
}
// fieldDescriptor 偏移值
let fieldDescriptorOffset = UnsafePointer<UInt32>.init(bitPattern: Int(exactly: filedDescriptorRelaticveAddress) ?? 0)?.pointee
// fieldDescriptor 内存地址
let fieldDescriptorAddress = filedDescriptorRelaticveAddress + UInt64(fieldDescriptorOffset!)
// fieldDescriptor 指针
let fieldDescriptor = UnsafePointer<FieldDescriptor>.init(bitPattern: Int(exactly: fieldDescriptorAddress) ?? 0)?.pointee
// 获取属性名
for i in 0..<fieldDescriptor!.numFields{
// FieldRecord 的大小是12,所以步幅就是12
let stride: UInt64 = UInt64(i * 12)
// 移动 16 个字节到 fieldRecords
let fieldRecordAddress = fieldDescriptorAddress + stride + 16
let fieldNameRelactiveAddress = UInt64(2 * 4) + fieldRecordAddress - linkBaseAddress + mhHeaderPtr_IntRepresentation
let offset = UnsafePointer<UInt32>.init(bitPattern: Int(exactly: fieldNameRelactiveAddress) ?? 0)?.pointee
let fieldNameAddress = fieldNameRelactiveAddress + UInt64(offset!) - linkBaseAddress
if let cChar = UnsafePointer<CChar>.init(bitPattern: Int(fieldNameAddress)){
print(String(cString: cChar))
}
}
// ********** 输出结果 **********
age
name
2.3.4 获取 方法名
接上面代码,修改 SSLTeacher
类添加 3
个方法,并添加代码打印方法名:
@interface SSLTest : NSObject
+ (void)callImp:(IMP)imp;
@end
@implementation SSLTest
+ (void)callImp:(IMP)imp {
imp();
}
@end
class SSLTeacher {
func teach1() {
print("testch1");
}
func teach2() {
print("testch2");
}
func teach3() {
print("testch3");
}
}
// 方法个数
let numVTables = classDescriptor?.methods
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
// 获得vTable 地址
let vTableAddress = mhHeaderPtr_IntRepresentation + UInt64(vTableOffSet)
let vTable = UnsafePointer<VTable>.init(bitPattern: Int(exactly: vTableAddress) ?? 0)?.pointee
// 得到 imp ,因为加了两遍linkBaseAddress所以删除一个
let impAddress = vTableAddress + 4 + UInt64(vTable!.offset) - linkBaseAddress
SSLTest.callImp(IMP(bitPattern: UInt(impAddress))!);
}
// ********** 输出结果 **********
testch1
testch2
testch3
三、内存绑定
Swift 提供了三种不同的 API 来绑定/重新绑定指针:
3.1 assumingMemoryBound(to:)
这个 API 的作用是告诉编译器预期是什么类型(让编译器绕过类型检查,并没有发生实际类型的转换),看下面的例子:
- 上面的报错是说将
元组类型
的指针赋值给了Int 类型
指针,类型不匹配 - 但实际上元组是值类型,本质上这块内存空间中存放的就是 Int 类型的数据
- 下面通过
assumingMemoryBound(to:)
函数进行修改
3.2 bindMemory(to:capacity:)
用于更改内存绑定的类型,如果当前内存还没有类型绑定,则将首次绑定为该类型。否则重新绑定该类型,并且内存中所有的值都会变成该类型。
func testPointer(_ p: UnsafePointer<Int>) {
print(p[0])
print(p[1])
}
let tuple = (10,20)
withUnsafePointer(to: tuple){ (tupleStr: UnsafePointer<(Int, Int)>) in
testPointer(UnsafeRawPointer(tupleStr).bindMemory(to: Int.self, capacity: 1))
}
输出:
10
20
3.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)
}
四、强引用
Swift 中使用自动引用计数(ARC)机制来追踪和管理内存。
先添加如下代码:
class SSLTeacher {
var age: Int = 18
var name: String = "Kody"
}
var t = SSLTeacher()
print(Unmanaged.passUnretained(t as AnyObject).toOpaque()) // 固定用法实例对象内存地址
NSLog("end")
断点调试,打印内存情况:
实例对象内存地址的前 16 个字节的后 8 个字节是存储引用计数的,这里的值是 0x3
,这个 0x3
是怎么来的呢,下面进行分析。
4.1 源码分析 refCounts
我们先找到引用计数的定义,打开源码在 HeapObject.h
中找到 refCounts
:
#define SWIFT_HEAPOBJECT_NON_OBJC_MEMBERS \
InlineRefCounts refCounts
refCounts
是 InlineRefCounts
类型的,查看 InlineRefCounts
的定义:
typedef RefCounts<InlineRefCountBits> InlineRefCounts;
typedef RefCounts<SideTableRefCountBits> SideTableRefCounts;
template <typename RefCountBits>
class RefCounts {
std::atomic<RefCountBits> refCounts;
...
}
InlineRefCounts
是 RefCounts<InlineRefCountBits>
的别名,RefCounts
是一个模版类,接收了 InlineRefCountBits
:
看下 InlineRefCountBits
的定义:
typedef RefCountBitsT<RefCountIsInline> InlineRefCountBits;
RefCountBitsT
又是一个模版类,RefCountIsInline
这个参数是 true 或者 false:
注意 bits
这个成员变量,后续的操作都是有关于这个成员变量,它是由RefCountBitsInt
中的 Type
来定义的。
点击 Type
查看:
当我们创建一个实例对象的时候,当前的引用计数是多少?
查看 refCounts
:
其实就是 RefCountBitsT
中的这个方法:
- strongExtraCount 就是 0,unownedCount 就是 1
- StrongExtraRefCountShift 是 33,PureSwiftDeallocShift 是 0,UnownedRefCountShift 是 1
- 所以:0 << 33 =
0
,1 << 0 =1
,1 << 1 =2
- 0 | 1 | 2 =
0x0000000000000003
,这样就解释了上面引用计数的地方是0x3
的原因
4.2 引用计数的内存分布
创建如下代码,打 3 个断点,断点处分别打印引用计数内存情况:
- 由上可以发现,当强引用为 1 时,因为左移了 33 位,所以引用计数存储在了高
33
位 - 当强引用为 2 时,因为左移了 33 位,所以引用计数存储在了高
34
位
引用计数内存分布图如下:
- UnownedRefCount:无主引用计数
- isDeinitingMask:是否正在析构
- StrongExtraRefCount:强应用计数
4.3 强引用计数增加操作
打开源码,找到强引用计数的相关增加函数:
通过上面的函数,可以看到强引用计数的增加方式是先将 1
左移 33
位,然后加到 bits
上,这也符合我们上面对引用计数内存分布的分析。
五、弱引用
看下面的代码,teacher 和 subject 会产生循环引用,在 Swift 中解决循环引用的方式有 弱引用
和无主引用
5.1 弱引用概述
class SSLTeacher {
var age: Int = 18
var name: String = "ssl"
var subject:SSLSubject?
}
class SSLSubject {
var subjectName: String
var subjectTeacher: SSLTeacher
init(_ subjectName: String, _ subjectTeacher: SSLTeacher) {
self.subjectName = subjectName
self.subjectTeacher = subjectTeacher
}
}
var teacher = SSLTeacher()
var subject = SSLSubject.init("数学",teacher)
teacher.subject = subject
弱引用
不会对其引用的实例保持强引用,因而不会阻止 ARC 释放被引用的实例。这个特性阻止了引用变为循环强引用。声明属性或者变量时,在前面加上 weak 关键字表明这是一个弱引用。
由于弱引用不会强保持对实例的引用,所以说实例被释放了弱引用仍旧引用着这个实例也是有可能的。因此,ARC 会在被引用的实例被释放时自动的设置弱引用为 nil。由于弱引用需要允许它们的值为 nil,它们一定得是可选类型。
weak 使用示例代码:
class SSLTeacher {
var age: Int = 18
var name: String = "ssl"
weak var subject:SSLSubject?
}
5.2 源码分析 SideTable引出
我们断点看一下,添加了 weak 关键字会调用什么函数
可以看到 swift_weakInit
函数被调用了,这个函数可以在 HeapObject.cpp 中可以找到
声明一个 weak 变量相当于定义了一个 WeakReference 对象,点击查看 nativeInit
到这里就可以发现,weak 本质上就是创建了一个 SideTable,函数中调用了 allocateSideTable
函数。
5.3 源码分析 SideTable
查看 allocateSideTable
:
先看下 HeapObjectSideTableEntry 和 InlineRefCounts 的对比关系:
查看 HeapObjectSideTableEntry
的定义:
HeapObjectSideTableEntry
通过 SideTableRefCounts 来存储引用计数,查看它的定义:
typedef RefCounts<InlineRefCountBits> InlineRefCounts;
typedef RefCounts<SideTableRefCountBits> SideTableRefCounts;
class alignas(sizeof(void*) * 2) SideTableRefCountBits : public RefCountBitsT<RefCountNotInline>
{
uint32_t weakBits;
...
}
可以看到,SideTableRefCounts
和 InlineRefCounts 共用一个模版类RefCounts<T>
,它们都继承自 RefCountBitsT,作为子类的 SideTableRefCounts
多了一个 weakBits 成员变量。
下面通过 lldb 调试,查看不同情况下引用计数的存储情况
0xc0000000200c437c
是弱引用后的值- 这里是弱引用的创建函数
- 把当前 side 对象的地址放到了 64 位的位域中
- 两个标识位,一个 62,一个 63,都为 1
- 将
0xc0000000200c437c
反向操作,得到散列表的内存地址0x100621BE0
- 通过 lldb 调试查看散列表的结构
0x0000000000000003
是强引用的值0x0000000000000002
是弱引用的值
六、无主引用
和弱引用类似,无主引用不会强引用实例。但是和弱引用有所不同,无主引用会假定实例是永远有值的。
当我们向下面这样去访问一个无主引用的时候,其实有点像访问一个野指针,因为总是假定有值的,所以这里就会发生程序的崩溃
根据苹果官方文档的建议,当我们知道两个对象的生命周期并不相关,那么我们必须使用 weak。相反,非强引用对象拥有和强引用对象同样或者更长的生命周期的话,则应该使用 unowned。
七、闭包的循环引用
7.1 Swift 中的闭包循环引用
在 Swift 中闭包一般会默认捕获外部的变量,看下面代码:
var age = 18
let closure = {
age += 1
}
closure()
print(age)
输出:19
从结果可以看出来,闭包内部对变量的修改将会改变外部原始变量的值。
那同样就会有一个问题,如果我们在 class 的内部定义一个闭包,当前闭包访问属性的过程中,就会对我们当前的实例对象进行捕获。
看下面的代码,deinit 中代码被打印,这里是实例被正常释放的情况:
class SSLTeacher {
var age: Int = 18
deinit {
print("SSLTeacher deinit")
}
}
func testARC() {
let t = SSLTeacher()
}
testARC()
输出:SSLTeacher deinit
再看下面的代码,teacher 和 closure 会产生循环引用,deinit 中的代码不会被打印:
class SSLTeacher {
var age: Int = 18
var testClosure:(() -> ())?
deinit {
print("SSLTeacher deinit")
}
}
func testARC() {
let t = SSLTeacher()
t.testClosure = {
t.age += 1
}
}
testARC()
析构函数没有被调用,说明 teacher 没有被释放,产生了循环引用。
7.2 解决循环引用
通过 weak
解决循环引用:
class SSLTeacher {
var age: Int = 18
var testClosure:(() -> ())?
deinit {
print("SSLTeacher deinit")
}
}
func testARC() {
let t = SSLTeacher()
t.testClosure = { [weak t] in
t!.age += 1
}
}
testARC()
输出:SSLTeacher deinit
可以看到通过 weak 的修饰,闭包中的内容可以正常打印,同时这里用 unowned 也是可以解决的:
func testARC() {
let t = SSLTeacher()
t.testClosure = { [unowned t] in
t.age += 1
}
}
testARC()
7.3 捕获列表
什么是捕获列表
,默认情况下,闭包表达式从其周围的范围捕获常量和变量,并强引用这些值。您可以使用捕获列表来显示控制如何在闭包中捕获值。
在参数列表前,捕获列表被写为用逗号括起来的表达式列表,并用方括号括起来。如果使用捕获列表,必须使用 in 关键字。
参数列表中的变量,是不进行强引用的,看下面示例代码:
var age = 1
var height = 0.0
let closure = { [age] in
print(age)
print(height)
}
age = 10
height = 1.85
closure() // 输出结果为 1 ,1.85