Swift笔记3:指针&内存管理

1,733 阅读9分钟

内存分区

我们可以通过插件libfooplugin.dylib 查看当前地址处于什么区 ,密码: vpha

栈区

栈区存放局部变量和函数运行时的上下文

func test(){
    var age: Int = 10
    print(age)
}
test()

查看SIL

通过Swift Intermediate Language (SIL)查找alloc_stack

注意是局部变量,如果使用let修饰的话

func test(){
    let age: Int = 10
    print(age)
}
test()

查看SIL

堆区

堆区是通过new & malloc分配空间,不连续,类似链表

class JClass {
    
}
func test(){
    var t = JClass()
    print(t)
}
test()

全局区
#include <stdio.h>

int age;
int a = 10;
int main(int argc, const char * argv[]) {
    
    printf(a);
    return 0;
}

多验证几个

#include <stdio.h>

int age;
int a = 10;
int age1;
int b = 20;
int main(int argc, const char * argv[]) {
    
    printf(a);
    return 0;
}

那么数据模型对全局区进一步划分

static

全局已经初始化静态变量未使用时

#include <stdio.h>


static int age = 10;

int main(int argc, const char * argv[]) {
    printf("hello");
    return 0;
}

那么我们使用一下

#include <stdio.h>

int a = 10;
int b;
int c = 20;
int d;
static int age = 10;
static int age2;

int main(int argc, const char * argv[]) {
    printf("%d\n",age);
    printf("%d\n",age2);
    printf("end");
    return 0;
}

通过MachOView查看

static修饰的全局变量只有使用的时候才会存储,这一点和普通的全局变量有区别

var

是不是应该在全局已初始化区__DATA,__data??

var a = 18
var b = a
print("end")

并不是__DATA,__data而是__DATA,__common!!!是在全局未初始化区!!!

let

这个是不是应该在全局已初始化区__DATA,__data??

var a = 18
var b = a
print("end")

查找不到符号

通过汇编查看

0x100003e50 + 0x41d8 = 0x100008028

也是__DATA,__common!!!是在全局未初始化区!!!

猜测:swift中好多地方都用到了延迟加载,这里应该也是因为编译的时候延迟加载的缘故,在编译的时候定义符号但并没有赋值,所以当作全局未初始化的变量处理,存储在__DATA,__common中,在运行时第一次使用到才会被赋值但不会改变地址

这里只是猜测,swift中的具体实现机制还没有搞明白,留下疑问

常量区

使用const关键字修饰

#include <stdio.h>

int a = 10;
const int age3 = 20;

int main(int argc, const char * argv[]) {
    printf("end");
    return 0;
}

如果同时使用static const修饰呢

#include <stdio.h>

int a = 10;
static const int age3 = 20;

int main(int argc, const char * argv[]) {
    printf("end");
    return 0;
}

通过汇编验证

代码段(⚠️这里只是我的个人理解⚠️)

通过MachOView来查看

__TEXT,__test

存储编译之后生成的汇编指令

__TEXT,__stubs

存储运行过程中需要动态绑定的一些函数,例如print函数只有运行的时候才知道我要打印谁,汇编指令

__TEXT,__stubs_helper

存储运行过程中需要动态绑定的一些汇编指令

__TEXT,__cstring

代码中用到的字符串符号

__TEXT,__unwind_info

不知道干啥的,但是看起来好像是记录内存分配的长度

Symbol Table

符号表,根据命名重整之后的符号来查找对应的符号,进而确定函数地址等信息,注意,如果在编译器就能确定的函数地址那么就不需要到这里来查询,releas包中的符号表也会少很多,只剩下不能在编译器确定的那些符号

Dynamic Symbol Table

动态符号表,存储运行时才能动态确定的一些符号

String Table

字符串表,存储代码中的字符串们

方法调度V-Table

首先记录几个ARM64架构的汇编指令

blr 带返回的跳转指令,跳转到指令后边跟随寄存器中保存的地址

mov 将某一寄存器的值复制到另一寄存器中(只能用于寄存器和寄存器,或者寄存器和常量之间传值,不能用于内存地址),如:mov: x1, x0表示将x0的值复制到x1寄存器中

ldr 将内存中的值读取到寄存器中,如 ldr x0, [x1, x2]表示将寄存器x1和x2的地址相加作为地址,取地址中的值赋值给x0

str 将寄存器中的值写入内存中,如 str x0, [x0 ,x1]表示将x0的值保存到x0+x1处

bl 跳转到某地址

读取内存起地址的lldb指令 register read x8

对于结构体中的方法都是静态调用(直接调用),而在swift中是通过V-Table进行调度的,V-TableSIL中是这么定义的

decl ::= sil-vtable
sil-vtable ::= 'sil_vtable' identifier '{' sil-vtable-entry* '}'

sil-vtable-entry ::= sil-decl-ref ':' sil-linkage? sil-function-name

V-Table类似数组结构,声明在class内部的方法在不加任何关键字修饰的情况下连续存放在V-Table所在地址空间中

class JClass {
    func a() {}
    func b() {}
    func c() {}
    func d() {}
}

class Sub: JClass {
    override func a() {}
    func e() {}
}

查看SIL

通过源码可以看到V-Table是通过for循环的形式存储在一块连续的内存空间中,具体的流程在后面类结构探索时详细研究

extension

在类扩展中声明的函数是直接调用的,这一点和OC有区别,并没有将扩展中的函数插入原有类的函数表中,OC的函数表是二维数组,可以知道类中的函数起始位置,但是我们在SIL文件中看到swift中的函数表V-Table是一维数组,并没有记录父类函数的位置

final

final修饰的函数也是静态调用的

@objc+NSObject

swift不能直接给OC使用

class JClass {
    func a() {}
    func b() {}
    func c() {}
    func d() {}
}

var t = JClass()
t.a()

只有一些宏定义,没有发现任何类相关的声明,这个时候在OC中也访问不到JClass

让类继承自NSObject

class JClass:NSObject {
    func a() {}
    func b() {}
    func c() {}
    func d() {}
}

var t = JClass()
t.a()

这时发现了暴露给OC的类声明

@objc标记的函数可以暴露给OC使用

class JClass:NSObject {
    @objc func a() {}
    func b() {}
    func c() {}
    func d() {}
}

var t = JClass()
t.a()

有了暴露给OC使用的方法,这时就可以在OC中访问a方法

通过SIL查看可以知道暴露给OC的函数默认还是调用了swift的函数,另外加上了retainrelease

要想在OC中使用swift中的方法需要同时满足类继承自NSObject@objc标记方法两个条件

dynamic

dynamic修饰的函数具有动态的特性,可以使用@_dynamicReplacement(for:)动态修改方法实现

class JClass {
    dynamic func a() {print("a")}
}

extension JClass{
    @_dynamicReplacement(for:a)
    func b() {print("b")}
}

var t = JClass()
t.a()

如果方法a不存在,那么编译器会报错

如果方法a存在但是没有标记dynamic编译器也会报错

dynamic+@objc+NSObject
class JClass {
    @objc dynamic func a() {}
    func b() {}
    func c() {}
    func d() {}
}

var t = JClass()
t.a()

既暴露给OC使用又有动态特性,那就直接成了动态消息转发objc-msgSend

这时也就可以进行Method Swizzling操作

class JClass:NSObject {
    @objc dynamic func a() {}
    func b() {}
    func c() {}
    func d() {}
}

var t = JClass()
t.a()

待验证

小结
  1. 类扩展extension中的方法是静态调用
  2. 任何给OC使用的swift类都必须继承自NSObject
  3. final标记的函数会变成直接调用,不能被继承
  4. dynamic标记的函数有了动态特性,没有改变调用方式
  5. @objc标记的函数暴露给OC使用,没有改变调用方式
  6. dynamic+@objc标记的函数既有动态性又暴露给OC调用方式变成动态消息转发,可以进行Method Swizzling操作
  7. dynamic+@objc+NSObjectOC使用的动态swift

指针的简单介绍

swift中的指针分为两类,指定类型指针typed pointer和原生指针raw pointer

typed pointerSwift中的表示是UnsafePoinster<T>

raw pointerSwift中的表示是UnsafeRawPointer

SwiftOC的对应关系

SwiftOC说明
UnsafePoinster< T >const T *指针不可变
UnsafeMutablePoinsterT *指针可变
UnsafeRawPointerconst void *指针指向未知类型
UnsafeMutableRawPointervoid *指针指向未知类型
raw pointer的使用

我们要开辟空间来存储4个Int类型的值,怎么实现呢

//开辟32字节空间,以8字节对齐
let p = UnsafeMutableRawPointer.allocate(byteCount: 32, alignment: 8)
//advanced表示指针前进的步长,这里我们取字节对齐的整数倍 MemoryLayout.stride
//storeBytes表示数据存储,需要知道类型
for i in 0..<4{
    p.advanced(by: i*8).storeBytes(of: i+1, as: Int.self)
}
//load用来读取数据,fromByteOffset相对首地址的指针偏移
for i in 0..<4{
    let a = p.load(fromByteOffset: i*8, as: Int.self)
    print(a)
}
//释放
p.deallocate()

打印结果

Type pointer的使用
var age = 10;
print(age)
age = withUnsafePointer(to: &age){prt in
    print(prt)
    return prt.pointee + 12
}
print(age)
withUnsafePointer(to: &age){prt in
        print(prt)
}

可以看到withUnsafePointer指针指向的内存的值可以修改

但是不能通过withUnsafePointer指针直接修改

var age = 10;
withUnsafePointer(to: &age){
    $0.pointee+=12
}

这时会报错

withUnsafeMutablePointer指向的值可以改变,也可以通过withUnsafeMutablePointer指针直接修改值

var age = 10
print(age)
age = withUnsafeMutablePointer(to: &age){
    print($0)
    return $0.pointee + 12
}
print(age)
withUnsafeMutablePointer(to: &age){
    print($0)
    $0.pointee += 12
}
print(age)

可以看到地址没有改变,值修改了

另一种开辟空间的方法

//另一种开辟空间的做法
var age = 10
let p = UnsafeMutablePointer<Int>.allocate(capacity: 4)

//通过advanced移动指针赋值和访问
for i in 0..<4{
    p.advanced(by: i).initialize(to: i+1)
}

for i in 0..<4{
    let value = p.advanced(by: i).pointee
    print(value)
}

//通过successor+predecessor赋值和访问
//successor表示后一个 predecessor表示前一个
var p1 = p;
for i in 0..<4{
    p1.initialize(to: i+1)
    p1 = p1.successor()
}

var p2 = p;
for i in 0..<4{
    let value = p2.pointee
    p2 = p2.successor()
    print(value)
}


//还可以这样赋值和访问
for i in 0..<4{
    (p+i).initialize(to: i+1)
}

for i in 0..<4{
    let value = (p+i).pointee
    print(value)
}


//也可以通过这种方式访问
for i in 0..<4{
    p[i] = i+1
}

for i in 0..<4{
    let value = p[i]
    print(value)
}

//注意使用完之后一定要释放
p.deinitialize(count: 4)
p.deallocate()

这几种方式都可以正常的赋值和访问

以上是值类型,如果是引用类型呢

class JClass {
    var age:Int = 18
    var name:String = "SC"
}

var p = UnsafeMutablePointer<JClass>.allocate(capacity: 2)
var j = JClass()
var j1 = JClass()
p.initialize(to: j)
p.advanced(by: 1).initialize(to: j1)


withUnsafePointer(to: &j){$0.pointee.age = 20}

withUnsafePointer$0.pointee中存储的是实例对象地址,只要地址不改变就可以,是可以通过地址来修改属性的,但是如果想要替换$0.pointee的值就报错了

class JClass {
    var age:Int = 18
    var name:String = "SC"
}

var p = UnsafeMutablePointer<JClass>.allocate(capacity: 2)
var j = JClass()
var j1 = JClass()
p.initialize(to: j)
p.advanced(by: 1).initialize(to: j1)

withUnsafePointer(to: &j){$0.pointee = j1}

这时使用withUnsafeMutablePointer可以实现

class JClass {
    var age:Int = 18
    var name:String = "SC"
}

var p = UnsafeMutablePointer<JClass>.allocate(capacity: 2)
var j = JClass()
var j1 = JClass()
p.initialize(to: j)
p.advanced(by: 1).initialize(to: j1)
withUnsafeMutablePointer(to: &j){$0.pointee = j1}

raw pointertype pointer使用过程中注意步长advanced的区别,原生指针中步长以字节为单位,type pointer是以具体类型占用内存长度为单位

Unmanaged(非托管)

下面代码打印结果是什么??应该是打印实例变量t的地址

class JClass {
    var age:Int = 18
    var name:String = "SC"
}
var t = JClass()
var p = withUnsafePointer(to: &t){ $0 }
print(p.pointee)

不符合预期,直接打印了类名称

那么这个时候我们就需要借助Unmanaged来获取类的实例对象指针,Unmanaged类似于__bridge,所有权的转换

class JClass {
    var age:Int = 18
    var name:String = "SC"
}

var t = JClass()
var p = withUnsafePointer(to: &t){ $0 }

var p1 = Unmanaged.passUnretained(t as AnyObject).toOpaque()
print(p1)
print("end")

bindMemory内存绑定
struct HeapObject {
    var kind:UnsafePointer<Int>
    var strongref: UInt32
    var unownedref: UInt32
}

class JClass {
    var age:Int = 18
    var name:String = "SC"
}

var t = JClass()
let p1 = Unmanaged.passUnretained(t as AnyObject).toOpaque()
let p2 = p1.bindMemory(to: HeapObject.self, capacity: 1)
print(p2.pointee)

这时原本指向JClass实例对象的地址就指向了HeapObject的类型对象,之所以可以这样绑定是因为内存结构是一样的,class的第一个字段是metadata8字节,第二个字段是countRef8字节,我们拆分成了两个4字节

那如果内存结构对应不上会怎么样呢

struct HeapObject {
    var kind:String
    var strongref: UInt32
    var unownedref: UInt32
}

class JClass {
    var age:Int = 18
    var name:String = "SC"
}

var t = JClass()
let p1 = Unmanaged.passUnretained(t as AnyObject).toOpaque()
let p2 = p1.bindMemory(to: HeapObject.self, capacity: 1)
print(p2.pointee)

这时就报错了

assumingMemoryBound假定内存绑定
var a = (4,8)
func f(c:UnsafePointer<Int>){
    
}
withUnsafePointer(to: &a) { (ptr:UnsafePointer<(Int,Int)>) in
    f(c: ptr)
}

函数f接收的是一个UnsafePointer<Int>类型的指针,但是传过去的是一个UnsafePointer<(Int,Int)>,类型不匹配报错

但是我们知道元组类型的指针其实就是指向第一个元素的指针,也就是UnsafePointer<Int>类型的,那么我们可以使用假定内存绑定指针assumingMemoryBound(to: )来告诉编译器我就是UnsafePointer<Int>类型的,你不需要来检查我了

var a = (4,8)
func f(c:UnsafePointer<Int>){
    
}
withUnsafePointer(to: &a) { (ptr:UnsafePointer<(Int,Int)>) in
    f(c: UnsafeRawPointer(ptr).assumingMemoryBound(to: Int.self))
}

这个时候是可以编译通过的!!!

从前面的表格我们看到UnsafePointer<T>相当于OC中的const T *,指针指向的位置不可变

var b = 1
struct S {
    var f:Int
    var f1:Int
    var f2:String
}

let p = withUnsafePointer(to: &b) { (ptr:UnsafePointer<Int>) in
    print(ptr)
    UnsafeRawPointer(ptr).bindMemory(to: S.self, capacity: 1)
    print(ptr)
}

print("end")

我们看到执行bindMemory前后并没有改变指针的指向,只是认为指向内存的类型发生了变化

所以一定要在能确定内存布局一致的情况下使用bindMemoryassumingMemoryBound,否则就可能报错