Swift中的指针大法

本文概要

  • 指针的种类及区别
  • 不同指针间的相互转换及常用方法
  • 各种类型的指针获取及应用
  • more than that

指针简介

打开开发文档,可以从Swift-->Swift Standard Library-->Manual Memory Management中找到指针类型:

指针类型

Swift中的指针类型分为两种:Typed PointersRaw Pointers

  • Typed Pointers
    typed pointer为类型指针,用于指向某种特定类型

  • Raw Pointers
    raw pointer为原始指针,未指明指向的类型,相当于C语言中的void *

  • Mutable
    指针中含Mutable的为可变类型指针,表明可对指针指向的内存进行写操作

  • Buffer
    指针中含Buffer的为缓冲类型指针,这类指针遵守了SequenceCollection协议,可以方便的进行一些集合操作,如flitermapreduce

转换

以下例子用于说明这8种指针间如何相互转换

  • UnsafeMutableRawPointer
let count = 2
let alignment = MemoryLayout<Int>.alignment     // 8
let stride = MemoryLayout<Int>.stride       // 8
let byteCount = count * stride      // 16

//  分配2个Int类型所需字节数给UnsafeMutableRawPointer指针
let mRawP = UnsafeMutableRawPointer.allocate(byteCount: byteCount, alignment: alignment)
//  使用defer确保释放UnsafeMutableRawPointer指针,以防遗漏
defer {
    mRawP.deallocate()
}
//  在mRawP中存储Int类型数据,257
mRawP.storeBytes(of: 257, as: Int.self)
//  在mRawP向后偏移8个字节的位置存储Int类型数据,100
mRawP.storeBytes(of: 100, toByteOffset: stride, as: Int.self)
//  获取mRawP向后偏移8个字节位置的指针
let anotherMRawP = mRawP.advanced(by: stride)
print("--------\(type(of: mRawP))---------")
print(mRawP.load(as: Int.self))     // 257
print(mRawP.load(fromByteOffset: stride, as: Int.self))     // 100
print(anotherMRawP.load(as: Int.self))      // 100

可变原生指针通过storeBytes存储数据,load读取数据,advanced移动字节数

  • UnsafeMutablePointer<Int>
// 将mRawP的内存数据绑定到Int类型上,生成UnsafeMutablePointer<Int>指针
let mP = mRawP.bindMemory(to: Int.self, capacity: byteCount)
// 将mP内存中的数据加1
mP.pointee += 1
// 将mP后一个指针中的内存数据加1
(mP + 1).pointee += 1
print("--------\(type(of: mP))---------")
print(mP.pointee)       // 258
print(mP.advanced(by: 1).pointee)       // 101
print(mP.successor().pointee)       // 101
print(mP.advanced(by: 1).predecessor().pointee)     // 258

可变类型指针通过pointee存取数据,advancedsuccessorpredecessor移动指针。
其中,successorpredecessor函数为指定类型指针特有的函数。successor指向下一个元素,predecessor指向上一个元素。

这里可能有点懵,为啥UnsafeMutableRawPointer中的advanced移动的是字节数,而UnsafeMutablePointer<Int>中的advanced移动的是元素数?这是由数据类型决定的,因为原生指针不知道存储元素的类型,所以指针以字节为单位移动。而指定类型指针知道存储的元素类型,所以指针以元素为单位移动。

  • UnsafeRawPointer与UnsafePointer<Int>
let rawP = UnsafeRawPointer(mRawP)
print("--------\(type(of: rawP))---------")
print(rawP.load(as: Int.self))      // 258
let p = rawP.bindMemory(to: Int.self, capacity: stride)
print("--------\(type(of: p))---------")
print(p.pointee)        // 258

不可变指针,没什么好说的

  • 地址比较
let mRawPAddress = String(Int(bitPattern: mRawP), radix: 16)
let mPAddress = String(Int(bitPattern: mP), radix: 16)
let rawPAddress = String(Int(bitPattern: rawP), radix: 16)
let pAddress = String(Int(bitPattern: p), radix: 16)
print("--------address---------")
print(mRawPAddress == rawPAddress)      // true
print(mRawPAddress == mPAddress)        // true
print(mRawPAddress == pAddress)     // true

虽然上文中的四种指针类型不相同,但显然他们的地址是相同的,因为都指向同一块内存区域,只是读取数据的方式不同而已。

  • UnsafeRawBufferPointer
let rawBufferP = UnsafeRawBufferPointer(start: rawP, count: byteCount)
print("-------\(type(of: rawBufferP))--------")
let _ = rawBufferP.map{print($0)}
print("-------")
print(rawBufferP.load(as: Int.self))        // 258
print("-------")
print(rawBufferP.load(as: Int8.self))       // 2
print("-------")
print(rawBufferP.load(fromByteOffset: 1, as: Int8.self))        // 1
UnsafeRawBufferPointer.png

前边说过,原生指针以字节为单位移动指针,所以map中打印出的为每个字节上的数据。如果直接以Int类型读取rawBufferP中的值,显然为258。但是如果以Int8类型,也就是字节为单位读取rawBufferP中的值,此时为2,而rawBufferP后一个字节值为1。一个字节最大存储数据为255,根据小段模式,此时数据为12(256进制),转换成十进制就是258。

  • UnsafeBufferPointer<Int>、UnsafeMutableRawBufferPointer、UnsafeMutableBufferPointer<Int>
let bufferP = rawBufferP.bindMemory(to: Int.self)
print("-------\(type(of: bufferP))--------")
let _ = bufferP.map{print($0)}
let mRawBufferP = UnsafeMutableRawBufferPointer(mutating: rawBufferP)
print("-------\(type(of: mRawBufferP))--------")
let _ = mRawBufferP.map{print($0)}
let mBufferP = UnsafeMutableBufferPointer(mutating: bufferP)
print("-------\(type(of: mBufferP))--------")
let _ = mBufferP.map{print($0)}
buffer pointers.png

各种类型的指针获取及应用

  • 可变类型指针的获取
var a = 1
withUnsafeMutablePointer(to: &a){$0.pointee += 1}
print(a)        // 2

var arr = [1, 2, 3]
withUnsafeMutablePointer(to: &arr){$0.pointee[0] += 1}
print(arr)      // [2, 2, 3]
arr.withUnsafeMutableBufferPointer{$0[0] += 1}
print(arr)      // [3, 2, 3]

通过withUnsafeMutablePointer获取可变类指针,通过withUnsafeMutableBufferPointer获取可变缓冲类型指针

  • Struct实例指针的获取
struct Rich {
    var money: Int
    var isRich: Bool
}
var rich = Rich(money: 99999999, isRich: true)
withUnsafeBytes(of: &rich) { bytes in
    for byte in bytes {
        print(byte)
    }
}
print("---------------")
let richP = withUnsafeMutablePointer(to: &rich) { UnsafeMutableRawPointer($0) }
let moneyP = richP.assumingMemoryBound(to: Int.self)
moneyP.pointee = 0
print(rich.money)
let isRichP = richP.advanced(by: MemoryLayout<Int>.stride).assumingMemoryBound(to: Bool.self)
isRichP.pointee = false
print(rich.isRich)
Struct实例
  1. 通过withUnsafeBytes获取可变原生缓冲类型指针,可获取到rich中每个字节的值
  2. 通过withUnsafeMutablePointer获取到可变Rich类型指针后,转为可变原生类型指针获取rich地址
  3. 获取moeny地址并赋值
  4. 获取isRich地址并赋值

这里或许有疑问,为什么要转成原生指针?因为原生指针以字节为单位进行操作,更方便获取想要的元素指针。

  • Class实例指针的获取
class CRich {
    var money = 0
    var isRich = false
}

var crich = CRich()
withUnsafeBytes(of: &crich) { bytes in
    for byte in bytes {
        print(byte)
    }
}
print("---------------")
let crichP = Unmanaged.passUnretained(crich as AnyObject).toOpaque()
let cmoneyP = crichP.advanced(by: 16).assumingMemoryBound(to: Int.self)
cmoneyP.pointee = 99999999
print(crich.money)
let cisRichP = crichP.advanced(by: 16 + MemoryLayout<Int>.stride).assumingMemoryBound(to: Bool.self)
cisRichP.pointee = true
print(crich.isRich)
Class实例
  1. 通过Unmanaged将rich转换为可变原生类型指针
  2. 获取money地址并赋值
  3. 获取isRich地址并赋值

这里应该又出现几个疑问:
1.通过withUnsafeBytes获取到的可变原生缓冲类型指针打印出的字节似乎不对?
2.为什么要通过Unmanaged获取指针而不是withUnsafeMutablePointer?
3.为什么获取rich指针后要偏移16个字节才是money的地址?

  • 值引用与类型引用
    Swift中的Struct属于值引用,而Class属于类型引用。对于值引用,所见即所得,所以上文通过withUnsafeBytes可以获取结构体各字节的值,而类型引用实际是指针指向具体的类型,所以通过withUnsafeBytes获取到的结果不对。

再从侧面验证一下这个结论:

print(MemoryLayout<Rich>.alignment)     // 8
print(MemoryLayout<Rich>.size)      // 9
print(MemoryLayout<Rich>.stride)        // 16
print("--------------")
print(MemoryLayout<CRich>.alignment)        // 8
print(MemoryLayout<CRich>.size)     //8
print(MemoryLayout<CRich>.stride)       //8

由于Rich是值引用,所以size为8 + 1等于9,由于内存对齐,所以stride等于16。而CRich是类型引用,实际是个指针,因此size与stride都是8。

  • Class的前16字节
    1.在32-bit设备上,前4byte存储类型信息,后8byte存储引用计数器,共12byte
    2.在64-bit设备上,前8byte存储类型信息,后8byte存储引用计数器,共16byte

因此,实际地址从16byte开始(以64-bit设备为例)

Class trick

既然说到这里,是否可以拿Class前8byte中存储的类型信息玩些小把戏?

class Dog {
    func description() {
        print("This is a dog.")
    }
}
class Cat {
    func description() {
        print("This is a cat.")
    }
}
var dog = Dog()
var cat = Cat()
let dogP = Unmanaged.passUnretained(dog as AnyObject).toOpaque()
let catP = Unmanaged.passUnretained(cat as AnyObject).toOpaque()
catP.bindMemory(to: Dog.self, capacity: 8).pointee = dogP.load(as: Dog.self)
cat.description()       // This is a dog.

由于Dog与Cat内存分布相同,强制将cat前8byte中的类型信息替换成dog的类型信息,此时通过cat调用description函数实际是调用到dog中去。

其实Objective-C中通过object_setClass函数也可以玩这种把戏:

@interface Dog : NSObject
@end

@implementation Dog
- (NSString *)description {
    return @"This is dog.";
}
@end

@interface Cat : NSObject
@end

@implementation Cat
- (NSString *)description {
    return @"This is cat";
}
@end
Dog *dog = [Dog new];
Cat *cat = [Cat new];
object_setClass(cat, dog.class);
NSLog(@"%@", cat.description);      // This is dog.

Have fun!

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容