swift 指针学习记录

参考文献

Swift结构体指针操作
官方文档
Swift 和 C 不得不说的故事
Swift指针和托管,你看我就够了

Why?

swift中并不推荐对指针直接操作,但毕竟iOS中有的库还是使用C语言构建的,在与C混编的时候,不可避免的要将对象转换为C的指针类型以便传值。比如GCD,CF框架Runloop操作。

各种指针

对应表

下面表中Type为类型占位,可替换为int等等

对于返回值、变量、参数的指针:

C Swift example
const Type * UnsafePointer<Type> const int * 转换为UnsafePointer <Int32>
Type * UnsafeMutablePointer<Type> int * 转换为UnsafeMutablePointer <Int32>

对于类对象的指针

C Swift
Type * const * UnsafePointer<Type>
Type * __strong * UnsafeMutablePointer<Type>
Type ** AutoreleasingUnsafeMutablePointer<Type>

对于无符号类型、

C Swift
const void * UnsafeRawPointer
void * UnsafeMutableRawPointer

如果像不完整结构体的这样的c指针的值的类型无法用Swift来表示,则用OpaquePointer来表示

常量指针

定义为 UnsafePointer<Type> 的指针为常量指针,在C中有const修饰。

当函数的参数修饰为 UnsafePointer<Type>的时候,可以接收下面几种类型的值:

  • 类型为UnsafePointer<Type>, UnsafeMutablePointer<Type>, or AutoreleasingUnsafeMutablePointer<Type>或者转换为UnsafePointer<Type>类型的值。
  • String类型的值,如果类型是Int8UInt8,那字符串会自动转换为UTF8的buffer,然后这个buffer的指针将会传入函数。
  • 表达式Type类型的变量、属性、同类型的下标表达(如:a[0]),这样的情况下,将左边起始地址传入函数。
  • [Type]数组类型值,将数组起始地址传入函数。

在调用函数时,必须保证传入的值可用。

例子:

func takesAPointer(_ p: UnsafePointer<Float>) {
    // ...
}

var x: Float = 0.0
takesAPointer(&x)
takesAPointer([1.0, 2.0, 3.0])

上面的例子中必须定义时必须指定一个类型,如:Float。可以使用UnsafeRawPointer修饰,则可以传入相同类型的Type。

例子:

func takesARawPointer(_ p: UnsafeRawPointer?)  {
    // ...
}

var x: Float = 0.0, y: Int = 0
takesARawPointer(&x)
takesARawPointer(&y)
takesARawPointer([1.0, 2.0, 3.0] as [Float])
let intArray = [1, 2, 3]
takesARawPointer(intArray)

可变指针

定义为UnsafeMutablePointer<Type>、UnsafeMutableRawPointer的指针为可变指针,可以接收下面几种类型的值:

  • 类型为UnsafeMutablePointer<Type>类型的值。
  • 表达式Type类型的变量、属性、同类型的下标表达(如:a[0]),这样的情况下,将地址传入函数。
  • 表达式[Type]类型的变量、属性、同类型的下标表达,这样的情况下,将左边起始地址传入函数。
func takesAMutablePointer(_ p: UnsafeMutablePointer<Float>) {
    // ...
}

var x: Float = 0.0
var a: [Float] = [1.0, 2.0, 3.0]
takesAMutablePointer(&x)
takesAMutablePointer(&a)

// 不指定具体类型
func takesAMutableRawPointer(_ p: UnsafeMutableRawPointer?)  {
    // ...
}

var x: Float = 0.0, y: Int = 0
var a: [Float] = [1.0, 2.0, 3.0], b: [Int] = [1, 2, 3]
takesAMutableRawPointer(&x)
takesAMutableRawPointer(&y)
takesAMutableRawPointer(&a)
takesAMutableRawPointer(&b)

Autoreleasing Pointers(自动释放指针)

定义为AutoreleasingUnsafeMutablePointer<Type>,可以接收下面几种类型的值:

  • AutoreleasingUnsafeMutablePointer<Type>的值
  • 表达式Type类型的变量、属性、同类型的下标表达(如:a[0]),这里会按位拷贝到临时缓冲区,缓冲区的值会被加载,retain,分配值。

注意:不能传数组

例子:

func takesAnAutoreleasingPointer(_ p: AutoreleasingUnsafeMutablePointer<NSDate?>) {
    // ...
}

var x: NSDate? = nil
takesAnAutoreleasingPointer(&x)

函数指针

在C中有回调函数,当swift要调用C中这类函数时,可以使用函数指针。

swift中可以用@convention 修饰一个闭包,

  • @convention(swift) : 表明这个是一个swift的闭包
  • @convention(block) :表明这个是一个兼容oc的block的闭包,可以传入OC的方法。
  • @convention(c) : 表明这个是兼容c的函数指针的闭包,可以传入C的方法。

C中的方法int (*)(void) 在swift中就是@convention(c) () -> Int32

在调用C函数需要传入函数指针时,swift可以传入闭包的字面量或者nil,也可以直接传入一个闭包。

例如:

func customCopyDescription(_ p: UnsafeRawPointer?) -> Unmanaged<CFString>? {
    // return an Unmanaged<CFString>? value
}
 
var callbacks = CFArrayCallBacks(
    version: 0,
    retain: nil,
    release: nil,
    copyDescription: customCopyDescription,
    equal: { (p1, p2) -> DarwinBoolean in
        // return Bool value
}
)
 
var mutableArray = CFArrayCreateMutable(nil, 0, &callbacks)

上面的例子中,retain、release是传入的nil,copyDescription是传入的函数字面量,equal是直接传入的闭包。

Buffer Pointers

buffer指针用于比较底层的内存操作,你可以使用buffer指针做高效的处理和应用程序与服务间的通信。

有下面几种类型的buffer指针

  • UnsafeBufferPointer
  • UnsafeMutableBufferPointer
  • UnsafeRawBufferPointer
  • UnsafeMutableRawBufferPointer

UnsafeBufferPointer、 UnsafeMutableBufferPointer,能让你查看或更改一个连续的内存块。

UnsafeRawBufferPointer、UnsafeMutableRawBufferPointer能让你查看或更改一个连续内存块的集合,集合里面每个值对应一个字节的内存。

指针用法

当使用指针实例的时候,可以使用pointee属性获取指针指向内容的值,指针不会自动管理内存或对准担保。你必须自己管理生命周期以避免泄露和未定义的行为。

内存可能有几种状态:未指定类型未初始化、指定类型未初始化、指定类型已初始化。

  • 未分配的:没有预留的内存分配给指针
  • 已分配的:指针指向一个有效的已分配的内存地址,但是值没有被初始化。
  • 已初始化:指针指向已分配和已初始化的内存地址。

指针将根据我们具体的操作在这 3 个状态之间进行转换。

用法示例

未分配的指针用allocate方法分配一定的内存空间。

 let uint8Pointer = UnsafeMutablePointer<UInt8>.allocate(capacity: 8)

分配完内存空间的指针用各种init方法来绑定一个值或一系列值。初始化时,必须保证指针是未初始化的。(初始化过的指针可以再次调用初始化方法不会报错,所以使用时需要特别注意。)

uint8Pointer.initialize(to: 20, count: 8)
print(uint8Pointer[0])  // 20

然后修改值

uint8Pointer[0] = 10
print(uint8Pointer[0])  // 10

回到初始化值之前,没有释放指针指向的内存,指针依旧指向之前的值。

uint8Pointer.deinitialize(count: 8)

释放指针指向的内存,据官方文档说,在释放指针内存之前,必须要保证指针是未初始化的,不然会产生问题。

uint8Pointer.deallocate(capacity: 8)
print(uint8Pointer[0])  // 可能是任何值,已经销毁了

其他用法

看个栗子:

struct MyStruct1{
    var int1:Int
    var int2:Int
}

var s1ptr = UnsafeMutablePointer<MyStruct1>.allocate(capacity: 5)

s1ptr[0] = MyStruct1(int1: 1, int2: 2)
s1ptr[1] = MyStruct1(int1: 1, int2: 2) // 似乎不应该是这样,但是这能够正常工作

s1ptr.deinitialize(count: 5)
s1ptr.deallocate(capacity: 5)

这个栗子中没有使用init方法来初始化指针,但是可以正常工作。不过这样写并不推荐,它不适用指针指向一个类,或某些特定的结构体和枚举的情况。

why?

当你使用上面提及的方式修改内存内容,从内存管理角度来说,有关这种行为背后的原因和发生时有关的。让我们来看一个不需要手动初始化内存的代码片段,倘若我们在没有初始化 UnsafePointer 情况下改变了指针指向的内存,会引发崩溃。

class TestClass{
    var aField:Int = 0
}

struct MyStruct2{
    var int1:Int
    var int2:Int
    var tc:TestClass // 这个字段是引用类型
}

var s2ptr = UnsafeMutablePointer<MyStruct2>.allocate(capacity: 5)

s2ptr.initialize(to: MyStruct2(int1: 1, int2: 2, tc: TestClass()), count: 2) // 删除这行初始化代码将引发崩溃

s2ptr[0] = MyStruct2(int1: 1, int2: 2, tc: TestClass())
s2ptr[1] = MyStruct2(int1: 1, int2: 2, tc: TestClass())

s2ptr.deinitialize(count: 5)
s2ptr.deallocate(capacity: 5)

MyStruct2 包含一个引用类型,所以它的生命周期交由 ARC 管理。当我们修改其中一个指向的内存模块值的时候,Swift 运行时将试图释放之前存在的对象,由于这个对象没有被初始化,内存存在垃圾,你的应用将会崩溃。

请牢记这一点,从安全的角度来讲,最受欢迎的初始化手段是使用 initialize 分配完成内存后,直接设置变量的初始值。

转换

可变 不可变

当一个函数需要传入不可变指针时,可变指针可以直接传入。

而当一个函数需要可变指针时,可以使用init(mutating other: UnsafePointer<Pointee>)方法转换

var i: Int8 = 12
func printPointer(p: UnsafePointer<Int8>) {
    let muS2ptr = UnsafeMutablePointer<Int8>.init(mutating: p)!
    print(muS2ptr.pointee)
}
printPointer(p: &i)

各种类型转换:

var i: Int8 = 12
func printPointer(p: UnsafePointer<Int8>) {
    let muS2ptr = UnsafeMutablePointer<Int8>.init(mutating: p)!  //  UnsafePointer<Int8> -> UnsafeMutablePointer<Int8>
    print(muS2ptr.pointee)
    
    var constUnTypePointer = UnsafeRawPointer(p) // UnsafePointer<Int8> - > UnsafeRawPointer
    var unTypePointer = UnsafeMutableRawPointer(mutating: constUnTypePointer) // UnsafeRawPointer -> UnsafeMutableRawPointer
    var unTypePointer2 = UnsafeMutableRawPointer(muS2ptr) // UnsafeMutablePointer<Int8> ->  UnsafeMutableRawPointer
    
}

指针可以使用load等方法转为对应的类型

func print<T>(address p: UnsafeRawPointer, as type: T.Type) {
    let value = p.load(as: type)
    print(value)
}

不同类型

下面的例子展示了将Uint8的指针 转换为UInt64类型。

var i: UInt8 = 125
func printPointer(uint8Pointer: UnsafePointer<UInt8>) {
    
    let pointer0 = UnsafeRawPointer(uint8Pointer)
        .bindMemory(to: UInt64.self, capacity: 1)
    let pointer0Value = pointer0.pointee
    print(pointer0Value) // UInt64
    
    let pointer1 = UnsafeRawPointer(uint8Pointer).assumingMemoryBound(to: UInt64.self)
    let pointer1Value = pointer1.pointee
    print(pointer1Value) // UInt64
    
    let pointer2Value = UnsafeRawPointer(uint8Pointer).load(as: UInt64.self)
    print(pointer2Value) // UInt64
    
    let pointer3 = UnsafeMutablePointer(mutating: uint8Pointer).withMemoryRebound(to: UInt64.self, capacity: 1) { return $0 }
    print(pointer3.pointee) // UInt64
}
printPointer(uint8Pointer: &i)

对象转换

在调用一些底层库的时候,经常要把Swift对象传入C中,然后将从C中的回调函数中转换成Swift对象。

下面是我自己写的一个例子:

定义的C代码是:

// c的头文件
#include <stdio.h>

typedef struct {
    void *info;
    const void *(*retain)(const void *info);
}Context;

void abcPrint(Context *info, void (*callback)(void *));
    
// c 的实现文件
void abcPrint(Context *info, void (*callback)(void *)){
    
    (*callback)(info->info);
    printf("abcPrint call");
}

上面的代码context中有个info可以带void *对象, 然后在回调方法中将info对象返回回来。

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
        
        var blockSelf = self
        let controllerPoint = withUnsafeMutablePointer(to: &blockSelf) { return $0}
        print("\(self)")
        
        var context = Context(info: controllerPoint, retain: nil)
        
        abcPrint(&context) {
            let controller1 = $0?.assumingMemoryBound(to: ViewController.self).pointee
            print("controller1: \(String(describing: controller1))")
            
            let controller2 = $0?.bindMemory(to: ViewController.self, capacity: 1).pointee
            print("controller2: \(String(describing: controller2))")
            
            let controller3 = $0?.load(as: ViewController.self)
            print("controller3: \(String(describing: controller3))")
        }
    }
}

withUnsafeMutablePointer方法可以将Swift对象ViewController转换为UnsafeMutablePointer<ViewController>类型,这样才可以当做参数传入C函数。

C函数的回调函数中,传出来一个UnsafeMutableRawPointer对象的指针,我展示了3种方式,可以将这个指针转换为ViewController对象。

CFRoopLoop使用

相信有的人看到上面的代码,知道我为什么会写这个知识点了。因为我在使用CFRunLoop优化大图加载的时候遇到了这样的一个需求。

当创建CFRunLoopObserverContext时需要传入Swift对象到info属性,在CFRunLoopObserverCreate的回调函数中,会把这个info返回回来使用。这里就需要这样的类型转换。

Unmanaged方式

苹果的一些底层框架返回的对象有的是自动管理内存的(annotated APIs),有的是不自动管理内存。

  • 对于Core Fundation中有@annotated注释的函数来说,返回的是托管对象,无需自己管理内存,可以直接获取到CF对象,并且可以无缝转化(toll free bridging)成Fundation对象,比如NSString和CFString。目前,内存管理注释正在一步步的完善,所以等到未来某一个版本我们就可以完完全全的像使用Fundation一样使用Core Fundation啦。

  • 对于尚未注释的函数来说,苹果给出的是使用非托管对象Unmanaged<T>进行管理的过渡方案。
    当我们从CF函数中获取到Unmanaged<T>对象的时候,我们需要调用takeRetainedValue或者takeUnretainedValue获取到对象T。具体使用哪一个方法,苹果提出了Ownership Policy,具体来说就是:

    1. 如果一个函数名中包含CreateCopy,则调用者获得这个对象的同时也获得对象所有权,返回值Unmanaged需要调用takeRetainedValue()方法获得对象。调用者不再使用对象时候,Swift代码中不需要调用CFRelease函数放弃对象所有权,这是因为Swift仅支持ARC内存管理,这一点和OC略有不同。
    2. 如果一个函数名中包含Get,则调用者获得这个对象的同时不会获得对象所有权,返回值Unmanaged需要调用takeUnretainedValue()方法获得对象。

下面是我自己的试验的代码:

func addRunloopOberver1() {
        
        let controllerPoint = Unmanaged<ViewController>.passUnretained(self).toOpaque()
        
        var content = CFRunLoopObserverContext(version: 0, info: controllerPoint, retain: nil, release: nil, copyDescription: nil)
        
        runloopObserver = CFRunLoopObserverCreate(nil, CFRunLoopActivity.beforeWaiting.rawValue, true, 0, { (oberver, activity, info) in
            
            if info == nil {//如果没有取到  直接返回
                return
            }
            
            let controller = Unmanaged<ViewController>.fromOpaque(info!).takeUnretainedValue()
            
            
            if controller.isKind(of: PicturesDetailViewController.self) {
                controller.runloopCall()
            }
            
            
        }, &content)
        
        CFRunLoopAddObserver(runloop, runloopObserver, CFRunLoopMode.commonModes)
    }

注意: 如果这里使用takeRetainedValue 、passRetained方法,会崩溃,我理解的是标明为retain的值,在使用后ARC会自动将它release一次,这样在某个时候self对象就会被释放了,当runloop再次用到self的时候就会崩溃。如果用unretain的值,ARC就不会去retain和release这个指针对象。

不建议的方式

看下面代码:

func addRunloopOberver() {
        
        let controllerPoint = unsafeBitCast(self, to: UnsafeMutableRawPointer.self)
        
        var content = CFRunLoopObserverContext(version: 0, info: controllerPoint, retain: nil, release: nil, copyDescription: nil)
        
        runloopObserver = CFRunLoopObserverCreate(nil, CFRunLoopActivity.beforeWaiting.rawValue, true, 0, { (oberver, activity, info) in
            
            if info == nil {//如果没有取到  直接返回
                return
            }
            
            let controller = unsafeBitCast(info, to: PicturesDetailViewController.self)
            
            if controller.isKind(of: ViewController.self) {
                controller.runloopCall()
            }
            
            
        }, &content)
        
        CFRunLoopAddObserver(runloop, runloopObserver, CFRunLoopMode.commonModes)
    }

上面使用了unsafeBitCast方法,强行将self对象转换为指针,最后也是强行转换回来。虽然说也可以运行,但是官方文档中建议,不到万不得已不要使用这个方法,特别危险。。

下面是官方文档原文
Use this function only to convert the instance passed as x to a layout-compatible type when conversion through other means is not possible. Common conversions supported by the Swift standard library include the following:

Warning
Calling this function breaks the guarantees of the Swift type system; use with extreme care.

  • Value conversion from one integer type to another. Use the destination type’s initializer or the numericCast(_:) function.
  • Bitwise conversion from one integer type to another. Use the destination type’s init(truncatingIfNeeded:) or init(bitPattern:) initializer.
  • Conversion from a pointer to an integer value with the bit pattern of the pointer’s address in memory, or vice versa. Use the init(bitPattern:) initializer for the destination type.
  • Casting an instance of a reference type. Use the casting operators (as, as!, or as?) or the unsafeDowncast(:to:) function. Do not use unsafeBitCast(:to:) with class or pointer types; doing so may introduce undefined behavior.
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350

推荐阅读更多精彩内容