swift进阶六:方法调度 & @objc & 指针

swift进阶 学习大纲

  1. 方法调度
  2. 指针

1. 方法调度

以下方法演示,仅在Debug环境下演示。
(因为release环境下,swiftvtable移除了,直接越过这一层级,调用指定函数。但Debug中能看清楚完整的流程

1. struct 和 class的调度差异

struct 直接调用,class先找到vtable,再调度:

  • SIL中可以看到,只有classvtable

    image.png

  • 汇编中可以看到,struct 是直接调用地址,而class函数调用,先找到vtable,再调用函数:

    image.png

  • swift源码中,可以看到vtable存储时,是数组结构顺序存储。所以验证了读取时可内存偏移读取

    image.png

ARM64常用指令

  • bl跳转到某地址
  • blr: 带返回跳转指令,到指令后寄存器中的地址
  • mov: 将寄存器的值赋值到另一寄存器(mov x1, x0: 将寄存器x0的值复制到寄存器x1中)
  • ldr: 将内存中的值读取到寄存器中(ldr x0, [x1, x2]: 读取x1+x2地址,返回值给x0)
  • str: 将寄存器中的值写入内存中(str x0,[x1, x2]: 寄存器x0的值,存放在x1+x2的地址处)

1.2 class 的 extension方法调度

  • classextension方法的调度,是直接调度。不会记入vtable表中:
image.png
  • vtable会如何记录父类函数呢?重写父类方法会怎样?
image.png
  1. 继承父类class中的方法,会被写入vtable中:
    没有重写,直接记录父类的函数。
    如果重写,会记录自己的函数
  2. 父类extension中的方法,不会写入vtable中,也不可重写。但被子类调用
  3. 被写入vtable的方法,都是通过地址直接调用

1.3 final修饰

  • final 修饰的函数属性,都写入vtable中,子类不可重写,但可调用
    image.png

1.4 @objc修饰

  • 声明函数被OC使用,编译后都会生成2个函数原函数@objc函数)。
    image.png

可以发现,@objc修饰函数本质上是调用了没被@objc修饰的原函数

  • 如果仅仅是支持#selector()的调用,直接使用@objc即可。
  • 如果需要OC文件中调用这个@objc函数,需要让类继承NSObject。只有继承自NSObject的类,才能被OC访问到。

OC-Swift 桥接演示

  • 创建一个OC项目,新建一个SwiftTest.swift文件:

    image.png

  • 可以看到桥接文件(swift在OC中的头文件):

    image.png

进入桥接文件,可以看到被@objc声明的属性函数都生成了OC格式
(没@objc声明的,生成)

image.png

  • OC调用swift文件,直接导入文件OCDemo-Swift.h,就可以调用了:
    image.png

1.5 dynamic

  • 函数变为动态性
  1. dynamic声明的函数,依旧是vtable调用:
image.png
  1. dynamic声明的函数,支持动态替换
image.png
  • _dynamicReplacement 可替换dynmic声明的函数
  • _dynamicReplacement 只能在extention使用
  1. swift函数具备OC动态性(函数调用使用objc_msgSend)
  • @objcdynamic同时修饰函数,就可以让swift函数调用变成objc_msgSend方式:(如果再继承NSObject,就可以被OC文件使用)
    image.png
image.png
  • 以上,就是swift函数调度,以及所有修饰符作用

总结

  1. struct:函数直接调用
    class:写在class中函数会被记录在vtable间接调用,但写在extension中的会被直接调用
  2. class父类,写在extension中的函数不可被子类重写,但可调用。调用方式是直接调用
  3. final:修饰的函数属性,不被写入vtable中,不可被子类重写,但可调用
  4. @objc:修饰的函数属性,本质会生成原函数+@objc函数,其中@objc函数可被OC类使用,如果class类需要被OC使用,则需要继承NSObject
  5. dynamic:修饰的函数可具备动态性,可在extension中进行@_dynamicReplacement (for: XXX)动态交换。dynamic配合@objc一起使用,是直接调用OC消息机制(objc_msgSend)进行函数调用

2. 指针

swift的指针分为两类,typed pointer(指定类型指针)和raw pointer(未知的类型的指针 - 原生指针)

  • raw pointer :在swift中表示的是UnsafeRawPointer
  • typed pointer,:在swift中表示的是UnsafePointer<T>(T是泛型)

swift指针OC指针对应关系:

image.png

  • swift指针操作,都是unsafe不安全的,操作不当可能crash。需要程序员自己判断

2.1 RawPointer的使用

  • 4个Int整形数据的存储读取为例:
/**
 RawPointer 未指定类型(原生指针)的使用
 */

// 1. 可变原生指针: 指定开辟32字节空间,遵循8字节对齐规则   (swift源码中可看到调用Builtin标准模块的allocRaw)
let p = UnsafeMutableRawPointer.allocate(byteCount: 32, alignment: 8)

// 2. 不同空间存放不同内容
// advanced: p前进的步长。(从p指针地址开始,需要偏移多少字节。 这个内存大小就是MemoryLayout.stride)
// storyBytes: 存储内容(of: 指定存储内容(T), as: 内容的类型(T.Type))
for i in 0..<4 {
    p.advanced(by: i * 8).storeBytes(of: i+1, as: Int.self)
}

// 3. 读取内存内容
// fromByteOffset: 从p指针地址开始,需要偏移多少字节
// as: 内容的类型(T.Type)
for i in 0..<4 {
    let value = p.load(fromByteOffset: i * 8, as: UInt64.self)
    print(value)
}

// 4. 释放指针。 (allocate和deallocate是对应关系。创建了就需要手动释放)
p.deallocate()

【注意】
RawPointer未指定类型指针, 可以看到我在storeBytes中,as传的是Int.self。而load读取时,使用的UInt64.self

  • 这是因为storeBytes时,并指定类型,而仅仅是以该类型大小为存储大小,进行空间划分
  • 所以读取时,我们只要按照相同大小类型读取数据,就可以完美的完成类型转换。(64位系统下,IntUInt64都是16字节大小)

在带来操作便捷性的同时,会带来crash风险(存储读取内存单位不一致)。所以对指针的操作Unsafe不安全的。

dvanced的补充

  • RawPointer(未指定类型)的指针dvanced接收完整bytes偏移值,
  • typed pointerr(已指定类型 )的指针dvanced接收单位偏移数

案例演示:

/**
RawPointer 未指定类型的指针
*/
// 1. 申请32字节空间,8字节对齐
let p1 = UnsafeMutableRawPointer.allocate(byteCount: 32, alignment: 8)
for i in 0..<4 {
   // 2. 偏移i*8插入i+1的值,占据Int类型大小
   p1.advanced(by: i * 8).storeBytes(of: i+1, as: Int.self)
   //3.  通过偏移拿到起始地址,读取Int类型数据
   print(p1.load(fromByteOffset: i * 8, as: Int.self))
}
// 4. 释放
p1.deallocate()

/**
Typed Pointer 已指定类型的指针
*/
// 1. 申请4个Int空间(共32字节)
let p2 = UnsafeMutablePointer<Int>.allocate(capacity: 4)
for i in 0..<4 {
   // 2. 每个(Int)空间initialize初始化,并赋值i+1
   p2.advanced(by: i).initialize(to: i+1)
   // 3. 直接下标打印(因为固定了类型,元素大小一样)
   print(p2[i])
//    print((p2 + i).pointee) // 也可通过单位地址偏移,读取pointee内容进行打印
}
// 4. deinitialize释放指定元素个数的空间
p2.deinitialize(count: 2)
// 5. 释放指针
p2.deallocate()

2.2 Type Pointer的使用

  • 指针地址内容读取映射

  • 【方法一】 通过Swift提供的API进行操作:
    withUnsafePointer: 不可更改原值
    withUnsafeMutablePointer: 可更改原值

var age = 10

print("------1-------")

// 1.通过Swift提供的API,读取指针的地址
let p = withUnsafePointer(to: &age) { $0 }
// pointee存放了指针的所有属性
print(p.pointee)      // 打印结果:10

print("------2-------")

// 2. withUnsafePointer 返回值是UnsafePointer不可修改的值。
//   我们不可以修改$0.pointee的值,但可以在闭包中对指针内容包装,可转换为任意类型的内容进行输出。
var b = withUnsafePointer(to: &age) { "age:\($0.pointee) 被我改成String了" }
print(b)             // 打印结果: age:10 被我改成String了
print(type(of: b))   // 打印结果: String

print("------3-------")

// 3. 如果想要在闭包内修改原值,就必须使用
print("修改前age:\(age)")   // 打印结果:修改前age:10
withUnsafeMutablePointer(to: &age) {  $0.pointee += 10 }
print("修改后age:\(age)")   // 打印结果:修改前age:20
  • 【方法二】 通过指针内存,进行操作:
var age = 10

// 1.使用UnsafeMutablePointer可变的指定Int类型为指针进行操作,读取一个单位内存大小
// capacity: 容量个数,表示1个Int类型大小,为8字节。
let p = UnsafeMutablePointer<Int>.allocate(capacity: 1)

print("初始化前的p:\(p.pointee)")   //打印内容: 初始化前的p:0

// 2. 通过【值拷贝】复制age指针内容,初始化当前的指针p
p.initialize(to: age)
print("初始化后的p:\(p.pointee)")   //打印内容:初始化后的p:10

p.pointee = 666
print("p值修改后的p:\(p.pointee)")  // 打印内容:p值修改后的p:666
print("p值修改后的age:\(age) " )    // 打印内容:p值修改后的age:10 【没有改变,说明上面是值拷贝】

// 3. 释放指针空间
p.deinitialize(count: 1)
// 4. 释放指针
p.deallocate()

所有对指针操作,都需要手动管理内存

  1. 开辟的内存都需要手动释放
  2. 用完的指针都要释放

实战案例:

  • 重写swift对象结构类结构读取系统对象指针空间强转为我们的对象,进行内容分析。
// 自定义对象默认结构(后面的自定义属性就不取了)
struct HeapObject {
   var metadata:               UnsafeRawPointer
   var strongRef:              UInt32
   var unownedRef:             UInt32
}

// 自定义swift类结构
struct Swift_class {
   var kind:                   UnsafeRawPointer
   var superClass:             UnsafeRawPointer
   var cacheData1:             UnsafeRawPointer    // 因为系统数据结构是CacheData[2],所以用2个来接
   var cacheData2:             UnsafeRawPointer
   var data:                   UnsafeRawPointer
   var flags:                  UInt32
   var instanceAddressOffset:  UInt32
   var instanceSize:           UInt32
   var flinstanceAlignMask:    UInt16
   var reserved:               UInt16
   var classSize:              UInt32
   var classAddressOffset:     UInt32
   var description:            UnsafeRawPointer
}

class HTTeacher {
   var age = 18
}

// 实例变量
var t = HTTeacher()

// 1. 将t变量转换为任意类型的指针(不对引用计数进行操作)
//  Unmanaged: 不托管的.  有passUnretained(引用计数不加1) 和 passRetained(应用计数+1) 两种。
//  toOpaque: 将类型对象转换为指针(不安全的)
let p = Unmanaged.passUnretained(t as AnyObject).toOpaque()

// 2. 将p指针绑定为HeapObject类型的指针
// bindMemory: 绑定内存为HeapObject类型,空间大小为1个HeapObject的stride步>长大小
let heapObject = p.bindMemory(to: HeapObject.self, capacity: 1)

// 3. 将heapObject内部的metadata指针绑定为Swift_class类型指针
let metadata = heapObject.pointee.metadata.bindMemory(to: Swift_class.self, capacity: 1)

// 【注意】只有UnsafeRawPointer未指定类型的指针,才可以使用bindMemory,转为为绑定指定类型的指针。
//  p指针通过toOpaque转为RawPointer,而heapObject的metadata指针,在struct结构中就定义为RawPointer。所以都可使用bindMemory
//  聪明的你,应该感受到,如果我直接在HeapObject结构中,就将metadata指定为UnsafePointer<Swift_class>,就完全不需要第3步动态绑定了。👍

print(metadata.pointee)

/**
打印结果:
Swift_class(kind: 0x00000001000081d0,       // 类型
           superClass: 0x00007fff91f91060,
           cacheData1: 0x00007fff6a3cb140,
           cacheData2: 0x0000002000000000,
           data: 0x0000000104049732,
           flags: 2,
           instanceAddressOffset: 0,
           instanceSize: 24,      // 实例大小  基础的16字节 + age 8字节  = 24
           flinstanceAlignMask: 7,
           reserved: 0,
           classSize: 136,
           classAddressOffset: 16,
           description: 0x0000000100003bec)
*/

总结

  1. 使用UnmanagedtoOpaque方法,将实例变量指针转换为UnsafeRawPointer未指定类型的指针。
  2. 调用bindMemory函数,将UnsafeRawPointer指针绑定为UnsafePointer< HeapObject >绑定类型为HeapObject的指针
  3. 同样调用bindMemory函数,将heapObjectmetadataUnsafeRawPointer指针绑定为UnsafePointer<Swift_class>指针。
  4. 此时可打印metadata.pointee查看内部结构。
  • 此案例主要目的,是演示类型强转,类似OC的__bridge业务中如果类型不一致,但我们完全确定可以是某种类型时,进行类型转换

还是得提醒一下,任何对指针的操作,都是不安全的,程序员全责😂

2.3 元组指针类型的转换

var tuple = (10, 20)

func testPointer(_ p: UnsafePointer<Int>) {
    print("地址:\(p) 内容:\(p.pointee)")   
    print("end")
}
 
withUnsafePointer(to: &tuple) { (t: UnsafePointer<(Int, Int)>) in
    let a = UnsafeRawPointer(t).bindMemory(to: Int.self, capacity: 1)
    testPointer(a) // 打印内容: 地址:0x00000001000081d8 内容:10

    let b = UnsafeRawPointer(t).assumingMemoryBound(to: Int.self)
    testPointer(b) // 打印内容: 地址:0x00000001000081d8 内容:10
    
    //(testPoiinter函数中,并不知道入参是元组,只是打印首地址内容:第一个元素10)
    
    print("--- 元组的打印 ---")
    print(t.pointee)   // 打印内容:(10, 20)
    print(t.pointee.0) // 打印内容:10
    print(t.pointee.1) // 打印内容:10
 
}

bindMemoryassumingMemoryBoundwithMemoryRebound的区别

  • bindMemory更改内存绑定的类型
    (之前没绑定,就首次绑定,如果绑定了重新绑定新类型
  • assumingMemoryBound假定内存绑定
    (告诉编译器不用检查,它就是我说的类型)
  • withMemoryRebound临时更改内存绑定类型
  • withMemoryRebound的使用
    image.png

2.4 如何拿到结构体属性指针

由于结构体属性直接存储值内容。所以读取时:

  • 非引用类型:直接读到值内容
  • 引用类型:读取到对象指针地址

结构体属性既然存在内存中,无论是否是引用类型,一定有指针地址。我们可以通过结构体对象进行指针偏移,拿到属性地址

class HTPerson {
    var age = 1
}

struct HeapObject {
    var perosn = HTPerson()
    var strongRef = 10
    var unownedRef = 20
}

var t = HeapObject()

func testPointer(_ p: UnsafePointer<Int>) {
    print("地址:\(p) 内容:\(p.pointee)")
}

// Q: 问题: 如何拿到HeapObject实例的stongRef属性指针

//指针偏移(从headpObject开始偏移)
withUnsafePointer(to: &t) {

    // offset: 指定属性名称
    let personRef = UnsafeRawPointer($0) + MemoryLayout<HeapObject>.offset(of: \HeapObject.perosn)!
    testPointer(personRef.assumingMemoryBound(to: Int.self))  // 打印结果: 地址:0x0000000100008380 内容:4354357216
    /**
     lldb打印: x/4gx 4354357216
     0x1038a37e0: 0x0000000100008248             0x0000000000000002
     0x1038a37f0: 0x0000000000000001(这个就是age) 0x0002000000000000
     */
    
    let strongRef = UnsafeRawPointer($0) + MemoryLayout<HeapObject>.offset(of: \HeapObject.strongRef)!
    testPointer(strongRef.assumingMemoryBound(to: Int.self))  // 打印结果: 地址:0x00000001000081f8 内容:10

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

推荐阅读更多精彩内容