`UnsafeMutablePointer.initialize()`实际上是做什么的?

7
以下是我的猜测,请有经验的人指出我理解不正确的部分。
如果我有一个类,它的实例占用128位,叫做Class128Bits。而我的程序运行在64位计算机上。
首先,我调用let pointer = UnsafeMutablePointer<Calss128Bits>.allocate(capacity: 2),内存布局应该如下:
000-063 064 bits chaos
064-127 064 bits chaos
128-255 128 bits chaos
256-383 128 bits chaos

如果我调用pointer.pointee = aClass128Bits,那么程序将会崩溃,因为前两个指针还没有被初始化。访问它们所指向的内容会导致不可预测的结果。
但是如果我调用pointer.initialize(to: aClass128Bits, count: 2),那么指针可以像这样被初始化:
000-063 address to offset 128
064-127 address to offset 256
128-255 a copy of aClass128Bits
256-383 a copy of aClass128Bits

然后任何访问都是安全的。然而这并不能解释为什么UnsafeMutablePointer<Int>不会崩溃。

原文

我面临的情况: Crash

指向Int的指针运行良好,但指向String的指针会崩溃。 我知道我需要像这样初始化它: Not Crash

但我看不出为什么需要两次传递"42"。 在C中,我可能会做类似这样的事情:

char *pointer = (char *)malloc(3 * sizeof(char));
memcpy(pointer, "42", 3);
free(pointer)

如果 allocate 等于 malloc free 等于 deallocate memcpy 等于 pointee {set} ,那么 initialize deinitialize 实际上是在做什么? 为什么我的代码会崩溃?

请查看Swiftdoc上这些方法的描述。特别是关于访问pointee属性,前置条件为_"前置条件:pointee已经用Pointee类型的实例进行了初始化。"_ 如需更多详细信息,请参考相关的源代码 - dfrib
你的猜测有一个很大的缺陷。在Swift中,类是引用类型,实例的内容不会在没有调用任何复制方法的情况下被复制。UnsafeMutablePointer<Class128Bits>.allocate(capacity: 2)只分配了16字节的区域,在64位平台上有2个指针。let pointer = UnsafeMutablePointer<Class128Bits>.allocate(capacity: 2);print(malloc_size(pointer)) -> 16 - OOPer
func initialize(to: Pointee, count: Int) 用count个newValue连续副本初始化self.pointee。那么Apple在文档中所说的COPIES是什么意思呢? - CopperCash
它并没有说“复制内容”。当 newValue 是一个引用时,引用会被复制。 - OOPer
3个回答

4
let pointer0 = UnsafeMutablePointer<String>.allocate(capacity: 1)
let pointer1 = UnsafeMutablePointer<Int>.allocate(capacity: 1)

让我们检查两者的大小。
MemoryLayout.size(ofValue: pointer0) // 8
MemoryLayout.size(ofValue: pointer1) // 8

让我们检查.pointee的值。

pointer0.pointee // CRASH!!!

当...期间
pointer1.pointee // some random value

为什么会这样呢?答案非常简单。我们分配了8个字节,与“相关”的类型无关。现在清楚了,在内存中8个字节不足以存储任何字符串。底层内存必须被间接引用。但是那里有一些随机的8个字节... 将地址表示为8个随机字节所代表的内存中的内容加载为字符串很可能会导致崩溃 :-)
为什么第二种情况没有崩溃?整数值长达8个字节,地址可以表示为整数值。
让我们在Playground中尝试。
import Foundation

let pointer = UnsafeMutablePointer<CFString>.allocate(capacity: 1)    

let us = Unmanaged<CFString>.passRetained("hello" as CFString)

pointer.initialize(to: us.takeRetainedValue())
print(pointer.pointee)

us.release()

// if this playground crash, try to run it again and again ... -)
print(pointer.pointee)

看看它给我打印的信息 :-)

hello
(
    "<__NSCFOutputStream: 0x7fb0bdebd120>"
)

没有任何奇迹。pointer.pointee 尝试将存储在指针地址中的内容表示为其关联类型的值。对于 Int,这从不会导致崩溃,因为内存中的每连续 8 个字节都可以表示为 Int。
Swift 使用 ARC,但创建 Unsafe[Mutable]Pointer 不会为 T 的实例分配任何内存,销毁它也不会释放任何内存。
在使用之前必须初始化类型化内存并在使用后进行去初始化。这可以通过 initialize 和 deinitialize 方法来完成。非平凡类型才需要去初始化。尽管如此,包括去初始化是一种未雨绸缪的方法,以防您更改为某些非平凡形式。 为什么使用 Int 值赋值给 .pointee 不会导致崩溃?
  1. 初始化:存储值的地址。
  2. 赋值给 pointee:更新存储地址处的值。
如果没有初始化,它很可能会崩溃,只有通过修改在某个随机地址上的连续 8 个字节来降低概率。
尝试一下。
import Darwin
var k = Int16.max.toIntMax()
typealias MyTupple = (Int32,Int32,Int8, Int16, Int16)

var arr: [MyTupple] = []
repeat {
    let p = UnsafeMutablePointer<MyTupple>.allocate(capacity: 1)
    if k == 1 {
        print(MemoryLayout.size(ofValue: p), MemoryLayout.alignment(ofValue: p),MemoryLayout.stride(ofValue: p))
    }
    arr.append(p.pointee)
    k -= 1
    defer {
        p.deallocate(capacity: 1)
    }
} while k > 0

let s = arr.reduce([:]) { (r, v) -> [String:Int] in
    var r = r
    let c = r["\(v.0),\(v.1),\(v.2),\(v.3)"] ?? 0
    r["\(v.0),\(v.1),\(v.2),\(v.3)"] = c + 1
    return r
}
print(s)

我收到了

8 8 8
["0,0,-95,4104": 6472, "0,0,0,0": 26295]
Program ended with exit code: 0

看起来不是很随机,这就解释了为什么使用指向Int的类型指针崩溃的可能性非常小。


1
这个回答没有显示任何与我的回答不一致的事实。你的 pointer0.pointee 崩溃是因为你没有初始化指向的区域。pointer1.pointee 不会崩溃,因为 Int 没有任何需要由 ARC 管理的成员。 - OOPer
不必要,但有时可能会发生 - user3441734
@CopperCash 很难回答,因为没有保证会发生什么。我可以想象,最有可能不会出什么问题,但正如苹果文档中所提到的,类型化内存必须在使用前初始化并在使用后取消初始化,并且始终在使用.pointee之前这样做是一个非常好的主意。把.pointee看作是地址存储在Unsafe[Mutable]Pointer中的内存视图。实际上,该值不一定在可寻址的内存中,它可能在处理器寄存器中... 等等。 - user3441734
在任何严肃的开发中,我一定会初始化每个Unsafe<Mutable>Pointer。我想要确保的是,我的UnsafeMutablePointer<Int>没有崩溃的事实是否只是巧合。 - CopperCash
这里有一些对UnsafeMutablePointer的阐述,它参与了ARC:https://academy.realm.io/posts/russ-bishop-unsafe-swift/ - Igor Vasilev
显示剩余2条评论

2
从文档中可以得出结论,.initialize()是一个方法,其作用是:

使用源元素初始化从self开始的内存。

.deinitialize()是一个方法,其作用是:

反初始化self开始的计数指针,将它们的内存返回到未初始化状态。

我们应该明白,当我们使用UnsafeMutablePointer时,我们需要自行管理内存。上述描述的方法帮助我们完成这一点。

因此,在您的情况下,让我们分析您提供的示例:

let pointer = UnsafeMutablePointer<String>.allocate(capacity: 1)
// allocate a memory space

pointer.initialize(to: "42")
// initialise memory

pointer.pointee // "42"
// reveals what is in the pointee location

pointer.pointee = "43"
// change the contents of the memory

pointer.deinitialize()
// return pointer to an unintialized state

pointer.deallocate(1)
// deallocate memory

你的代码崩溃是因为你没有初始化内存而试图设置值。
以前在Objective-C中,当我们使用对象时,我们总是使用[[MyClass alloc] init]]
在这种情况下:
分配:
分配一部分内存来保存对象,并返回指针。
初始化:
设置对象的初始参数并返回它。
所以基本上.initialize()将值设置为分配的内存部分。当你只用alloc创建一个对象时,你只是把引用设置到堆中的空内存部分。当你调用.initialize()时,你将值设置到堆中的这个内存分配中。
关于指针的好article

请问您能否进一步解释一下在调用initialize函数后内存中会发生什么?在C语言中,我在使用alloc分配内存后不需要再进行初始化。 - CopperCash
allocate 之后,我应该有一些已分配的内存和一个指针引用它。我知道如果我尝试通过指针读取这些未初始化的内存中的内容,程序将会崩溃。但是 pointee{ set } 并不是读取内容,而是写入内容。写入内存应该类似于 memcpy 的行为,并且不应该导致程序崩溃。 - CopperCash

2

你需要initialize()的一个原因,也许现在唯一的原因是

用于自动引用计数(ARC)

当看到ARC如何工作时,最好考虑使用本地范围变量:

func test() {
    var refVar: RefType = initValue  //<-(1)
    //...
    refVar = newValue //<-(2)
    //...
    //<-(3) just before exiting the loacl scope
}

对于像(2)这样的常规任务,Swift 会生成以下代码:

swift_retain(_newValue)
swift_release(_refVar)
_refVar = _newValue

假设_refVar_newValue是未管理的伪变量。

Retain意味着将引用计数增加1,而release意味着将引用计数减少1。


但是,请考虑当初始值分配在(1)时会发生什么。

如果通常的分配代码被生成,代码可能会在此行崩溃:

swift_release(_refVar)

由于新分配的变量区域可能充满垃圾数据,所以无法安全地执行swift_release(_refVar)
将新区域填充为零(null),并安全地忽略null后再release可能是一种解决方案,但这有点多余且不太有效。
因此,Swift 为初始值分配生成了这种代码:
(对于已经保留的值,如果您知道所有权模型,则由您拥有。)
_refVar = _initValue

(针对未保留值,意味着你尚未拥有所有权。)
swift_retain(_initValue)
_refVar = _initValue

这是initialize(初始化)。

不释放垃圾数据,分配一个初始值,在需要时保留它。

(上述“常规赋值”的解释有点简化,Swift在不需要时省略了swift_retain(_newValue)。)


当退出本地作用域时(3),Swift只生成这种代码:

swift_release(_refVar)

所以,这是deinitialize
当然,您知道对于像Int这样的基本类型不需要保留和释放,因此对于这些类型,initializedeinitialize可以对这些类型执行donothing操作。
当您定义包含一些引用类型属性的值类型时,Swift会生成针对该类型专门的initializedeinitialize过程。
本地范围示例适用于在堆栈上分配的区域,而UnsafeMutablePointerinitialize()deinitialize()适用于在堆中分配的区域。
而且Swift正在迅速发展,您可能会发现将来需要initialize()deinitialize()的另一个原因,最好养成习惯,对任何Pointee类型的分配的所有UnsafeMutablePointer进行initialize()deinitialize()

那么调用 .initialize() 只是为了提供一个可以在第一次赋值时 swift_release() 的东西吗?这是可能的,但有点难以置信。 - CopperCash
我不同意...请看我的回答。Swift的Unsafe[Mutable]Pointer<T>由ARC维护,而不是.pointee。 - user3441734

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接