随笔 - 48  文章 - 0  评论 - 0  阅读 - 11657

Swift-指针

 Swift-指针

一、指针

1.指针的不安全性

1.1.1 野指针

什么是野指针;我们在创建对象时需要在堆上分配内存空间,但内存空间的生命周期是有限的,如果我们使用指针指向这块空间,如果后续内存空间的额生命周期结束(当引用计数为0时),那么此时的指针就成为了野指针。

1.1.2 指针越界

什么是指针越界:我们都知道内存空间是有边界的,如果存在一个大小为10的数组,我们通过指针可以访问index = 11的位置时候,指针就出现了越界的情况。

1.1.3 类型不一致

什么是类型不一致;比如说存储的内容为Int类型,但是如果指针式Int8类型,就会导致精度损失。

1.2指针类型

1.2.1 数据类型计算工具

  • MemoryLayout<Int>.size MemoryLayout<Int>.stride MemoryLayout<Int>.alignment
  •  size:计算数据类型的实际大小
  • stride:计算数据的步长(连续存储,从当前实例的开始位置到下一个实例的开始位置的内存长度)
  • aligment:字节对齐方式(内存会自动对齐)

下面我们通过一个例子来说明:

复制代码
struct LGTeacher{
    var age: Int = 18
    var name: Bool = true
}
print(MemoryLayout<LGTeacher>.size)
print(MemoryLayout<LGTeacher>.stride)
print(MemoryLayout<LGTeacher>.alignment)
//9 INT是8个字节、BOOL是1个字节
//16 按照8字节对齐后,步长是两个8字节
//8 对齐以8字节对齐
复制代码

1.2.2常用的指针类型

  • 常用的指针类型,带<T>表示数据类型指针,带Raw表示表示原生指针,带Buffer表示连续内存地址

 

 

1.2.3 raw pointer(原生指针) 

  • UnsafeMutableRawPointer(可修改原生指针):可用StoreBytes和load进行数据的存储与读取
复制代码
let p = UnsafeMutableRawPointer.allocate(byteCount: 32, alignment: 8)
for i in 0..<4{
    p.advanced(by: i * MemoryLayout<Int>.stride).storeBytes(of: i, as: Int.self)
}
for i in 0..<4{
    let value = p.load(fromByteOffset: i*8, as: Int.self)
    print("index:\(i),value:\(value)")
}
p.deallocate()
//

index:0,value:0

index:1,value:1

index:2,value:2

index:3,value:3

复制代码
  • allocate(byteCount: 32, alignment: 8)表示创建一个X字节的容量,用X字节对齐方式的内存空间
  • advanced(by:)表示每次向后移动X字节
  • storeBytes(of: i, as:T)将T类型的值i存到指针中
  • load(fromByteOffset: i*8, as: T)表示偏移X字节来读取T类型的值
  • deallocate()表示回收内存空间

1.2.4 typed pointer(泛型指针)

  • 通过withUnsafePointer等来访问当前变量地址,数据存取不再通过storeBytes和load进行,而是通过操作当前泛型指针内置的变量pointee
  • unsafePointer的两种获取方式(通过已有变量获取、或者分配内存)

第一种:通过已有变量获取

var age = 20
age = withUnsafePointer(to: &age){ ptr in
    ptr.pointee + 10 //ptr指针的值
print(age)
//30

第二种:通过直接分配内存获取

复制代码
var ptr = UnsafeMutablePointer<LGTeacher>.allocate(capacity: 5)
ptr[0] = LGTeacher(age: 10, name: true)
ptr[1] = LGTeacher(age: 11, name: false)
print("\(ptr)\n,\(ptr.pointee)\n,\(ptr[0])\n,\(ptr[1])\n")
ptr.deinitialize(count: 5)
ptr.deallocate()
//
0x00000001013259c0
,LGTeacher(age: 10, name: true)
,LGTeacher(age: 10, name: true)
,LGTeacher(age: 11, name: false)
复制代码
  • allocate(capacity: 5)表示创建多大的空间
  • deinitialize(count: 5)表示将这五个空间抹零
  • deallocate()释放

第三种:通过advanced分配内存

var ptr = UnsafeMutablePointer<LGTeacher>.allocate(capacity: 5)
ptr.initialize(to: LGTeacher())
ptr.advanced(by: MemoryLayout<LGTeacher>.stride).initialize(to: LGTeacher())
print(ptr.pointee)
ptr.deinitialize(count: 5)
ptr.deallocate()
//
LGTeacher(age: 18, name: true)

二、内存绑定(Swift有三种绑定/重新绑定的API)

2.1 assumingMemoryBound(to:)

  • 目的:绕过编译器检查并省去繁琐的类型转换
  • 使用场景:我们在处理代码中,只有原始指针(没有指针类型),但此刻我们明确知道指针的类型,那么我们可以通过这个API告诉编译器预期的类型
  • 并未发生实际类型的转换
复制代码
var tuple = (10,20)
withUnsafePointer(to:tuple){(tuplePtr:UnsafePointer<(Int,Int)>) in
    printMemory(ptr: UnsafeRawPointer(tuplePtr).assumingMemoryBound(to: Int.self))
}
func printMemory(ptr:UnsafePointer<Int>){
    print(ptr[0])
    print(ptr[1])
}
//
10
20
复制代码
  • UnsafeRawPointer(tuplePtr).assumingMemoryBound(to: Int.self))先将tuple转化为原生指针,使用assumeMemoryBound来告诉编译器类型,此时编译器就不会作出检查了。

2.2 bindMemory(to:capaity)

  • 目的:更改内存绑定的类型
  • 如果内存没有绑定类型,则首次绑定该类型,否则重新绑定,并且内存中的所有值都会变为该类型
  • 发生类型的转换

2.3 withMemeoryRebound(to:capaity:body:)

  • 目的:临时更改绑定类型
  • 当我们给娃补函数传递参数时,可能会存在数据类型不一致的问题,如果我们进行类型转化,会产生数据的来回复制,这个时候我们就可以用withMemeoryRebound,更改的类型仅在作用域内生效
复制代码
let Int8Ptr = UnsafePointer<Int8>.init(bitPattern: 20)
Int8Ptr?.withMemoryRebound(to: UInt8.self, capacity: 1, {(UInt8Ptr:UnsafePointer<UInt8>)in
    printMemory(ptr:UInt8Ptr)
})
  
func printMemory(ptr:UnsafePointer<UInt8>){
    print(ptr)
}
//
0x0000000000000014(16进制下的20)
复制代码
  • 这里解释一下,因为向printMemory传递参数的时候需要参数转换,此时就可以用临时绑定的API。

三、内存管理(从引用计数的角度出发)

  • Swift中使用自动引用计数(ARC)机制来追踪和管理内存,那么我们都知道Swift对象是有Metadata和refcounts组成的,那么refcounts就是管理引用计算的工具。
  • 查看refcounts需要用到swift对象的内存指针,他位于后8个字节(16进制中2位为一个字节)
  • 对于实例对象的内存指针的查看,我们可以用 Unmanaged.passRetained(XXX as AnyObject).toOpaque()来打印
let teacher = LGTeacher()
print(Unmanaged.passRetained(teacher as AnyObject).toOpaque())
//
0x0000000101531f70
  • 我们来观察一下对象内存地址中内容的变化
let teacher = LGTeacher()
print(Unmanaged.passRetained(teacher as AnyObject).toOpaque())
let t1 = teacher
print(Unmanaged.passRetained(teacher as AnyObject).toOpaque())
let t2 = teacher
print(Unmanaged.passRetained(teacher as AnyObject).toOpaque())
print()

 

  •  红色标出的就是refcount的位置,我们可以发现每产生一次引用时,refcount就发生了变化。

3.1 强引用

  • 我们在HeapOject.h中可以发现refcounts属于InlineRefCounts的宏定义(Swift源码可以在github中找到)

 

 

 我们通过查看lnlineRefCount内容发现

  • InlineRefCounts传入的是InlineRefCountBits
  • SideTableRefCounts(弱引用使用),传入SideTableRefCountBits

 

 

 

 

  • 接下来我们查看一下RefCounts的模版结构 

  • 接下来,我们看一下RefCountsBits 

 

 

 

  •  BitsType宏定义的bits,后续操作都是关于这个参数的,由此我们可以得知,无论是swift还是OC里面的引用计数中都有一个64位的位域信息,在这个64位中中存储了当前类运行的生命周期相关的引用计数。
  • 那么引用计数是怎么增加的呢,我们接下来看一个实例引用计数的变化,首先找到他的创建方法

  • 进入方法查看

 

  • 接下来,我们看一下什么是RefCountBits

  • 这里其实是对控制台输出的引用计数+1的值的计算方式,那接下来我们看看引用计数是怎么+1的

 

 

3.2 弱引用

  • swift提供了两种方式来避免循环引用的问题(无主引用、弱引用)
  • 弱引用不会持有实例,在实例被释放时,可能存在弱引用仍然引用着这个实例,因此,ARC会在实例被释放时自动设置弱引用为nil,这同时也意味着弱引用可以是可选类型
  • 声明一个weak变量相当于定义了一个WeakRefence对象,就会形成一个散列表
  • 下面我们先看一个强引用的例子
复制代码
class LGTeacher{
    var age:Int = 19
    var subject:LGSubject?
}
class LGSubject{
    var name:String
    init(name:String){
        self.name = name
    }
var t = LGTeacher()
var s = LGSubject.init(name: "swift")
t.subject = s
复制代码
  • 此时,我们就可以通过一个弱引用来解决这个问题(weak var t = XXX)
  • 通过汇编我们可以发现弱引用会走一个swift_weakinit的方法

 

 

  • 创建一个弱引用时,系统会创建一个Side Table,Side Table是一种类名,为HeapObjectSideTableEntry的结构,里面有RefCounts成员,内部是SideTableRefCountBits,其实就是原来的uint64_t加上一个存储弱引用数的uint32_t

3.3 无主引用

  • 无主引用不能设置为nil,不是一个可选类型
  • 如果弱引用双方的生命周期之间没有任何关联,就是用weak
  • 如果一个对象销毁,另一个对象也跟着销毁,就要使用unowned
  • 一般来说,我们都是用weak,但是unowned的性能更好,他不需要再创建一个散列表,直接对refcounts的64位操作即可

四、闭包循环引用

  • 闭包内部对变量的修改会导致外部原始变量值的改变
  • 如果我们在class内部定义了一个闭包,当前闭包在访问属性的过程中,就会对我们当前的实例对象进行捕获,如果实例又调用了闭包就会产生循环引用
class LGTeacher{
    var age = 18
    var complateCallBack:(()->())?
}
let t = LGTeacher()
t.complateCallBack{
    t.age += 1
}
  • 如果要避免这类问题,就需要使用weak/unowned来修饰捕获列表
t.complateCallBack = {[Unowned t] in
    t.age += 1
}

五、捕获列表

  • 默认情况下,闭包表达式从周围环境中捕获常量和变量,并强引用这些值,可以使用捕获列表来显式控制如何在闭包中捕获该值
  • 在参数列表之前,捕获列表被写为用逗号括起来的表达式,并用方括号括起来,如果使用捕获列表,则即使省略参数名称,参数类型和返回类型,也必须使用in关键字

 

posted on   suanningmeng98  阅读(174)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· DeepSeek “源神”启动!「GitHub 热点速览」
· 我与微信审核的“相爱相杀”看个人小程序副业
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库
· 上周热点回顾(2.17-2.23)
< 2025年2月 >
26 27 28 29 30 31 1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 1
2 3 4 5 6 7 8

点击右上角即可分享
微信分享提示