为了避免疏漏, 我从官方文档作了截图, 苹果官网文档1 , 文档2
本文概要
- 按照官方文档, 介绍
Swift
中的指针, 包括Typed Pointers
,Raw Pointers
,Memory Access
,Memory Layout
,Reference Counting
. - 打印 class 和 struct 对象地址
- 介绍通过
指针
, 如何为对象的属性设值.
第一部分: Swift中的指针
Swift中的指针分为两类, typed pointer
指定数据类型指针, raw pointer
未指定数据类型的指针(原生指针)
下面是C
和Swift
关于指针的对照表:
C Syntax | Swift Syntax | Note |
---|---|---|
const Type * |
UnsafePointer<Type> |
指针可变,指针指向的内存值不可变。 |
Type * |
UnsafeMutablePointer<Type> |
指针和指针指向的内存值均可变。 |
ClassType * const * |
UnsafePointer<ClassType> |
指针的指针:指针不可变,指针指向的类可变。 |
ClassType * __strong * |
UnsafeMutablePointer<ClassType> |
指针的指针:指针和指针指向的类均可变。 |
ClassType ** |
AutoreleasingUnsafeMutablePointer<Type> |
作为OC方法中的指针参数 |
const void * |
UnsafeRawPointer |
指针指向的内存区类型未定。 |
void * |
UnsafeMutableRawPointer |
同上 |
StructType * |
OpaquePointer |
C 语言中的一些自定义类型,Swift 中并未有相对应的类型。 |
int8_t a[] |
var x:[Int8] -> UnsafeBufferPointer |
Buffer 一词不难联想到数组 |
Typed pointers
UnsafePointer 与 UnsafeMutablePointer
UnsafePointer / UnsafeMutablePointer 实例引用的内存可以处于多种状态之一. 许多指针操作只能应用于内存处于特定状态. 这同样应用于 UnsafeRawPointer, UnsafeRawBufferPointer.
- untyped and uninitialized (未分配内存)
- typed and uninitialized (未初始化)
- typed and initialized (已初始化)
- 先前分配的内存可能已被解除分配,使现有指针引用未分配的内存 (未分配内存)
// 分配内存
let a = UnsafeMutablePointer<Int>.allocate(capacity: sizeof_sfntInstance)
// 初始化
a.initialize(to: 10)
// 设值
a.pointee = 10
print(a.pointee)
// 释放内存
a.deallocate(capacity: 1)
- 分配内存, 在指定内存容量时, 可以手动指定为
sizeof_sfntInstance
, 由系统决定实例对象占多大内存空间, 这里值 为4
个字节. - 对于
Int
,Float
,Double
这些基本数据类型, 可以不进行initialize
操作, 因为分配内存之后会有默认值 0
- 对于引用类型类型, 必须进行initialize操作, 才能在内存中写入值, 否则编译器报错.
- 每一个
Pointer
对象都有一个属性pointee
, 可以通过这个属性直接获取指针指向内存中的值. - 有分配内存, 最好在不需要的时候主动释放内存
将指针引用的内存作为不同的类型访问
- 将内存临时重新绑定到其他类型.
var uint8: UInt8 = 123
let unit8Pointer = UnsafeMutablePointer(&uint8)
unit8Pointer.withMemoryRebound(to: Int8.self, capacity: 8) {
$0.pointee // 123
}
- 将内存永久重新绑定到其他类型
这里我们用到指向内存的原始指针
let uint64Pointer = UnsafeRawPointer(unit8Pointer).bindMemory(to: UInt64.self, capacity: 1)
uint64Pointer.pointee // 123
- 为了访问不同类型的相同内存, 只需绑定类型, 和目标类型是普通类型即可
var uint64Num: UInt64 = 257
let rawPointer = UnsafeRawPointer(UnsafeMutablePointer(&uint64Num))
let fullInteger = rawPointer.load(as: UInt64.self) // 257
let firstByte = rawPointer.load(as: UInt8.self) // 1
这里需要解释一下, 为什么 257 在这里以 UIn8 类型访问是 1 呢.
UInt8 表示存储8个字节的无符号整数, , [0, 255]
Int8 表示存储8个字节的整数, 范围 [-128, 127]
UInt8: [0, 255] Int8: [-128, 127]
UInt16: [0, 65535], Int16: [-32768, 32767]
UInt32: [0, 4294967295], Int32: [-2147483648, 2147483647]
UInt64: [0 , 9223372036854775807],
Int64: [-9223372036854775808, 9223372036854775807]
257 (10) = 1 0000 0001 (2)
rawPointer 以 UInt8 类型加载数据 257 时, 只能加载到 1 , 超出8个字节范围的无法加载.
UnsafeBufferPointer 与 UnsafeMutableBufferPointer
- 用于连续存储在内存中的元素缓冲区, UnsafeBufferPointer 用于处理不可变的元素缓冲区, UnsafeMutableBufferPointer 用于处理可变的元素缓冲区.
- 这两者的实例是内存视图, 不拥有它引用的内存, 即 复制UnsafeBufferPointer (UnsafeMutableBufferPointer) 类型的值不会复制存储在基础内存中的实例.
- BufferPointer / UnsafeMutableBufferPointer 实现了Collection ,因此可以直接使用Collection中的各种方法来遍历操作数据,filter,map...,Buffer可以实现对一块连续存在空间进行操作.
遍历数组
var array = [1,2,3,4]
let ptr = UnsafeBufferPointer(start: &array, count: sizeof_sfntInstance)
ptr.forEach {
print("\($0)") // 1 2 3 4
}
array.withUnsafeBufferPointer({ptr in
ptr.forEach({
print("\($0)") // 1 2 3 4
})
})
Raw Pointers
UnsafeRawPointer 与 UnsafeRawBufferPointer
UnsafeRawPointer 用于 访问 非类型化数据的原始指针.
UnsafeRawBufferPointer 用于 访问 和 操作 非类型化数据的原始指针.
刚刚分配的原始内存处于未初始化的无类型状态。未初始化的内存必须先使用类型的值进行初始化,然后才能与任何类型的操作一起使用.
要将未初始化的内存绑定到类型而不初始化它, 使用bindMemory(to:count :)
方法, 此方法返回一个类型化指针, 以进一步对内存进行类型化访问.
var uint64Num: UInt64 = 257
let rawPointer = UnsafeRawPointer(UnsafeMutablePointer(&uint64Num))
let fullInteger = rawPointer.load(as: UInt64.self) // 257
let firstByte = rawPointer.load(as: UInt8.self) // 1
使用原始指针的指针算法在字节级执行
// 分配内存
let bytePointer = UnsafeMutableRawPointer.allocate(byteCount: 4, alignment: 1)
// 将给定值的字节存储在指定偏移量的原始内存中
bytePointer.storeBytes(of: 0xFFFF_FFFF, as: UInt32.self)
// 从 bytesPointer 引用的内存中加载值
let x = bytePointer.load(as: UInt8.self)// 255
// 从最后两个分配的字节加载一个值
let offsetPointer = bytePointer + 2 // bytePointer 偏移 2 个字节
let y = offsetPointer.load(as: UInt16.self) // 65535
// 将值 0xFFFF_FFFF 存储到四个新分配的字节中,
// 将第一个字节作为UInt8实例加载
// 将第三个和第四个字节作为UInt16实例加载
// 释放分配的内存
bytePointer.deallocate()
需要说明的是
0xFFFF_FFFF = 1111 1111__1111 1111__1111 1111__1111 1111
UnsafeRawBufferPointer 与 UnsafeMutableRawBufferPointer
- UnsafeRawBufferPointer / UnsafeMutableRawBufferPointer 实例是内存区域中原始字节的视图.
- 内存中的每个字节都被视为一个UInt8值, 与该内存中保存的值的类型无关
- 通过原始缓冲区从内存中读取是一种无类型操作, UnsafeMutableRawBufferPointer 实例可以写入内存, UnsafeRawBufferPointer 实例不可以
要通过类型化操作访问底层内存,必须将内存绑定到一个简单的类型.
Memory Access
- withUnsafePointer(to:_:)
- withUnsafeMutablePointers(_:)
- withUnsafeBytes(_:)
- withUnsafeMutableBytes(_:)
这是 Swift 提供的几个用于处于指针的方法, 共同点在于如果需要返回值供外界使用, 直接 return, 否则, 无需 return.
var a = 0
a = withUnsafePointer(to: &a, { (ptr) in
return ptr.pointee + 2
// 此时, 会新开辟空间, 令a指向新地址, 值为2,
})
// 修改指针指向的内存值
var a = 42
withUnsafeMutablePointer(to: &a) {
$0.pointee += 100 // 未开辟新的内存空间, 直接修改a所指向的内存值
}
print(a) // 142
// 同理
var arr = [1, 2, 3]
withUnsafeMutablePointer(to: &arr) {ptr in
ptr.pointee[0] = 10
}
print(arr) // [10, 2, 3]
Buffer pointer
arr.withUnsafeBufferPointer({ptr in
ptr.forEach({
print("\($0)") // 1 2 3
})
})
// 修改内存值
array.withUnsafeMutableBufferPointer { mptr in
mptr[0] = 100
}
ptr.forEach {
print("\($0)") // 100 2 3
}
// 打印字符串
let str = "hello"
let strData = str.data(using: .ascii)
strData?.withUnsafeBytes({ (ptr: (UnsafePointer<Int8>)) in
print(ptr.pointee) // 104 = 'h'
})
// ASCII <==> String
let h_ASCII = UnicodeScalar("h")?.value // 104
let h = Character(UnicodeScalar(104)) // h
// 交换两个指针对应的内存值
var arr = [4, 5, 6]
var arr2 = [100, 101, 102]
swap(&arr, &arr2)
print(arr, arr2) // [100, 101, 102], [4, 5, 6]
Reference Counting
Unmanaged
Unmanaged 是一个 Structure, 用来管理Unmanaged 对象的引用, 类似于OC 中对象的引用计数管理对象内存.
打印 class 对象的地址
Swift中的单例
class Singleton {
static let instance = Singleton()
private init() {}
}
let instance = Singleton.instance
// 将 Singleton 对象引用 转化为 unmanaged 对象引用
let unmanagedObj = Unmanaged.passUnretained(instance as AnyObject)
// 将 unmanaged class reference 转化 为pointer
let ptr = unmanagedObj.toOpaque()
// 打印内存地址
print(ptr.debugDescription)
还有另外一种方法打印 class 对象地址
let instance = Singleton.instance
// 类型转换: 将 instance 引用转换为 Int 类型
let addressValue = unsafeBitCast(instance as AnyObject, to: Int.self)
// 转化为16进制字符串
print(String(addressValue,radix: 16))
用 struct 能创建单例对象吗? 像下面这样.
struct Person {
static let instance = Person()
private init() {}
}
测试一下
var p1 = Person.instance
var p2 = Person.instance
// 打印 struct 的内存地址
func address_struct(o: UnsafeRawPointer) -> String {
return String(Int(bitPattern: o), radix: 16)
}
print("p1: ", address_struct(o: &p1)) // p1: 1004e3670
print("p2: ", address_struct(o: &p2)) // p2: 1004e3675
很明显, 这两个地址值并不一样, 所以 struct 是不能创建单例的. 因为这是值语义, 每次都会创建一个新的实例, 并且采用的 写时复制 (copy on write
) 的机制进行优化.
有几点需要说明
- 以前是通过
String(unsafeBitCast(point, to: Int.self), radix: 16)
这种方式转化struct对象为内存地址, 但是这已经过时了. 现在采用String(Int(bitPattern: pointer), radix: 16)
- Person 对象是存储在栈空间, 而不是堆空间的.
- struct 是值传递, 不能创建单例, 每次调用都是一个新的对象, 并且存储在栈中, class 是值引用, 它可以创建单例. 并且存储在堆中.
Memory Layout
Memory Layout
MemoryLayout 是 一个 Enumeration 的, 它是一个类型的内存布局,描述其大小,步幅和对齐方式.
struct Person {
var name: String = "jack"
var age: Int = 18
var isBoy: Bool = true
var height: Double?
}
class PersonClass {
var name: String = "Lucy"
var age: Int = 18
var isBoy: Bool = false
var height: Double?
}
MemoryLayout<Person>.size // 41 = 16 + 8 + 1 + 16
MemoryLayout<Person>.stride // 48
MemoryLayout<Person>.alignment // 8
MemoryLayout<PersonClass>.size // 8
MemoryLayout<PersonClass>.stride // 8
MemoryLayout<PersonClass>.alignment // 8
这里有几点说明一下:
String 类型占 16 个字节, Int 类型占 8 个字节, Bool 类型占 1 个字节, Double 类型占 8 个字节, 由于可选类型占 1 个字节, 以及内存对齐的限制, 导致增加了8个字节的存储空间, 最终占16 个字节.
size 指 Person 实例连续内存占用, stride 指存储在连续内存或Array 中时, 从Person 的实例的开始到下一个实例的开始的字节数.
-
alignment, 默认内存对齐方式.
class 是引用类型,生成的实例分布在 Heap(堆) 内存区域上,在 Stack(栈)只存放着一个指向堆中实例的指针.
所以MemoryLayout<PersonClass>.size 才是 8, 这里是栈中存放的指针的大小.因为考虑到引用类型的动态性和 ARC 的原因,class 类型实例需要有一块单独区域存储类型信息和引用计数(meta数据).
通过指针为属性赋值
既然知道了类的实例在内存中的分布情况,
- 我们可以通过指向实例的起始指针,
- 找到每一个属性所对应的内存,
- 通过指针在对应内存中写入值.
为 struct Person 的 属性赋值
var personStruct = Person()
// 获得头部指针
let pStructHeadP = withUnsafeMutablePointer(to: &personStruct, {
return UnsafeMutableRawPointer($0).bindMemory(to: Int8.self, capacity: MemoryLayout<Person>.stride)
})
// 将 headPointer 转化为 rawPointer, 方便移位操作
let pStructHeadRawP = UnsafeMutableRawPointer(pStructHeadP)
// 每个属性在内存中的位置
let namePosition = 0
let agePosition = namePosition + MemoryLayout<String>.stride
let isBoyPosition = agePosition + MemoryLayout<Int>.stride
// 将内存临时重新绑定到其他类型进行访问.
let namePtr = pStructHeadRawP.advanced(by: 0).assumingMemoryBound(to: String.self)
let agePtr = pStructHeadRawP.advanced(by: agePosition).assumingMemoryBound(to: Int.self)
let isBoyPtr = pStructHeadRawP.advanced(by: isBoyPosition).assumingMemoryBound(to: Bool.self)
// 设置属性值
namePtr.pointee = "lily"
agePtr.pointee = 20
isBoyPtr.pointee = false
// 测试
personStruct.name // "lily"
personStruct.age // 20
personStruct.isBoy // false
为 PersonClass 的属性设置
var personClass = PersonClass()
// 获得头部指针
let pClassHeadRawP = Unmanaged.passUnretained(personClass as AnyObject).toOpaque()
// 获取每个属性在内存中的位置
let namePosition2 = 16
let agePosition2 = namePosition + MemoryLayout<String>.stride + 16
let isBoyPosition2 = agePosition + MemoryLayout<Int>.stride + 16
// 将内存临时重新绑定到其他类型进行访问.
let namePtr2 = pClassHeadRawP.advanced(by: namePosition2).assumingMemoryBound(to: String.self)
let agePtr2 = pClassHeadRawP.advanced(by: agePosition2).assumingMemoryBound(to: Int.self)
let isBoyPtr2 = pClassHeadRawP.advanced(by: isBoyPosition2).assumingMemoryBound(to: Bool.self)
// 设置属性值
namePtr2.pointee = "maris"
agePtr2.pointee = 38
isBoyPtr2.pointee = true
// 测试
personClass.name // "maris"
personClass.age // 38
personClass.isBoy // true
测试发现, 即使是为常量, 也是可以通过指针修改内存中的属性值.
其他
- 在上一篇中, 我们通过Runtime的方式, 获取到程序运行中加载的所有class, 用代理模式, 通过匹配遵循协议的代理类, 来判断是否执行代理方法, 在这个方法中, 执行目标方法
// 定义一个指针, 分配内存, 指针和指针指向的类均可变。
let types = UnsafeMutablePointer<AnyClass>.allocate(capacity: typeCount)
// 创建 AutoreleasingUnsafeMutablePointer
let autoreleasingTypes = AutoreleasingUnsafeMutablePointer<AnyClass>(types)
// 将原缓存区的数据拷贝到types所指向的那块内存
objc_getClassList(autoreleasingTypes, Int32(typeCount))
AutoreleasingUnsafeMutablePointer
这个指针通常是用来作为OC方法中的指针参数, 在Swift中调用.
参考
Swift Pointer 使用指南
Pointer In Swift
Swift内存赋值探索一: 理解对象在内存中的存储状态
Swift 对象内存模型探究(一)