Swift内存赋值探索二: 指针在Swift中的使用

任何语言的底层实现,其实都离不开指针,大部分高级语言都会将指针的操作隐匿起来,比如在Swift中我们很少会直接使用指针。但是这并不意味着我们在Swift中不能使用指针了,只是使用起来会更加麻烦 — 高层建筑总能能带来便利性,但是,这将牺牲它的灵活性 — 没有什么比指针更加灵活了。
由于Swift它是一门严格安全的语言,如果在Swift的框架之下,我们在编程中使用到的内存管理将由Swift自动处理,而指针则游离在此之外,也就是说,我们需要自己去管理指针占有内存的管理,这将回到更加古老的编程手段中,显然,在Swift中使用指针并不便捷,同时也不安全。
Swift当然考虑到了这一点,于是增添了一系列和指针有关的类和结构,目的是让我们更轻松的在Swift中使用指针。
Swift中有两大类的指针: typed pointer指定类型指针,raw pointer未指定类型的指针。不管是typed pointer还是raw pointer,他们的几个子类都是以unsafe开头,这同时也在告知使用者,在Swift 中使用指针是不安全的。

一、typed point

typed point是指定类型的指针,我们在使用typed point的使用,会明确知道该指针指向的类型。它包含了一共四个子类型,他们分别是:

  • UnsafePointer
  • UnsafeMutablePointer
  • UnsafeBufferPointer
  • UnsafeMutableBufferPointer

typed point指定了类型,也即相当于我们在C语言中类似的指明了类型指针。

1. UnsafePointer

UnsafePointer 的官方描述是 访问特定类型数据的指针。意即这个指针的类型是已知。
在上一篇文章中了解到,内存的三种状态:

  • 未绑定类型同时未初始化值
  • 绑定类型但是未初始化值
  • 绑定类型同时初始化值

每一种状态都必须要有特定的指针区表示。这里的UnsafePointer就是指向绑定类型同时初始化值内存的。

另外,它相当于C系列语言中的const对象,我们不能直接初始化UnsafePointer对象(当然如果指定地址是可以的,但是我们在实际开发中,我们不太可能直接使用这个方法初始化一个指针。因为这样初始化的指针很不透明,你必须要确定对应的地址存放的数据类型是对应的,不然得到的将是一个位置的值)。同时我们也不能修改UnsafePointer指向的值,因为这是一个常量指针!

//  通过地址初始化,也即是让这个指针指向某个地址,因为我们仅仅制定了指针的类型,并没有给指针初始化一块内存,所以它指向一个位置的东西。
let usp = UnsafePointer<Int>.init(bitPattern: 20) 
print(usp.pointee) // 这里的打印是没有意义的

//通过另一个变量指针初始化一个`UnsafePointer`常量指针
let usmp = UnsafeMutablePointer<Int>.allocate(capacity: sizeof_sfntInstance)
usmp.pointee = 20   
let usp2 = UnsafePointer<Int>.init(usmp)
print(usp2.pointee)  // 20

我们可以通过 pointee 属性发访问指针的值。但是要注意,UnsafePointer并不能对值进行设置,如果需要可以设置指向值的指针,应该使用UnsafeMutablePointer
当然我们还可以直接打印指针,得到这个指针指向的地址。
print(usp2) // 0x0000000133d08340

2. UnsafeMutablePointer

上面已经做了一些对UnsafeMutablePointer的描述,它相当于C系列语言中的变量指针。 :
C语言中:

int *p = malloc(sizeof(int));

Swift中创建一个UnsafeMutablePointer:

let usmp = UnsafeMutablePointer<Int>.allocate(capacity: sizeof_sfntInstance)

两者的意义相同。
我们同样可以通过 pointee 属性发访问指针的值。同时,你还可以直接修改 pointee的值,就像我们使用Swift的其他类一样。

let usmp = UnsafeMutablePointer<Int>.allocate(capacity: sizeof_sfntInstance)
usmp.pointee = 100   
print(usmp.pointee) // 100

3. UnsafeBufferPointer

UnsafeBufferPointer指代一系列连续的内存。我们可以使用这个指针作为一个序列的指针,并通过指针直接访问旗下元素。

let intArr = [10,20,30,40,50]
let usbp = UnsafeBufferPointer.init(start: intArr, count: 4)  // 截取了四个元素
print(usbp[0],usbp[1],usbp.count,usbp.last ?? "0")
usbp.forEach { (a) in
      print(a)
}
//
10 20 4 Optional(40)
10
20
30
40

尽管这是一个Swift 的指针,但是我们依然像使用对象一样操作了数组,因为UnsafeBufferPointer 遵循了Collection 协议,说明这是一个可变的集合,对于集合我们进行遍历则无可厚非了。
但是同样的,这个UnsafeBufferPointer是常量,它只能获取到数据,但是通过这个指针去修改数据。与之对应的是UnsafeMutableBufferPointer指针。

4. UnsafeMutableBufferPointer
可变的系列指针。UnsafeMutableBufferPointer拥有对指向序列修改的能力:

let usmp = UnsafeMutablePointer<Int>.allocate(capacity: sizeof_sfntInstance)
let usmbp = UnsafeMutableBufferPointer<Int>.init(start: usmp, count: 5)  // 拓展为5各元素的大小
usmbp[0] = 120
usmbp[1] = 130   //进行修改   其他未修改的内容将产生随机值
usmbp.forEach { (a) in
       print(a)
}
 print(usmbp.count)

状况跟UnsafeBufferPointer有点类似,只是在初始化的时候,需要借助UnsafeMutablePointer。 并不能直接使用已经存在序列进行初始化。
值的注意的是:如果一个序列被初始化之后,没有给每一个元素赋值的话,这些元素的值都将出现随机值。

二、raw pointer

raw pointer指未知类型的指针,这个类型相当于C语言中的(void *)类型,。
同样的,raw pointer也包含了四个子类型:

  • UnsafeRawPointer
  • UnsafeMutableRawPointer
  • UnsafeBufferRawPointer
  • UnsafeMutableBufferRawPointer

1. UnsafeRawPointerUnsafeMutableRawPointer
UnsafeRawPointer类型不提供自动内存管理,也不保证其内存,同时没有做任何的内存对齐。 开发者在使用UnsafeRawPointer的时候,应该手动对指针做内存管理,以避免泄漏或未定义的行为。这一点和UnsafePointer类似,但是相对来说UnsafeRawPointer更加灵活,我们可以单纯的创建一个指针,而不管它的类型和值。当需要使用来操作某个内存的时候,再来绑定类型和赋值。
UnsafePointer一样,UnsafeRawPointer同样不能直接创建,我们需要借助的它的可变形态UnsafeMutableRawPointer来创建。
UnsafeMutableRawPointerUnsafeRawPointer的可变版本,但是它依然没有任何类型,也不确定任何值,我们创建的时候,需要给它指定占用内存的大小和对齐方式,这样就能形成一个完成的内存布局:

let usmrp = UnsafeMutableRawPointer.allocate(bytes: 4, alignedTo: 1)

接下来通过创建的 usmrp创建一个UnsafeRawPointer的不可变的指针:

let usrp = UnsafeRawPointer.init(usmrp)
print(usmrp)
print(usrp)
// 同一块内存
0x0000000101e1a570
0x0000000101e1a570

我们在一开始就说了,raw pointer 没有指定内存的类型,也没有初始化的值,他只是圈了一块内存,所以我们压根就不知道它指向的那个内存中到有什么。并且,它也不包含pointee这个属性了。
虽然我们不知道raw pointer指向的内存中有什么,但是我们可以通过raw pointeradvanced(by:)函数获取之后的地址的块区。每一次调用advanced(by:)都会返回一个地址。如果将之后的地址绑定某个类型之后,我们就可以在这个地址的片区中赋值了。

 let newPtr = usrp.advanced(by: 16)  // 这是往后偏移 16个字节之后的地址

需要进一步使用usrpusmrp,应该给他们绑定类型。 如果我们需要绑定类型,可以使用Swift提供的两个函数:

public func bindMemory<T>(to type: T.Type, capacity count: Int) -> UnsafeMutablePointer<T>    // 绑定一个类型,以及类型的大小

public func assumingMemoryBound<T>(to: T.Type) -> UnsafeMutablePointer<T>
// 重绑定一个类型

我们可以通过上述的两个函数来给一个指定的地址区域赋值:

newPtr. assumingMemoryBound(to: Int.self) // 将内存绑定为 Int 类型
newPtr.initialize(12)     //初始化赋值  
print(newPtr.pointee)  // 12

这里可以看到,我并没有显示的指定需要的赋值的哪部分内存有多大,是因为当我们给内存绑定了类型的时候,系统会自动计算我想要是使用的内存区域大小(sizeOf(Int))。

不管是UnsafeRawPointer还是UnsafeMutableRawPointer都有上面两个方法,他们都会返回 typed pointer, 分别和UnsafePointerUnsafeMutablePointer对应。
经过绑定的raw pointer 就变成了typed pointer,这时候的使用参考文中typed pointer的内容即可。

2. UnsafeRawBufferPointerUnsafeMutableRawBufferPointer
内存中的每个字节都被视为一个独立于内存绑定类型的 UInt8 值。 UnsafeRawBufferPointerUnsafeMutableRawBufferPointer 指代的是一系列的没有被绑定类型的内存区域。我们可以理解成他们实际上就是一些数组,再绑定内存之前,其中包含的元素则是每一个字节。
在底层,基本数据单元的复制是有效的,另外没有被 retain 和 stong 的也是能够安全的复制的,同样的,对于来自C API的对象也能够安全的复制。对于原声的Swift类型,有的包含了引用的对象的复制则有可能失败,但是我们可以使用指针对他们的值进行复制,这样的结果是有效的。如果我们强行对一下发类型进行复制,不一定有效,除非使用像C语言中的APImemmove().来操作。
UnsafeRawBufferPointerUnsafeMutableRawBufferPointer是内存视图,尽管我们知道它指向的内存区域,但是它并不拥有这块内存的引用。复制UnsafeRawBufferPointer 类型的变量不会复制它的内存;但是初始化一个集合到另一个新的集合过程会复制集合中的引用内存。
代码实现一下如何创建UnsafeRawBufferPointerUnsafeMutableRawBufferPointer:

let usmrbp = UnsafeMutableRawBufferPointer.allocate(count: 3)

创建一个无类型的UnsafeMutableRawBufferPointer指针。 之前说了,内存中每个字节都被视为一个独立于内存绑定类型的 UInt8 值,因此,这里的无类型实际上是有类型的,他应该属于 UInt8 类型。 而这个系列的大小应该为 3 * MemoryLayout<T>.stride 。
我们可以向该指针中的内存中赋值一个遵循 collect 的对象:

usmrbp.copyBytes(from: [5,56,6])
usmrbp.forEach { (a) in
     print(a)
}
//
5
56
6

基本上, typed point描述的是我们已经知道的哪部分内存,通过typed point我们可以知道内存的地址,内存的类型,以及内存中存储的是什么。raw pointer 则描述的是一个内存的起始地址值,通过这个起始的地址,我们能够逐步的获取到后续的地址值,结合绑定类型之后, typed point总能在使用一块合适地址保存我们想要保存的值。

不管是raw pointer还是typed point指针,两者都是不安全的,不安全的意义包含了内存的管理和对其他未知内存的侵害。 有时候使用指针无意改变了某些系统内部的关键变量,将会导致莫名其妙的崩溃。

三、如何正确的使用raw pointertyped point

我们自己创建的指针缺乏对内存的管理,因此非常脆弱,如何正确安全的使用指针值得我们探究。Swift 提供了好几种方式供我们用于指针和Swift原生类的交互。

1. Unmanaged 托管

因为直接使用指针需要我们去管理内存,这很繁琐,并且很危险。于是,Unmanaged 出现了。Unmanaged 能够将由 C API 传递过来的指针进行托管,我们可以通过Unmanaged标定它是否接受引用计数的分配,以便实现类似自动释放的效果;同时,如果不是使用引用计数,也可以使用Unmanaged 提供的release函数来手动释放,这比在指针中进行这些操作要简单很多。

首先,假设我有一个 PersonClass 类型的对象,我想将它用一个指针表示:

class PersonClass{
    let name: String = "hua"
    let age: Int = 22
}

let um = Unmanaged.passUnretained(PersonClass() as AnyObject).toOpaque()   
let tpeUns = um.bindMemory(to: Byte.self, capacity: MemoryLayout<Int>.stride)
print(UnsafeMutablePointer<Byte>(tpeUns))
//
0x0000000153e490e0

打印的地址则是这个对象的存储地址了,我们可以使用这个指针来表示这个对象。如果我们想取出和更改这个对象中的属性 nameage 的时候,可以这样操作:

let rptr = UnsafeMutableRawPointer(tpeUns)  // 转化成不透明指针, 用于重绑定类型。

// 注意,再上一篇文章中已经介绍,Swift 中类的首地址有效值在 16 字节之后。
let namePtr = rptr.advanced(by: 16).assumingMemoryBound(to: String.self)    
let agePtr = rptr.advanced(by: MemoryLayout<String>.stride + 16).assumingMemoryBound(to: Int.self)
print(namePtr.pointee,agePtr.pointee)  // 取出内存值
print(ps.name,ps.age)  // 打印原对象
//namePtr.pointee = "Tom - Loo1"   //直接给指针赋值也是可以的。
namePtr.initialize(to: "Tom - Loo")  //  重新初始化内存中的值
agePtr.initialize(to: 26)
print(ps.name,ps.age)

// 打印
Tom 22
Tom - Loo 26

总结起来,基本思路就是获取到了对象的地址(即首地址,注意这个地址应该使用不透明指针表示,以便后续根据属性的不同类型重新绑定),然后偏移16个字节,跳过 meta 信息所占的空间。根据属性的排布顺序,逐步计算取出每个属性所对应的指针,有了每个属性的指针,要赋值就简单了。 令人惊喜的是: 这里不管是let 属性还是 var 属性,都可以进行赋值的。

其实到这里,如何进行内存的基本思路已经出来了。 但是这篇文章主要是介绍指针的使用,我们继续看看另一种指针使用方式,用来获取一个属性而非整个对象的指针。

2. withPoint系列方法

Swift提供了一种更为简便的方式给我们获取一个属性的指针,那就是withPoint系列方法:

public func withUnsafeMutablePointer<T, Result>(to arg: inout T, _ body: (UnsafeMutablePointer<T>) throws -> Result) rethrows -> Result

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

public func withExtendedLifetime<T, Result>(_ x: T, _ body: (T) throws -> Result) rethrows -> Result

public func withExtendedLifetime<T, Result>(_ x: T, _ body: () throws -> Result) rethrows -> Result

一般使用前面两个函数的概率更高,我重点说下这两个。
同样对于上述的PersonClass类,如果修改它的属性值,我们可以如下操作:

        withUnsafeMutablePointer(to: &ps.name) {
            print($0.pointee)
            $0.pointee = "Tom - hans"
        }
        
        print(ps.name)

这个函数如果使用在这里实际上并没有什么优势: 如果使用withUnsafeMutablePointer(to:) 方法修改属性值,必须保证属性的类型是 var ,虽然在内部我们可以使用初始化指针的方式强行改变,但是在调用方法时,编译器会提示需要加入一个 var 的变量,而不能是常量。 如此说,我们为什么不直接使用属性的点语法直接赋值呢?

本文对Swift的中指针介绍只是它的一部分功能,笔者能力有限,可能在文中出现纰漏和错误。欢迎指正。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335

推荐阅读更多精彩内容