[译]Swift 3.0中不安全的世界

原文链接

就像大多数现在的变成语言一样,在 Swfit 中你就像生活在一个幸福的世界中,这里的内存被额外的部分所管理,而像这样的内存管理语言的编译和运行要么就像 Swift 一样,要么他运行的好坏取决于他的垃圾回收机制。而这些我们所提到的这些隐藏在编程语言中的,你不必要去或者很少的情况下你需要去思考这些问题。

然而由于 Swift 的多样性的特点,你可能需要调用一个危险的 C 的 Api 比如说 OpenGL 或者 POSIX 中的函数,在这些情况下你可能需要处理一些让我们头疼的情况。没错,我说的就是指针和手动在堆中申请内存空间。

在 Swift 3.0 以前,Swift 的不安全的 API 有点混乱,你可以通过好几个方法来达到相同的结果,但是那只是你从 stackoverflow 上复制、粘贴来的,但是你没有彻底的理解真正发生了什么。在 Swift 3.0 中所有事物都发生了改变,而且他变得更容易理解。

在这篇文章中,我不会告诉你如何将代码从 Swift 2.x 迁移到 Swift 3.0。反而我将会告诉你这些事情在 Swift 3.0 中如何工作,因为通常造成不安全的引用的主要原因是与 C 的底层 API 的交互。

让我们从最简单的操作开始——开辟内存空间来存储一个整型变量。

在 C 中,你将会写下下面这样的代码

int *a = malloc(sizeof(int));
*a = 42;

printf("a's value: %d",*a)

free(a)

而这在 Swift 在这么实现的:

let a = UnsafeMutablePointer<Int>.allocate(capacity: 1)
a.pointee = 42

print("a's value: \(a.pointee)") //42

a.deallocate(capacity:1)

第一类我们所看到的是 Swift 中的 UnsafeMutablePointer<T> ,这普通的结构体相当于一个 T 的指针,正如你所示的,他有一个静态函数, allocate 将会开辟需要的内存空间。

正如你所想的,这个 UnsafeMutablePointer 还有一个变形—— UnsafePointer ,这个类型不允许你修改指针的值,此外不可修改的 UnsafePointer 甚至没有 allocate 方法。

在 Swift 中,你还有另外一个方法来创建一个 Unsafe[Mutable]Pointer 方法,那就是使用 & 操作。当传一个 block 或者函数,你可以使用一个 & 来传入一个指针。让我们来看下面这个例子

func receive(pointer:UnsafePointer<Int>){
  print("param value is:\(pointer.pointee)")    //42
}

var a:Int = 42
receive(pointer: &a)

& 操作需要一个 var 变量,但是这个将会提供给你你所需要解决的各种情况。比如说,你可以使用可修改的引用(mutable reference),甚至修改它,比如说:

func receive(mutablePointer:UnsafeMutablePointer<Int>){
  mutablePointer.pointee *= 2
}

var a = 42
receive(mutablePointer:&a)
print("A's value has changed in the function:\(a)") //84

这个例子和前面那个例子有重要的区别。在前面的例子中,我们需要手动开辟内存空间(我们需要在创建好后手动分配内存空间),同时在这个简单的例子中的函数中,我们快速的创建了一个指向内存的指针。明显的,管理内存并且使用指针指向他是2个不同的话题,在接下来的例子中,我们将会聊一聊如何管理内存空间。

但是我们如何在Swift中如何在不创建一个函数的情况下,调用指针。为了达到这种目的,我们需要使用 withUnsafeMutablePointer ,他将会调用一个 Swift 的引用类型和一个有参数的 block ,让我们来看看下面这个例子。

var a = 42
withUnsafeMutablePointer(to: &a){ $0.pointee *= 2}
print("a's value is: \(a)") //84

现在我们知道了这个方法,现在我们调用 C 中那些有指针的 API ,让我们看来看下下面这个 POSIX 的打开读取路径并获取其中内容的当前地址的方法。

var dirEnt: UnsafeMutablePointer<dirent>?
var dp:UnsafeMutablePointer<Dir>?

let data = ".".data(using:ascii)
data?.withUnsafeBytes({(ptr:UnsafePointer<Int8>) in
    dp = opendir(ptr)
})

repeat{
  dirEnt = readdir(dp)
  if let dir = dirEnt{
    withUnsafePointer(to:&dir.pointee.d_name,{ ptr in
      let ptrStr = unsafeBitCase(ptr,to:UnsafePointer<CChar>.self)
      let name = String(cString:ptrStr)
      print("\(name)")
    })
  }
}

while dirEnt != nil

指针转换

当处理 C 的 API 的时候,你有时候需要将指向结构体的指针转换为不同的结构体。对于 C 的 API 的处理很简单(同时也是十分危险并且容易出现报错)的,就像你在 Swift 中所看到的,所有指针的类型是被固定的,这意味着一个 UnsafePointer<Int> 的指针不能再用在需要 UnsafePointer<UInt8> 的地方,这使得能够更好的编写出更加安全的代码,但是同样意味着你不能在你需要的时候随意转换指针类型。比如说 socket 中的 bind() 方法比如说这些情况下,我们将会使用 withMemoryRebound 这个我们用来将一个指针类型转换为另一个指针类型的方法。让我们来看看我们是如何使用角色转换,在 bind 函数中当你创建一个 sockaddr_in 结构体转换为 sockaddr

var addrIn = sockaddr_in()
// Fill sockaddr_in fields 
withUnsafePointer(to: &addrIn) { ptr in
    ptr.withMemoryRebound(to: sockaddr.self, capacity: 1, { ptrSockAddr in
        bind(socketFd, UnsafePointer(ptrSockAddr), socklen_t(MemoryLayout<sockaddr>.size))
    })
}

这个一个用来转变指针类型的特别的方法,一些 C 的 API 需要传一个 void* 指针。在 Swift 3.0 以前,你可能需要使用 UnsafePointer<Void> 。然而在3.0中有一个新的类型来处理这些指针: UnsafeRawPointer 。这个结构体和不同的结构体不同,所以这意味着他不会将其中的信息绑定到任何指定的类型中,这另我们的编码过程变得很简单。为了创建一个 UnsafeRawPointer 指针,我们只需要调用它的创建函数来包裹我们所需要的那个指针。如果我们想要用另外的方法,来将这个 UsafeRawPointer 的指针转化为其他类型的指针的时候,我们需要使用 withMemoryRebound 的上一个版本的方法,在这里他叫做 assumingMemoryBound

let intPtr = UnsafeMutablePointer<Int>.allocate(capacity: 1)
let voidPtr = UnsafeRawPointer(intPtr)
let intPtrAgain = voidPtr.assumingMemoryBound(to: Int.self)

数组指针

到这里,我们我们已经学会了一些指针的基本使用方法,同时你可以处理大多数的 C 的 API 调用。然而指针使用的地方还有很多,比如说遍历内存块,这对于程序员来说是我们可以获得很多重要信息。在 Swift 中我们有一些方法来做这些事情,比如说 UnsafePointer 有提供了一个方法 advanced(by:) 来遍历内存,这个方法返回了另一个 UnsafePointer ,这样我们就可以读写那个内存区域里面的内容。

let size = 10
var a = UnsafeMutablePointer<Int>.allocate(capacity: size)
for idx in 0..<10 {
    a.advanced(by: idx).pointee = idx
}
a.deallocate(capacity: size)

另外, Swift 还有一个 UnsafeBufferPointer 的结构体来更方便的实现这个需求。这个结构体是一个Swift数组和指针的桥梁。如果我们使用 UnsafePointer 来作为变量从而调用创建函数创建一个 UnsafeBufferPointer ,我们将能够使用大多数的Swift原生的数组(Array)方法,因为 UnsafeBufferPointer 遵守并实现了 CollectionsIndexableRandomAccessCollection 协议。所以我们可以像这样遍历内存:

// Using a and size from previous code 
var b = UnsafeBufferPointer(start: a, count: size)
b.forEach({
    print("\($0)" // Prints 0 to 9 that we fill previously 
)})

当我们提到 UnsafeBufferPointer 的是一个Swift中数组的桥梁的时候,这也意味着我们很容易使用 UnsafeBufferPointer 来调用一个已经存在的数组,比如说下面这个例子:

var a = [1, 2, 3, 4, 5, 6]
a.withUnsafeBufferPointer({ ptr in
    ptr.forEach({ print("\($0)") }) // 1, 2, 3... 
})

内存管理带来的危害

我们已经看到了很多方法来引用原始内存,但是我们不能忘记我们正在进入一个危险区域。可能重复 Unsafe 单词可能会提醒我们要小心的使用它们。然而我们我们是使用 unsafe 引用来混合两个世界(不需要内存管理和手动内存管理)。让我们来看看他在我们灵活使用中所带来的危害。

var collectionPtr: UnsafeMutableBufferPointer<Int>?
 
func duplicateElements(inArray: UnsafeMutableBufferPointer<Int>) {
    for i in 0..<inArray.count {
        inArray[i] *= 2
    }
}
 
repeat {
    var collection = [1, 2, 3]
    collection.withUnsafeMutableBufferPointer({ collectionPtr = $0 })
} while false
 
duplicateElements(inArray: collectionPtr!) // Crash due to EXC_BAD_ACCESS 

虽然这个简单的例子我们不会真正的碰到,但是实际在快速创建变量的过程中我们会碰到和他类似但是比他更加复杂的代码。在这里, collection 在一个 block 中被创建,同时在 block 结束后引用被释放。我们有意的在调用 collection 后将引用保存在了 collectoinPtr 中,然后在原始的 collection 不在存在后继续调用,所以程序在调用 duplicateElements(inArray:) 后崩溃了,如果我们想要使用指针来快速创建变量,我们需要确定这些变量能够在我们需要使用它们的时候可用。注意ARC将在每个变量离开他的作用于的时候为每个变量添加 release 方法,如果这个变量没有被强引用的话,他就会被释放。

一个解决方法是不适用 Swift 的内存管理方法而是我们自己手动开辟内存空间,就像我们文章中所提到的那些简单的代码一样,这就解决了访问无效引用的问题,但是这引入了另一个问题。如果我们没有手动释放内存,那么就会存在内存泄漏问题。

使用 bitPattern 来修改指针的值

为了更好地完成这篇文章,在这我将介绍一些 Swift 中指针的用法。 第一个就是在使用C的API的时候使用 void* 方法而不是使用内存地址。通常这会发生在一个函数接受不同类型的参数,并简单的将参数打包成 void* 类型,就像下面这个例子一样:

void generic_function(int value_type, void* value);
 
generic_function(VALUE_TYPE_INT, (void *)2);
struct function_data data;
generic_function(VALUE_TYPE_STRUCT, (void *)&data);

如果我们想要在 Swift 中调用第一个函数,我们需要使用特别的构造函数,这会创建一个特殊的地址的。所有这些函数将不会改变允许你改变内存地址中变量的值,所以我们将会在这种情况下使用 UnsafePointer(bitPattern:)

generic_function(VALUE_TYPE_INT, UnsafeRawPointer(bitPattern: 2))
var data = function_data()
withUnsafePointer(to: &data, { generic_function(VALUE_TYPE_STRUCT, UnsafeRawPointer($0)) } )

透明指针

在这篇文章的最后我想说的就是如何使用 Swift 中的透明指针。在C的 API 中我们经常会调用用户数据,而用户的数据将会成为一个 void* 指针,该用户数据将是一个 void * 指针,他将保存在一个任意内存中。一个通用的使用方法是当处理函数并设置回调方法的时候,事件将会被调用。在这种情况下,传入一个引用到一个 Swift 对象中,然后我们就可以在 C 的回调函数中调用指针的方法。

我们能够使用 UnsafeRawPointer 就像我们曾在这篇文章中的其他例子中所看到的。然而正如我们所看到的,这些调用在内存管理中有一定的问题,当我们传入一个指针到 C 中来指向一个我们没有 retain 的变量的时候,这个对象将被释放,同时这个程序将会崩溃。

Swift 有一个实用的方法来根据我们是否真的需要,从而决定指向这个对象的指针是否进行 retain 。这就是 Unmanaged 结构体的一个静态函数。使用 passRetained() 我们将会创建一个被 retained 的指向这个对象的指针,那么我们就能保证当他在 C 中被调用的时候他仍旧在那。当这个对象已经在回调函数中被 retianed 的时候我们可以使用 passUnretained() 。这两个方法将会产生 Unmanaged 的实例变量,这个实力变量将会通过调用 toOpaque() 方法转换为 UnsafeRawPointer

在另一方面我们可以将 UnsafeRawPointer 通过相反的 API fromOpaque()takeRetained() 转换为一个类或者结构体

void set_callback(void (*functionPtr)(void*), void* userData));
struct CallbackUserData {
    func sayHello() { print("Hello world!" ) }
}
 
func callback(userData: UnsafeMutableRawPointer) {
    let callbackUserData = Unmanaged.fromOpaque(userData).takeRetainedValue()
 
    callbackUserData.sayHello() // "Hello world!" 
}
 
var userData = CallbackUserData()
let reference = Unmanaged.passRetained(userData).toOpaque()
set_callback(callback, reference)

总结

正如你所看到的,调用 C 的代码在 Swift 是可行的,同时知道了有这些方法使得我们不需要用大量的代码就能实现我们想要的效果。不安全和非管理的 API 在本文中被大量的提到,但是我希望这是一个很好的进行深入了解的机会,从而你可以对他感兴趣或者能够真正的使用它。

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

推荐阅读更多精彩内容

  • 就像大多数现在的变成语言一样,在 Swfit 中你就像生活在一个幸福的世界中,这里的内存被额外的部分所管理,而像这...
    iOS开发攻城狮阅读 486评论 0 0
  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 12,010评论 4 62
  • 我的眼里饱含泪水 从春到冬 从花开到叶落 从春露到暮雪 繁星满天 蓝天白云 日日夜夜 灼烧滚烫
    听故事的耳朵阅读 129评论 0 4
  • 真是个爱哭鬼,一部电影能哭八百回。 我想有个小房子,房子里装个投影,我可以窝在沙发里用一整面白墙看电影。如果你在身...
    言小善阅读 3,113评论 3 4
  • 现在已经是11月底了,再过两个月就过年了。 今天逛知乎,发现有一个帖子:学英语给你带来哪些好处? 很多的答主都说了...
    Conly阅读 1,403评论 0 1