深度探究HandyJSON(一) Swift 指针的使用

为了避免疏漏, 我从官方文档作了截图, 苹果官网文档1 , 文档2

来源于Xcode.png

本文概要

  • 按照官方文档, 介绍Swift中的指针, 包括 Typed Pointers, Raw Pointers, Memory Access, Memory Layout, Reference Counting.
  • 打印 class 和 struct 对象地址
  • 介绍通过指针, 如何为对象的属性设值.

第一部分: Swift中的指针

Swift中的指针分为两类, typed pointer 指定数据类型指针, raw pointer 未指定数据类型的指针(原生指针)

下面是CSwift关于指针的对照表:
C Syntax Swift Syntax Note
const Type * UnsafePointer<Type> 指针可变,指针指向的内存值不可变。
Type * UnsafeMutablePointer<Type> 指针和指针指向的内存值均可变。
ClassType * const * UnsafePointer<ClassType> 指针的指针:指针不可变,指针指向的类可变。
ClassType * __strong * UnsafeMutablePointer<ClassType> 指针的指针:指针和指针指向的类均可变。
ClassType ** AutoreleasingUnsafeMutablePointer<Type> 作为OC方法中的指针参数
const void * UnsafeRawPointer 指针指向的内存区类型未定。
void * UnsafeMutableRawPointer 同上
StructType * OpaquePointer C 语言中的一些自定义类型,Swift 中并未有相对应的类型。
int8_t a[] var x:[Int8] -> UnsafeBufferPointer Buffer 一词不难联想到数组

Typed pointers

UnsafePointer 与 UnsafeMutablePointer

UnsafePointer / UnsafeMutablePointer 实例引用的内存可以处于多种状态之一. 许多指针操作只能应用于内存处于特定状态. 这同样应用于 UnsafeRawPointer, UnsafeRawBufferPointer.

  1. untyped and uninitialized (未分配内存)
  2. typed and uninitialized (未初始化)
  3. typed and initialized (已初始化)
  4. 先前分配的内存可能已被解除分配,使现有指针引用未分配的内存 (未分配内存)
// 分配内存
let a = UnsafeMutablePointer<Int>.allocate(capacity: sizeof_sfntInstance)
// 初始化
a.initialize(to: 10)
// 设值
a.pointee = 10
print(a.pointee)
// 释放内存
a.deallocate(capacity: 1)
  • 分配内存, 在指定内存容量时, 可以手动指定为sizeof_sfntInstance, 由系统决定实例对象占多大内存空间, 这里值 为 4个字节.
  • 对于Int, Float, Double这些基本数据类型, 可以不进行initialize操作, 因为分配内存之后会有默认值 0
  • 对于引用类型类型, 必须进行initialize操作, 才能在内存中写入值, 否则编译器报错.
  • 每一个Pointer 对象都有一个属性pointee, 可以通过这个属性直接获取指针指向内存中的值.
  • 有分配内存, 最好在不需要的时候主动释放内存

将指针引用的内存作为不同的类型访问

  1. 将内存临时重新绑定到其他类型.
var uint8: UInt8 = 123
let unit8Pointer = UnsafeMutablePointer(&uint8)
unit8Pointer.withMemoryRebound(to: Int8.self, capacity: 8) {
    $0.pointee  // 123
}
  1. 将内存永久重新绑定到其他类型
    这里我们用到指向内存的原始指针
let uint64Pointer = UnsafeRawPointer(unit8Pointer).bindMemory(to: UInt64.self, capacity: 1)
uint64Pointer.pointee  // 123
  1. 为了访问不同类型的相同内存, 只需绑定类型, 和目标类型是普通类型即可
var uint64Num: UInt64 = 257
let rawPointer = UnsafeRawPointer(UnsafeMutablePointer(&uint64Num))
let fullInteger = rawPointer.load(as: UInt64.self)  // 257
let firstByte = rawPointer.load(as: UInt8.self)     // 1

这里需要解释一下, 为什么 257 在这里以 UIn8 类型访问是 1 呢.
UInt8 表示存储8个字节的无符号整数, 2^8 = 256, [0, 255]
Int8 表示存储8个字节的整数, 范围 [-128, 127]

UInt8: [0, 255]  Int8: [-128, 127]
UInt16: [0, 65535], Int16: [-32768, 32767]
UInt32: [0, 4294967295], Int32: [-2147483648, 2147483647]
UInt64: [0 , 9223372036854775807],
Int64: [-9223372036854775808, 9223372036854775807]

257 (10) = 1 0000 0001 (2)
rawPointer 以 UInt8 类型加载数据 257 时, 只能加载到 1 , 超出8个字节范围的无法加载.

UnsafeBufferPointer 与 UnsafeMutableBufferPointer

  • 用于连续存储在内存中的元素缓冲区, UnsafeBufferPointer 用于处理不可变的元素缓冲区, UnsafeMutableBufferPointer 用于处理可变的元素缓冲区.
  • 这两者的实例是内存视图, 不拥有它引用的内存, 即 复制UnsafeBufferPointer (UnsafeMutableBufferPointer) 类型的值不会复制存储在基础内存中的实例.
  • BufferPointer / UnsafeMutableBufferPointer 实现了Collection ,因此可以直接使用Collection中的各种方法来遍历操作数据,filter,map...,Buffer可以实现对一块连续存在空间进行操作.

遍历数组

var array = [1,2,3,4]
let ptr = UnsafeBufferPointer(start: &array, count: sizeof_sfntInstance)
ptr.forEach {
    print("\($0)") // 1 2 3 4
}
        
array.withUnsafeBufferPointer({ptr in
    ptr.forEach({
        print("\($0)") // 1 2 3 4
    })
})

Raw Pointers

UnsafeRawPointer 与 UnsafeRawBufferPointer

UnsafeRawPointer 用于 访问 非类型化数据的原始指针.
UnsafeRawBufferPointer 用于 访问操作 非类型化数据的原始指针.

刚刚分配的原始内存处于未初始化的无类型状态。未初始化的内存必须先使用类型的值进行初始化,然后才能与任何类型的操作一起使用.
要将未初始化的内存绑定到类型而不初始化它, 使用bindMemory(to:count :) 方法, 此方法返回一个类型化指针, 以进一步对内存进行类型化访问.

var uint64Num: UInt64 = 257
let rawPointer = UnsafeRawPointer(UnsafeMutablePointer(&uint64Num))
let fullInteger = rawPointer.load(as: UInt64.self)  // 257
let firstByte = rawPointer.load(as: UInt8.self)     // 1

使用原始指针的指针算法在字节级执行

// 分配内存
let bytePointer = UnsafeMutableRawPointer.allocate(byteCount: 4, alignment: 1)
// 将给定值的字节存储在指定偏移量的原始内存中
bytePointer.storeBytes(of: 0xFFFF_FFFF, as: UInt32.self)

// 从 bytesPointer 引用的内存中加载值
let x = bytePointer.load(as: UInt8.self)// 255

// 从最后两个分配的字节加载一个值
let offsetPointer = bytePointer + 2  // bytePointer 偏移 2 个字节
let y = offsetPointer.load(as: UInt16.self)  // 65535

// 将值 0xFFFF_FFFF 存储到四个新分配的字节中,
// 将第一个字节作为UInt8实例加载
// 将第三个和第四个字节作为UInt16实例加载

// 释放分配的内存
bytePointer.deallocate()

需要说明的是
0xFFFF_FFFF = 1111 1111__1111 1111__1111 1111__1111 1111

UnsafeRawBufferPointer 与 UnsafeMutableRawBufferPointer

  1. UnsafeRawBufferPointer / UnsafeMutableRawBufferPointer 实例是内存区域中原始字节的视图.
  2. 内存中的每个字节都被视为一个UInt8值, 与该内存中保存的值的类型无关
  3. 通过原始缓冲区从内存中读取是一种无类型操作, UnsafeMutableRawBufferPointer 实例可以写入内存, UnsafeRawBufferPointer 实例不可以

要通过类型化操作访问底层内存,必须将内存绑定到一个简单的类型.

Memory Access

  • withUnsafePointer(to:_:)
  • withUnsafeMutablePointers(_:)
  • withUnsafeBytes(_:)
  • withUnsafeMutableBytes(_:)

这是 Swift 提供的几个用于处于指针的方法, 共同点在于如果需要返回值供外界使用, 直接 return, 否则, 无需 return.

var a = 0
a = withUnsafePointer(to: &a, { (ptr) in
    return ptr.pointee + 2
    // 此时, 会新开辟空间, 令a指向新地址, 值为2,
})

// 修改指针指向的内存值
var a = 42
withUnsafeMutablePointer(to: &a) { 
    $0.pointee += 100   // 未开辟新的内存空间, 直接修改a所指向的内存值
}
print(a)   // 142

// 同理    
var arr = [1, 2, 3]
withUnsafeMutablePointer(to: &arr) {ptr in
    ptr.pointee[0] = 10
}
print(arr)   // [10, 2, 3]

Buffer pointer
arr.withUnsafeBufferPointer({ptr in
    ptr.forEach({
       print("\($0)")  // 1 2 3 
     })
})

// 修改内存值
array.withUnsafeMutableBufferPointer { mptr in
    mptr[0] = 100
}

ptr.forEach {
    print("\($0)") // 100 2 3
}

// 打印字符串
let str = "hello"
let strData = str.data(using: .ascii)
strData?.withUnsafeBytes({ (ptr: (UnsafePointer<Int8>)) in
      print(ptr.pointee) // 104 = 'h'
})

//  ASCII <==> String
let h_ASCII = UnicodeScalar("h")?.value // 104
let h = Character(UnicodeScalar(104)) // h

// 交换两个指针对应的内存值
var arr = [4, 5, 6]
var arr2 = [100, 101, 102]
swap(&arr, &arr2)
print(arr, arr2)   // [100, 101, 102], [4, 5, 6]

Reference Counting

Unmanaged

Unmanaged 是一个 Structure, 用来管理Unmanaged 对象的引用, 类似于OC 中对象的引用计数管理对象内存.

打印 class 对象的地址

Swift中的单例

class Singleton {
    static let instance = Singleton()
    private init() {}
}
let instance = Singleton.instance
// 将 Singleton 对象引用 转化为 unmanaged 对象引用
let unmanagedObj = Unmanaged.passUnretained(instance as AnyObject)
// 将 unmanaged class reference 转化 为pointer
let ptr = unmanagedObj.toOpaque()
// 打印内存地址
print(ptr.debugDescription)

还有另外一种方法打印 class 对象地址

let instance = Singleton.instance
// 类型转换: 将 instance 引用转换为 Int 类型
let addressValue = unsafeBitCast(instance as AnyObject, to: Int.self)
// 转化为16进制字符串
print(String(addressValue,radix: 16))

用 struct 能创建单例对象吗? 像下面这样.

struct Person {
    static let instance = Person()
    private init() {}
}

测试一下

var p1 = Person.instance
var p2 = Person.instance

// 打印 struct 的内存地址
func address_struct(o: UnsafeRawPointer) -> String {
    return String(Int(bitPattern: o), radix: 16)
}

print("p1: ", address_struct(o: &p1))  // p1:  1004e3670
print("p2: ", address_struct(o: &p2)) // p2:  1004e3675

很明显, 这两个地址值并不一样, 所以 struct 是不能创建单例的. 因为这是值语义, 每次都会创建一个新的实例, 并且采用的 写时复制 (copy on write) 的机制进行优化.

有几点需要说明

  1. 以前是通过 String(unsafeBitCast(point, to: Int.self), radix: 16) 这种方式转化struct对象为内存地址, 但是这已经过时了. 现在采用 String(Int(bitPattern: pointer), radix: 16)
  2. Person 对象是存储在栈空间, 而不是堆空间的.
  3. struct 是值传递, 不能创建单例, 每次调用都是一个新的对象, 并且存储在栈中, class 是值引用, 它可以创建单例. 并且存储在堆中.

Memory Layout

Memory Layout

MemoryLayout 是 一个 Enumeration 的, 它是一个类型的内存布局,描述其大小,步幅和对齐方式.

struct Person {
    var name: String = "jack"
    var age: Int = 18
    var isBoy: Bool = true
    var height: Double?
    
}

class PersonClass {
    var name: String = "Lucy"
    var age: Int = 18
    var isBoy: Bool = false
    var height: Double?
}

MemoryLayout<Person>.size // 41 = 16 + 8 + 1 + 16
MemoryLayout<Person>.stride  // 48
MemoryLayout<Person>.alignment // 8

MemoryLayout<PersonClass>.size      // 8
MemoryLayout<PersonClass>.stride    // 8
MemoryLayout<PersonClass>.alignment // 8

这里有几点说明一下:

  • String 类型占 16 个字节, Int 类型占 8 个字节, Bool 类型占 1 个字节, Double 类型占 8 个字节, 由于可选类型占 1 个字节, 以及内存对齐的限制, 导致增加了8个字节的存储空间, 最终占16 个字节.

  • size 指 Person 实例连续内存占用, stride 指存储在连续内存或Array 中时, 从Person 的实例的开始到下一个实例的开始的字节数.

  • alignment, 默认内存对齐方式.

    struct 实例内存分布

  • class 是引用类型,生成的实例分布在 Heap(堆) 内存区域上,在 Stack(栈)只存放着一个指向堆中实例的指针.
    所以MemoryLayout<PersonClass>.size 才是 8, 这里是栈中存放的指针的大小.

  • 因为考虑到引用类型的动态性和 ARC 的原因,class 类型实例需要有一块单独区域存储类型信息和引用计数(meta数据).

class 实例内存分布

通过指针为属性赋值

既然知道了类的实例在内存中的分布情况,

  1. 我们可以通过指向实例的起始指针,
  2. 找到每一个属性所对应的内存,
  3. 通过指针在对应内存中写入值.

为 struct Person 的 属性赋值

var personStruct = Person()
// 获得头部指针
let pStructHeadP = withUnsafeMutablePointer(to: &personStruct, {
    return UnsafeMutableRawPointer($0).bindMemory(to: Int8.self, capacity: MemoryLayout<Person>.stride)
})
// 将 headPointer 转化为 rawPointer, 方便移位操作
let pStructHeadRawP = UnsafeMutableRawPointer(pStructHeadP)

// 每个属性在内存中的位置
let namePosition = 0
let agePosition = namePosition + MemoryLayout<String>.stride
let isBoyPosition = agePosition + MemoryLayout<Int>.stride

// 将内存临时重新绑定到其他类型进行访问.
let namePtr = pStructHeadRawP.advanced(by: 0).assumingMemoryBound(to: String.self)
let agePtr = pStructHeadRawP.advanced(by: agePosition).assumingMemoryBound(to: Int.self)
let isBoyPtr = pStructHeadRawP.advanced(by: isBoyPosition).assumingMemoryBound(to: Bool.self)

// 设置属性值
namePtr.pointee = "lily"
agePtr.pointee = 20
isBoyPtr.pointee = false

// 测试
personStruct.name     // "lily"
personStruct.age      // 20
personStruct.isBoy    // false

为 PersonClass 的属性设置

var personClass = PersonClass()
// 获得头部指针
let pClassHeadRawP = Unmanaged.passUnretained(personClass as AnyObject).toOpaque()

// 获取每个属性在内存中的位置
let namePosition2 = 16
let agePosition2 = namePosition + MemoryLayout<String>.stride + 16
let isBoyPosition2 = agePosition + MemoryLayout<Int>.stride + 16

// 将内存临时重新绑定到其他类型进行访问.
let namePtr2 = pClassHeadRawP.advanced(by: namePosition2).assumingMemoryBound(to: String.self)
let agePtr2 = pClassHeadRawP.advanced(by: agePosition2).assumingMemoryBound(to: Int.self)
let isBoyPtr2 = pClassHeadRawP.advanced(by: isBoyPosition2).assumingMemoryBound(to: Bool.self)

// 设置属性值
namePtr2.pointee = "maris"
agePtr2.pointee = 38
isBoyPtr2.pointee = true

// 测试
personClass.name  // "maris"
personClass.age     // 38
personClass.isBoy  // true

测试发现, 即使是为常量, 也是可以通过指针修改内存中的属性值.

其他

  • 在上一篇中, 我们通过Runtime的方式, 获取到程序运行中加载的所有class, 用代理模式, 通过匹配遵循协议的代理类, 来判断是否执行代理方法, 在这个方法中, 执行目标方法
// 定义一个指针, 分配内存, 指针和指针指向的类均可变。
let types = UnsafeMutablePointer<AnyClass>.allocate(capacity: typeCount)
// 创建 AutoreleasingUnsafeMutablePointer
let autoreleasingTypes = AutoreleasingUnsafeMutablePointer<AnyClass>(types)
// 将原缓存区的数据拷贝到types所指向的那块内存
objc_getClassList(autoreleasingTypes, Int32(typeCount))

AutoreleasingUnsafeMutablePointer 这个指针通常是用来作为OC方法中的指针参数, 在Swift中调用.

参考

Swift Pointer 使用指南
Pointer In Swift
Swift内存赋值探索一: 理解对象在内存中的存储状态
Swift 对象内存模型探究(一)

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

推荐阅读更多精彩内容