Swift系列二十五 - 内存管理(二)

930 阅读7分钟

谈内存必然离不开指针的概念,指针既是难点也是重点。

一、指针简介

Swift中也有专门的指针类型,这些都被定性为Unsafe(不安全的),常见的有以下4种类型:

  • UnsafePointer<Pointee>:类似于const Pointee *(只读的泛型指针)
  • UnsafeMutablePointer<Pointee>:类似于Pointee *(可读可写的泛型指针)
  • UnsafeRawPointer:类似于const void *(只读的原始类型指针)
  • UnsafeMutableRawPointer:类似于void *(可读可写的原始类型指针)

示例代码一:

var age = 10
func test1(_ ptr: UnsafeMutablePointer<Int>) {
    ptr.pointee += 10
    print("test1 \(ptr.pointee)")
}
test1(&age) // 输出:test1 20

func test2(_ ptr: UnsafePointer<Int>) {
    print("test2 \(ptr.pointee)")
}
test2(&age) // 输出:test2 20

func test3(_ ptr: UnsafeMutableRawPointer) {
    ptr.storeBytes(of: 30, as: Int.self)
    print("test3 \(ptr.load(as: Int.self))")
}
test3(&age) // 输出:test3 30

func test4(_ ptr: UnsafeRawPointer) {
    print("test4 \(ptr.load(as: Int.self))")
}
test4(&age) // 输出:test4 30

泛型指针可以通过指针变量属性pointee读写内存。

原始指针通过load实例方法读取内存数据,参数as传入创建的实例类型。

原始指针通过storeBytes实例方法写入数据,参数as传入存储数据的类型,参数of`传入数据。

二、获取指针

2.1. 获取指向某个变量的指针

示例代码:

var age = 10
func test(_ ptr: UnsafePointer<Int>) {
    print(ptr.pointee)
}
test(&age)

调用函数的时候传参&age,意味着传入的是指针地址,通过这个地址可以直接获取age的内存。能不能直接定义变量的时候就定义呢?

直接定义一个指针变量指向了age

var ptr: UnsafePointer<Int> = &age

明显不行,编译器直接报错了:

可以使用下面的方法获取指针变量:

@inlinable public func withUnsafePointer<T, Result>(to value: inout T, _ body: (UnsafePointer<T>) throws -> Result) rethrows -> Result

使用:

var ptr = withUnsafePointer(to: &age) { $0 }
print(ptr.pointee) // 输出:10

发现ptr就是Int类型指针:

withUnsafePointer函数的第一个参数是传入变量的地址,第二个参数是闭包,闭包的参数其实是函数的第一个参数,返回值是一个泛型(传参是什么类型,返回值就是什么类型)。

模仿实现withUnsafePointer代码:

func withUnsafePointer<Result, T>(to: UnsafePointer<T>, body: (UnsafePointer<T>) -> Result) -> Result {
    body(to)
}

withUnsafePointerwithUnsafeMutablePointer返回的都是泛型指针,通过修改尾随闭包的返回值类型可以间接修改这两个函数的返回值类型。

UnsafeRawPointerUnsafeMutableRawPointer都有各自的初始化器。

var ptr = withUnsafePointer(to: &age) { UnsafeRawPointer($0) }
// ptr是UnsafeRawPointer类型

var ptr2 = withUnsafeMutablePointer(to: &age) { UnsafeMutableRawPointer($0) }
// ptr2是UnsafeMutableRawPointer类型

证明获取的是指针:

var age = 10
var ptr = withUnsafePointer(to: &age) { $0 }
print(ptr) // 输出:0x000000010000c340

通过计算得出:0x100001d2e + 0xa612 = 0x10000C340,和上面指针变量ptr打印的地址值一样,也就是age保存的地址。

2.2. 获得指向堆空间实例的指针

示例代码:

class Person {
    var age: Int
    init(age: Int) {
        self.age = age
    }
}

var person = Person(age: 10)

var ptr = withUnsafePointer(to: &person) { $0 }
print(ptr.pointee.age) // 输出:10

思考:ptr存储的是什么?存储的是变量person的地址值还是堆空间Person对象的地址值?

print(ptr) // 输出:0x000000010000c4c8
print(Mems.ptr(ofVal: &person)) // 输出:0x000000010000c4c8(person变量的地址值)
print(Mems.ptr(ofRef: person)) // 输出:0x00000001006302d0(堆空间Person对象的地址值)

很明显ptr存储的是person变量的地址值。其实从withUnsafePointer的入参和返回值也能反映出ptr存储的是person变量的地址值,因为传入什么,返回值就是什么。ptr本质就是person

获取堆空间的地址:

var person = Person(age: 10)

var ptr1 = withUnsafePointer(to: &person) { UnsafeRawPointer($0) }
var personObjAddress = ptr1.load(as: UInt.self)
var ptr2 = UnsafeMutableRawPointer(bitPattern: personObjAddress)
print(ptr2) // 输出:Optional(0x0000000100657f60)

ptr1保存的是person地址值,所以ptr1.load取的是person保存的地址personObjAddress(对象堆空间地址)。ptr2指针装的就是对象堆空间地址,换句话说就是ptr2指针指向了对象堆空间地址。

三、创建指针

示例代码一(通过malloc创建):

// 堆空间创建指针(16代表申请16个字节的内存,返回值类型是UnsafeMutableRawPointer可选类型)
var ptr = malloc(16)

// 存数据
// 指针前8个字节填充数据(Int类型数字10)
// of: 存放的数据
// as: 存放的数据类型
ptr?.storeBytes(of: 10, as: Int.self)
// 指针后8个字节填充数据(Int类型数字20)
// toByteOffset: 字节偏移量(偏移8代表是因为Int占用8个字节)
ptr?.storeBytes(of: 20, toByteOffset: 8, as: Int.self)

// 取数据
// 取出指针指向的内存前8个字节的数据
print((ptr?.load(as: Int.self))!) // 输出:10
// 取出指针指向的内存后8个字节的数据(参数同存数据)
print((ptr?.load(fromByteOffset: 8, as: Int.self))!) // 输出:20

// 销毁
free(ptr)

通过malloc创建的指针一定要在结束后销毁。

示例代码二(通过UnsafeMutableRawPointer.allocate创建):

// 创建指针(返回值是UnsafeMutableRawPointer类型)
// byteCount: 申请的字节大小
// alignment: 对齐数(一般写1就好)
var ptr = UnsafeMutableRawPointer.allocate(byteCount: 16, alignment: 1)

// 存数据
// 存储前8个字节数据
ptr.storeBytes(of: 11, as: Int.self)
// advanced返回的是ptr偏移by字节的UnsafeMutableRawPointer类型的指针
// 意思是:ptr后8个字节存储Int类型22
ptr.advanced(by: 8).storeBytes(of: 22, as: Int.self)

// 取数据
print(ptr.load(as: Int.self)) // 输出:11
print(ptr.advanced(by: 8).load(as: Int.self)) // 输出:22

// 销毁
ptr.deallocate()

注意advanced返回的是偏移指定字节后的指针。

示例代码三(通过UnsafeMutablePointer.allocate创建):

// 创建指针
// capacity: 容量(3就是申请24字节的内存--因为Int占用8个字节,所以一共是24字节)
var ptr = UnsafeMutablePointer<Int>.allocate(capacity: 3)

// 存数据
// 第一种初始化方式:pointee: 操作的是前8个字节
//ptr.pointee = 10

// 第二种初始化方式:initialize: 用10去初始化内存空间(前8个字节)
ptr.initialize(to: 10)

// 第三种初始化方式:连续存储:连续count*Int个字节重复存储repeating(连续2块内存都存储数字10)
// count: 重复次数
// repeating: 重复数据
//ptr.initialize(repeating: 10, count: 2)

// 后继: 跳过8个字节,返回指针(跳过前8个字节的指针存储20)
ptr.successor().initialize(to: 20)
// 可以连续跳(连续跳过前面16个字节)
ptr.successor().successor().initialize(to: 30)

// 取数据
// 取数据方式一
// 前8个字节
print(ptr.pointee) // 输出:10
// 往下移动n*8个字节
print((ptr + 1).pointee) // 输出:20
print((ptr + 2).pointee) // 输出:30
// 取数据方式二
// 取出第n段(每段8个字节)的字节
print(ptr[0]) // 输出:10
print(ptr[1]) // 输出:20
print(ptr[2]) // 输出:30

// 反初始化
ptr.deinitialize(count: 3)
// 销毁
ptr.deallocate()

使用泛型指针initialize初始化数据时,一定要使用deinitialize反初始化(countinitialize次数要一致)。

注意: UnsafeMutableRawPointer.allocate因为没有指定类型,所以需要传入指定类型和字节。

UnsafeMutablePointer.allocate是泛型指针,所以只需要告诉系统创建多大容量的内存。

示例代码四:

class Person {
    var age: Int
    var name: String
    init(age: Int, name: String) {
        self.age = age
        self.name = name
    }
    deinit {
        print(name, "deinit")
    }
}

var ptr = UnsafeMutablePointer<Person>.allocate(capacity: 3)
ptr.initialize(to: Person(age: 10, name: "Jack"))
(ptr + 1).initialize(to: Person(age: 11, name: "Rose"))
(ptr + 2).initialize(to: Person(age: 12, name: "Kate"))
print("1")
ptr.deinitialize(count: 3)
print("2")
ptr.deallocate()
print("3")
/*
 输出:
 1
 Jack deinit
 Rose deinit
 Kate deinit
 2
 3
 */

如果上面的代码不写deinitialize,就不会有deinit输出;如果deinitialize(count: 2),第三个initialize就不会释放;所以一定要及时正确的使用deinitialize,否则会有内存泄漏。

建议:在函数内部创建指针时,把指针释放的代码放到defer函数体内。

四、指针之间的转换

示例代码:

// 创建指针
var ptr = UnsafeMutableRawPointer.allocate(byteCount: 16, alignment: 1)

// 把原始指针转换为泛型指针
// to: 转换目标类型
ptr.assumingMemoryBound(to: Int.self).pointee = 11
(ptr + 8).assumingMemoryBound(to: Double.self).pointee = 22.0

// unsafeBitCast强制转换
// 第一个参数:待转换的指针
// 第二个参数:转换目标指针类型
print(unsafeBitCast(ptr, to: UnsafePointer<Int>.self).pointee) // 输出:11
print(unsafeBitCast(ptr + 8, to: UnsafePointer<Double>.self).pointee) // 输出:22.0

ptr.deallocate()

unsafeBitCast是忽略数据类型的强制转换,不会因为数据类型的变化而改变原来的内存数据(可以认为是内存数据直接搬过去的,一般情况下的强制转换都会改变原来的内存数据形成新的内存数据存储)。

unsafeBitCast也可以直接创建一个指针指向堆空间:

class Person { }
var person = Person()
print(Mems.ptr(ofRef: person)) // 输出:0x0000000100556d10
var ptr = unsafeBitCast(person, to: UnsafeRawPointer.self)
print(ptr) // 输出:0x0000000100556d10

person保存的是Person对象的堆空间地址,unsafeBitCast就是把person保存的内存地址拿出来原封不动转换为UnsafeRawPointer类型的指针给ptr,所以ptr保存的地址和person保存的地址是一样的。

还有一种方式是把person地址取出来,利用地址创建一个指针指向堆空间:

var address = unsafeBitCast(person, to: UInt.self)
var ptr = UnsafeRawPointer(bitPattern: address)

注意:原始指针和泛型指针ptr + 8是有区别的。原始指针ptr + 8指的是跳过8个字节,泛型指针指的是跳过8*类型占用字节个字节。

更多系列文章,请关注微信公众号【1024星球】