swift高级1. 类与结构体(上)

497 阅读23分钟

类与结构体

本文主要介绍为什么 结构体是值类型, 类是引用类型 , 对应的方法调度, 内存插件libfooplugin类生命周期 以及 Swift源码分析

初识类与结构体

类与结构体相同点与不同点

Swift中类用class修饰,结构体用struct修饰

struct/class Teacher {
    var age: Int 
    var name: String
    
    init(age: Int, name: String) { 
        self.age = age
        self.name = name 
    }
    
    deinit{} 
}

结构体和类的主要相同点有:

  1. 定义存储值的属性
  2. 定义方法
  3. 定义下标以使用下标语法提供对其值的访问
  4. 定义初始化器
  5. 使用 extension 来拓展功能
  6. 遵循协议来提供某种功能

主要的不同点有:

  1. 类有继承的特性,而结构体没有
  2. 类型转换使您能够在运行时检查和解释类实例的类型
  3. 类有析构函数用来释放其分配的资源
  4. 引用计数允许对一个类实例有多个引用

类是引用类型

对于类与结构体我们需要区分的第一件事就是:

类是引用类型。也就意味着一个类类型的变量并不直接存储具体的实例对象,是对当前存储具体实例内存地址的引用。

class Teacher {
    var age = 18
    var name = "aa"
    
    init(age: Int, name: String) {
        self.age = age
        self.name = name
    }
}
var t = Teacher(age: 18, name: "tony")
var t1 = t
print("end") // 断点

(lldb) po t
<Teacher: 0x10050ee10>

(lldb) po t1
<Teacher: 0x10050ee10>

可以看到 t 和 t1 的地址是相同的,也就说明了类是引用类型。

1012

结构体是值类型

swift 中有引用类型,就有值类型,最典型的就是 Struct ,结构体的定义也非常简单,相比较 类类型的变量中存储的是地址,那么值类型存储的就是具体的实例(或者说具体的值)。

struct Student {
    var age: Int
    var name: String
}
var s = Student(age: 19, name: "tony")
var s1 = s
print("end") // 断点

(lldb) po s
 Student
  - age : 19
  - name : "tony"

(lldb) po s1
 Student
  - age : 19
  - name : "tony"
  
(lldb) po withUnsafePointer(to: &s) { print($0) }
0x0000000100008158
0 elements

(lldb) po withUnsafePointer(to: &s1) { print($0) }
0x0000000100008170
0 elements
(lldb) cat address 0x0000000100008158
address:0x0000000100008158, 0SwiftTest.s : SwiftTest.Student <+0> , ($s9SwiftTest1sAA7StudentVvp), External: NO SwiftTest.__DATA.__common +0
(lldb) cat address 0x0000000100008170
address:0x0000000100008170, 18SwiftTest.s1 : SwiftTest.Student <+0> , ($s9SwiftTest2s1AA7StudentVvp), External: NO SwiftTest.__DATA.__common +18

可以看到,结构体是直接存储值。

1013

  • 其实引用类型就相当于在线的 Excel ,当我们把这个链接共享给别人的时候,别人的修改我们 是能够看到的;
  • 值类型就相当于本地的 Excel ,当我们把本地的 Excel 传递给别人的时候,就 相当于重新复制了一份给别人,至于他们对于内容的修改我们是无法感知的。

存储区域的不同

另外引用类型和值类型还有一个最直观的区别就是存储的位置不同:一般情况,值类型存储的在 栈上,引用类型存储在堆上。

结构体的内存分配

来看例子

struct Teacher {
    var age = 18
    var name = "tony"
}

func test() {
    var t = Teacher()
    print("end")    // 断点
}
test()

(lldb) frame variable -L t
0x000000016fdff318: (SwiftTest.Teacher) t = {
0x000000016fdff318:   age = 18
0x000000016fdff320:   name = "tony"
}
(lldb) cat address 0x000000016fdff318
address:0x000000016fdff318, stack address (SP: 0x16fdff2d0 FP: 0x16fdff330) SwiftTest.test() -> ()
  • t是存储在栈上的,age和name也是存储在栈上的
  • t的首地址直接是存储的age变量,age和name也是连续的
  • 当执行var t = Teacher()这段代码时,会在栈上开辟所需的内存空间,当test()函数执行完时,回收所开辟的内存空间。

当前结构体在内存当中的分布示意图:

1014

类的内存分配

如果我们把其他条件不变,将strcut修改成class的情况我们来看一下:

class Teacher {
    var age = 18
    var name = "tony"
}

func test() {
    var t = Teacher()
    print("end")    // 断点
}
test()

(lldb) frame variable -L t
scalar: (SwiftTest.Teacher) t = 0x0000000100708020 {
0x0000000100708030:   age = 18
0x0000000100708038:   name = "tony"
}
(lldb) cat address 0x0000000100708020
address:0x0000000100708020, (String) $R0 = "0x100708020 heap pointer, (0x30 bytes), zone: 0x1f4a1c000"

(lldb) x/8g 0x100708020
0x100708020: 0x0000000100008288 0x0000000200000003
0x100708030: 0x0000000000000012 0x00000000796e6f74
0x100708040: 0xe400000000000000 0x00000001a1398348
0x100708050: 0x20000000100729f6 0x900000001007296a
  • 在栈上会开辟8字节内存空间,来存储Teacher的地址
  • 在堆上寻找合适大小的内存块分配,然后将value拷贝到堆内存存储,并在栈上存储一个指向这块堆内存的指针, 其中t占了48个字节,age占了8个字节,age占16个字节
  • 函数结束时,离开作用域,堆内存销毁(查找并把内存块重新插入到栈内存中),栈内存销毁(移动栈针)
  • 当然栈内存在分配的过程中还要保证线程安全(这也是一笔很大的开销)

1015

结构体和类的时间分配

这里我们也可以通过github上 StructVsClassPerformanc 这个案例来直观的测试当前结构体和类的时间分配。

Running tests

class (1 field)
2.280184208328137

struct (1 field)
1.5351142500003334

class (10 fields)
2.1326438749965746

struct (10 fields)
1.6441537916689413

经过测试可以发现,结构体速度快于类, 但demo中也不是特别快。

优化案例

Understanding Swift Performance参考资料

下面看一个在实际应用中的优化方案

enum Color { case blue, green, gray }
enum Orientation { case left, right }
enum Tail { case none, tail, bubble }

var cache = [String : UIImage]()

func makeBalloon(_ color: Color, orientation: Orientation, tail: Tail) -> UIImage {
let key = "\(color):\(orientation):\(tail)"
   if let image = cache[key] {
      return image
   }
   ...
}

这段代码的应用场景是我们常见的iMessage聊天界面气泡生产能function,三个enum表示气泡的不同情况,比方蓝色朝左带尾巴,因为用户可能经常滑动聊天列表来查看消息makeBalloon这个方法的调用时非常频繁的,为了避免多次生成UIImage,我们用Dictionary来充当一个cache,用不同情况下的enum序列化一个String来充当cache的key。

思考一下这样写有什么问题么?

问题就出在key上面,用String充当key,首先在类型安全上面无法保证存放的一定是一个气泡的类型,因为是String所以你可以存放阿猫阿狗等等。其次String的character是存储在堆上面的,所以每次调用makeBalloon虽然命中cache,但仍然不停的设计堆内存分配和销毁

怎么解决?

struct Attributes : Hashable {
   var color: Color
   var orientation: Orientation
   var tail: Tail
}

let key = Attributes(color: color, orientation: orientation, tail: tail)

看到优化代码你应该恍然大悟,原来Swift已经为我们准备好了更好地类型struct并且他可以作为Dictionary的key,这样就不涉及任何堆内存分配销毁,并且Attributes可以确保类型安全

下面看一个在实际应用中的优化方案

struct Attachment {
    let fileURL: URL
    let uuid: String
    let mineType: String
   
    init?(fileURL: URL, uuid: String, mimeType: String) {
        guard mineType.isMineType else { return nil }
        
        self.fileURL = fileURL
        self.uuid = uuid
        self.mineType = mimeType
    }
}

这段代码是我们给message新建的一个model,表示我们聊天时发送的文件,它含有一个URL类型的fileURL表示文件的位置,一个String类型的uuid方便我们在不同设备定位这个文件,因为不可能支持所有的文件格式,所以有一个String类型的mineType用于过滤不支持的文件格式

想一下这么创建model会有什么性能上的问题或者有什么优化的方案么?

来看一下Attachment的存储情况

1016

因为URL为引用类型,String的character存储在堆上,所以Attachment在引用计数的层面消耗有点大,感觉可以优化,那怎么优化?

首先uuid声明为String类型真的好么?答案是不好,一是无法在类型上限制uuid的安全性,二是涉及到引用计数,那怎么优化呢?Fundation中加入了UUID这个类型,是一个值类型不涉及引用计数,也能在类型安全上让我们满意,

public struct UUID : ReferenceConvertible, Hashable, Equatable, CustomStringConvertible {}

再来看一下mineType

extension String {
   var isMimeType: Bool {
      switch self {
      case "image/jpeg":
         return true
      case "image/png":
         return true
      case "image/gif":
         return true
      default:
         return false
      }
   }
}

我们是通过给String扩展的方式来过滤我们支持的文件格式,看到这里你肯定想到了Swift为我们提供的更好的表示多种情况的抽象类型enum吧,它同为值类型,不涉及引用计数

优化后的模型:

struct Attachment {
    let fileURL: URL
    let uuid: UUID
    let mineType: MimeType

    init?(fileURL: URL, uuid: UUID, mimeType: String) {
        guard let mimeType =  MimeType(rawValue: mimeType) else { return nil }
        
        self.fileURL = fileURL
        self.uuid = uuid
        self.mineType = mimeType
    }
}

enum MimeType : String {
   case jpeg = "image/jpeg"
   case png = "image/png"
   case gif = "image/gif"
}

1017

类的初始化器

Swift 中创建类和结构体的实例时必须为所有的存储属性设置一个合适的初始值。

结构体初始化器

struct Teacher {
    var age: Int
    var name: String
}

// 配置Run Script
swiftc -emit-sil ${SRCROOT}/TT/main.swift | xcrun swift-demangle > ./main.sil && open main.sil

// SIL文件
struct Teacher {
  @_hasStorage var age: Int { get set }
  @_hasStorage var name: String { get set }
  init(age: Int, name: String)
}

类编译器默认不会自动提供成员初始化器,但是对于结构体来说编译器会提供默认的初始化方法(前提是我们自己没有指定初始化器)!

类初始化器

类必须要提供对应的指定初始化器,同时我们也可以为当前的类提供便捷初始化器

class Person {
    var age: Int
    var name: String
}

报错:
Class 'Person' has no initializers


class Person {
    var age: Int
    var name: String
    
    init(_ age: Int, _ name: String) {
        self.age = age
        self.name = name
    }
    
    convenience init(_ age: Int) {
        self.init(18, "tony")
        self.age = age
    }
}

SIL文件
class Person {
  @_hasStorage var age: Int { get set }
  @_hasStorage var name: String { get set }
  init(_ age: Int, _ name: String)
  convenience init(_ age: Int)
  @objc deinit
}

注意:便捷初始化器必须从相同的类里调用另一个初始化器。

当我们派生出一个子类SubTeacher,看下它的指定初始化器的写法

class Person {
    var age: Int
    var name: String
    
    init(_ age: Int, _ name: String) {
        self.age = age
        self.name = name
    }
    
    convenience init(_ age: Int) {
        self.init(18, "tony")
        self.age = age
    }
}

class Teacher: Person {
    var subjectName: String
    
    init(_ subjectName: String) {
        self.subjectName = subjectName
        super.init(18, "mark")
        self.age = 17
    }
}

SIL文件
class Person {
  @_hasStorage var age: Int { get set }
  @_hasStorage var name: String { get set }
  init(_ age: Int, _ name: String)
  convenience init(_ age: Int)
  @objc deinit
}

class Teacher : Person {
  @_hasStorage var subjectName: String { get set }
  init(_ subjectName: String)
  override init(_ age: Int, _ name: String)
  @objc deinit
}
  • 指定初始化器必须保证在向上委托给父类初始化器之前,其所在类引入的所有属性都要初始化完成。

  • 指定初始化器必须先向上委托父类初始化器,然后才能为继承的属性设置新值。如果不这样做,指定初始化器赋予的新值将被父类中的初始化器所覆盖。

  • 便捷初始化器必须先委托同类中的其它初始化器,然后再为任意属性赋新值(包括同类里定义的属性)。如果没这么做,便捷构初始化器赋予的新值将被自己类中其它指定初始化器所覆盖。

  • 初始化器在第一阶段初始化完成之前,不能调用任何实例方法、不能读取任何实例属性的值,也不能引用 self 作为值。

可失败初始化器

意思就是当前因为参数的不合法或者外部条件的不满足,存在初始化失败的情况。

这种 Swift 中可失败初始化器写 return nil 语句, 来表明可失败初始化器在何种情况下会触发初始化失败。写法也非常简单:

class Person {
    var age: Int
    var name: String
    
    init?(_ age: Int, _ name: String) {
        // 年龄小于 18 认为不是一个合法的成年人,创建失败
        guard age < 18 else { return nil }
        self.age = age
        self.name = name
    }
    
    convenience init?(_ age: Int) {
        self.init(18, "tony")
        self.age = age
    }
}

必要初始化器

在类的初始化器前添加required修饰符来表明所有该类的子类都必须实现该初始化器

1018

如果子类没有实现该必要初始化器,就会报错。

类的生命周期

1.Swift编译

iOS开发的语言不管是OC还是Swift后端都是通过LLVM进行编译的,如下图所示:

1019

  • OC通过clang编译器,编译成IR,然后再生成可执行文件.o
  • Swift则是通过Swift编译器编译成IR,然后在生成可执行文件。

详细看下Swift的编译过程:

// 分析输出AST
swiftc main.swift -dump-parse

// 分析并且检查类型输出AST 
swiftc main.swift -dump-ast

// 生成中间体语言(SIL),未优化 
swiftc main.swift -emit-silgen

// 生成中间体语言(SIL),优化后的 
swiftc main.swift -emit-sil

// 生成LLVM中间体语言 (.ll文件) 
swiftc main.swift -emit-ir

// 生成LLVM中间体语言 (.bc文件) 
swiftc main.swift -emit-bc

// 生成汇编
swiftc main.swift -emit-assembly

// 编译生成可执行.out文件 
swiftc -o main.o main.swift

1020

2.sil文件分析

在main.swift中写入下面的代码:

class Person {
    var age: Int = 18
    var name: String = "tony"
}
var p = Person()

运行脚本,将main.swift编译成main.sil文件

class Person {
  @_hasStorage @_hasInitialValue var age: Int { get set }
  @_hasStorage @_hasInitialValue var name: String { get set }
  @objc deinit
  init()
}


// main
sil @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 {
bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>):
  alloc_global @main.s : main.Person             // id: %2
  %3 = global_addr @main.s : main.Person : $*Person // user: %7
  %4 = metatype $@thick Person.Type               // user: %6
  // function_ref Person.__allocating_init()
  %5 = function_ref @main.Person.__allocating_init() -> main.Person : $@convention(method) (@thick Person.Type) -> @owned Person // user: %6
  %6 = apply %5(%4) : $@convention(method) (@thick Person.Type) -> @owned Person // user: %7
  store %6 to %3 : $*Person                       // id: %7
  %8 = integer_literal $Builtin.Int32, 0          // user: %9
  %9 = struct $Int32 (%8 : $Builtin.Int32)        // user: %10
  return %9 : $Int32                              // id: %10
} // end sil function 'main'


// 去堆区申请内存, 调用关键函数__allocating_init()
// Person.__allocating_init()  
sil hidden [exact_self_class] @main.Person.__allocating_init() -> main.Person : $@convention(method) (@thick Person.Type) -> @owned Person {
// %0 "$metatype"
bb0(%0 : $@thick Person.Type):
  ; 去堆区申请内存空间
  %1 = alloc_ref $Person                          // user: %3
  // function_ref Person.init()
  %2 = function_ref @main.Person.init() -> main.Person : $@convention(method) (@owned Person) -> @owned Person // user: %3
  %3 = apply %2(%1) : $@convention(method) (@owned Person) -> @owned Person // user: %4
  return %3 : $Person                             // id: %4
} // end sil function 'main.Person.__allocating_init() -> main.Person'
  • @main:入口函数
  • %0:寄存器,虚拟的
  • sil语法

3.断点汇编分析

class Person {
    var age: Int = 18
    var name: String = "tony"
}
var p = Person() // 断点

1021

按住Control + 点击Step info

SwiftTest`Person.__allocating_init():
->  0x100003a48 <+0>:  stp    x20, x19, [sp, #-0x20]!
    0x100003a4c <+4>:  stp    x29, x30, [sp, #0x10]
    0x100003a50 <+8>:  add    x29, sp, #0x10            ; =0x10 
    0x100003a54 <+12>: mov    x0, x20
    0x100003a58 <+16>: mov    w8, #0x28
    0x100003a5c <+20>: mov    x1, x8
    0x100003a60 <+24>: mov    w8, #0x7
    0x100003a64 <+28>: mov    x2, x8
    0x100003a68 <+32>: bl     0x100003c58               ; symbol stub for: swift_allocObject
    0x100003a6c <+36>: mov    x20, x0
    0x100003a70 <+40>: bl     0x100003aa8               ; SwiftTest.Person.init() -> SwiftTest.Person at main.swift:10
    0x100003a74 <+44>: ldp    x29, x30, [sp, #0x10]
    0x100003a78 <+48>: ldp    x20, x19, [sp], #0x20
    0x100003a7c <+52>: ret 

我们看到了swift_allocObject

下面我们找到关键函数swift_allocObject,进行源码的分析。

4.Swift源码分析

搜索swift_allocObject, 找到HeapObject.cpp文件

#define CALL_IMPL(name, args) do { \
    void *fptr; \
    memcpy(&fptr, (void *)&_ ## name, sizeof(fptr)); \
    extern char _ ## name ## _as_char asm("__" #name "_"); \
    fptr = __ptrauth_swift_runtime_function_entry_strip(fptr); \
    if (SWIFT_UNLIKELY(fptr != &_ ## name ## _as_char)) \
      return _ ## name args; \
    return _ ## name ## _ args; \
} while(0)

HeapObject *swift::swift_allocObject(HeapMetadata const *metadata,
                                     size_t requiredSize,
                                     size_t requiredAlignmentMask) {
  CALL_IMPL(swift_allocObject, (metadata, requiredSize, requiredAlignmentMask));
}

swift_allocObject中调用了_swift_allocObject_函数:

static HeapObject *_swift_allocObject_(HeapMetadata const *metadata,
                                       size_t requiredSize,
                                       size_t requiredAlignmentMask) {
  assert(isAlignmentMask(requiredAlignmentMask));
  auto object = reinterpret_cast<HeapObject *>(
      swift_slowAlloc(requiredSize, requiredAlignmentMask));

  // NOTE: this relies on the C++17 guaranteed semantics of no null-pointer
  // check on the placement new allocator which we have observed on Windows,
  // Linux, and macOS.
  new (object) HeapObject(metadata);

  // If leak tracking is enabled, start tracking this object.
  SWIFT_LEAKS_START_TRACKING_OBJECT(object);

  SWIFT_RT_TRACK_INVOCATION(object, swift_allocObject);

  return object;
}

_swift_allocObject_中又调用了swift_slowAlloc:

void *swift::swift_slowAlloc(size_t size, size_t alignMask) {
  void *p;
  // This check also forces "default" alignment to use AlignedAlloc.
  if (alignMask <= MALLOC_ALIGN_MASK) {
#if defined(__APPLE__)
    p = malloc_zone_malloc(DEFAULT_ZONE(), size);
#else
    p = malloc(size);
#endif
  } else {
    size_t alignment = (alignMask == ~(size_t(0)))
                           ? _swift_MinAllocationAlignment
                           : alignMask + 1;
    p = AlignedAlloc(size, alignment);
  }
  if (!p) swift::crash("Could not allocate memory.");
  return p;
}

可以看到 Swift 中也调用了 malloc 函数。

由此我们可以得到 Swift 对象的内存分配:

  • __allocating_init -> swift_allocObject -> swift_allocObject -> swift_slowAlloc -> Malloc
  • Swift对象的内存结构HeapObject(OC objc_object),有两个属性: 一个是Metadata,一个是RefCount,默认占用16字节大小。
#define SWIFT_HEAPOBJECT_NON_OBJC_MEMBERS       \
  InlineRefCounts refCounts

/// The Swift heap-object header.
/// This must match RefCountedStructTy in IRGen.
struct HeapObject {
  /// This is always a valid pointer to a metadata object.
  HeapMetadata const *__ptrauth_objc_isa_pointer metadata;

  SWIFT_HEAPOBJECT_NON_OBJC_MEMBERS;

#ifndef __swift__
  HeapObject() = default;

  // Initialize a HeapObject header as appropriate for a newly-allocated object.
  constexpr HeapObject(HeapMetadata const *newMetadata) 
    : metadata(newMetadata)
    , refCounts(InlineRefCounts::Initialized)
  { }
  
  // Initialize a HeapObject header for an immortal object
  constexpr HeapObject(HeapMetadata const *newMetadata,
                       InlineRefCounts::Immortal_t immortal)
  : metadata(newMetadata)
  , refCounts(InlineRefCounts::Immortal)
  { }

#ifndef NDEBUG
  void dump() const SWIFT_USED;
#endif

#endif // __swift__
};

简化:
struct HeapObject {
  HeapMetadata const * metadata;
  InlineRefCounts refCounts;
}

objc_object 只有一个 isa,HeapObject 缺有两个属性,接下来我们对 Metadata 进行探索

5.类的结构探索

  1. Metadata源码分析

Metadata是HeapMetadata类型,查看HeapMetadata:

struct InProcess;

template <typename Target> struct TargetHeapMetadata;
using HeapMetadata = TargetHeapMetadata<InProcess>;

这是一个类似别名的定义,下面查看TargetHeapMetadata

template <typename Runtime>
struct TargetHeapMetadata : TargetMetadata<Runtime> {
  using HeaderType = TargetHeapMetadataHeader<Runtime>;

  TargetHeapMetadata() = default;
  constexpr TargetHeapMetadata(MetadataKind kind)
    : TargetMetadata<Runtime>(kind) {}
#if SWIFT_OBJC_INTEROP
  constexpr TargetHeapMetadata(TargetAnyClassMetadata<Runtime> *isa)
    : TargetMetadata<Runtime>(isa) {}
#endif
};
using HeapMetadata = TargetHeapMetadata<InProcess>;
  • TargetHeapMetadata继承了TargetMetadata
  • 在初始化方法中,当是纯Swift类时传入了MetadataKind,如果和OC交互时,它就传了一个isa
  1. MetadataKind 源码分析
enum class MetadataKind : uint32_t {
#define METADATAKIND(name, value) name = value,
#define ABSTRACTMETADATAKIND(name, start, end)                                 \
  name##_Start = start, name##_End = end,
#include "MetadataKind.def"
  
  LastEnumerated = 0x7FF,
};

MetadataKind可以看到它是一个uint32_t类型

和Swift中的类型关联表如下:

/// A class type.
NOMINALTYPEMETADATAKIND(Class, 0)

/// A struct type.
NOMINALTYPEMETADATAKIND(Struct, 0 | MetadataKindIsNonHeap)

/// An enum type.
/// If we add reference enums, that needs to go here.
NOMINALTYPEMETADATAKIND(Enum, 1 | MetadataKindIsNonHeap)

/// An optional type.
NOMINALTYPEMETADATAKIND(Optional, 2 | MetadataKindIsNonHeap)

/// A foreign class, such as a Core Foundation class.
METADATAKIND(ForeignClass, 3 | MetadataKindIsNonHeap)

/// A type whose value is not exposed in the metadata system.
METADATAKIND(Opaque, 0 | MetadataKindIsRuntimePrivate | MetadataKindIsNonHeap)

/// A tuple.
METADATAKIND(Tuple, 1 | MetadataKindIsRuntimePrivate | MetadataKindIsNonHeap)

/// A monomorphic function.
METADATAKIND(Function, 2 | MetadataKindIsRuntimePrivate | MetadataKindIsNonHeap)

/// An existential type.
METADATAKIND(Existential, 3 | MetadataKindIsRuntimePrivate | MetadataKindIsNonHeap)

/// A metatype.
METADATAKIND(Metatype, 4 | MetadataKindIsRuntimePrivate | MetadataKindIsNonHeap)

/// An ObjC class wrapper.
METADATAKIND(ObjCClassWrapper, 5 | MetadataKindIsRuntimePrivate | MetadataKindIsNonHeap)

/// An existential metatype.
METADATAKIND(ExistentialMetatype, 6 | MetadataKindIsRuntimePrivate | MetadataKindIsNonHeap)

/// A heap-allocated local variable using statically-generated metadata.
METADATAKIND(HeapLocalVariable, 0 | MetadataKindIsNonType)

/// A heap-allocated local variable using runtime-instantiated metadata.
METADATAKIND(HeapGenericLocalVariable,
             0 | MetadataKindIsNonType | MetadataKindIsRuntimePrivate)

/// A native error object.
METADATAKIND(ErrorObject,
             1 | MetadataKindIsNonType | MetadataKindIsRuntimePrivate)

/// A heap-allocated task.
METADATAKIND(Task,
             2 | MetadataKindIsNonType | MetadataKindIsRuntimePrivate)

/// A non-task async job.
METADATAKIND(Job,
             3 | MetadataKindIsNonType | MetadataKindIsRuntimePrivate)

kind种类总结如下

namevalue
Class0x0
Struct0x200
Enum0x201
Optional0x202
ForeignClass0x203
Opaque0x300
Tuple0x301
Function0x302
Existential0x303
Metatype0x304
ObjCClassWrapper0x305
ExistentialMetatype0x306
HeapLocalVariable0x400
HeapGenericLocalVariable0x500
ErrorObject0x501
LastEnumerated0x7FF
  1. 类结构源码分析

查看TargetHeapMetadata的父类TargetMetadata:

template <typename Runtime>
struct TargetMetadata {
  using StoredPointer = typename Runtime::StoredPointer;

  /// The basic header type.
  typedef TargetTypeMetadataHeader<Runtime> HeaderType;

  constexpr TargetMetadata()
    : Kind(static_cast<StoredPointer>(MetadataKind::Class)) {}
  constexpr TargetMetadata(MetadataKind Kind)
    : Kind(static_cast<StoredPointer>(Kind)) {}

#if SWIFT_OBJC_INTEROP
protected:
  constexpr TargetMetadata(TargetAnyClassMetadata<Runtime> *isa)
    : Kind(reinterpret_cast<StoredPointer>(isa)) {}
#endif

private:
  /// The kind. Only valid for non-class metadata; getKind() must be used to get
  /// the kind value.
  StoredPointer Kind;
public:
  /// Get the metadata kind.
  MetadataKind getKind() const {
    return getEnumeratedMetadataKind(Kind);
  }
  
  /// Set the metadata kind.
  void setKind(MetadataKind kind) {
    Kind = static_cast<StoredPointer>(kind);
  }
  
  ......
  
  ConstTargetMetadataPointer<Runtime, TargetTypeContextDescriptor>
  getTypeContextDescriptor() const {
    switch (getKind()) {
    case MetadataKind::Class: {
      const auto cls = static_cast<const TargetClassMetadata<Runtime> *>(this);
      if (!cls->isTypeMetadata())
        return nullptr;
      if (cls->isArtificialSubclass())
        return nullptr;
      return cls->getDescription();
    }
    case MetadataKind::Struct:
    case MetadataKind::Enum:
    case MetadataKind::Optional:
      return static_cast<const TargetValueMetadata<Runtime> *>(this)
          ->Description;
    case MetadataKind::ForeignClass:
      return static_cast<const TargetForeignClassMetadata<Runtime> *>(this)
          ->Description;
    default:
      return nullptr;
    }
  }

可以看到TargetMetadata中有一个Kind成员变量,当kind是Class时,会将this强转为TargetClassMetadata类型。当kind是Struct时,会将this强转为TargetValueMetadata类型

查看TargetClassMetadata

struct TargetClassMetadata : public TargetAnyClassMetadata<Runtime> {}

struct TargetAnyClassMetadata : public TargetHeapMetadata<Runtime> {
    ......
    
    TargetSignedPointer<Runtime, const TargetClassMetadata<Runtime> * __ptrauth_swift_objc_superclass> Superclass;

#if SWIFT_OBJC_INTEROP
  TargetPointer<Runtime, void> CacheData[2];

  StoredSize Data;
  
  static constexpr StoredPointer offsetToData() {
    return offsetof(TargetAnyClassMetadata, Data);
  }
#endif
}

在TargetAnyClassMetadata中我们可以找到superclass、Data、CacheData等成员变量,经过上面一系列的分析,我们可以得到swift类的数据结构如下

struct Metadata { 
    var kind: Int
    var superClass: Any.Type
    var cacheData: (Int, Int)
    var data: Int
    var classFlags: Int32
    var instanceAddressPoint: UInt32 var instanceSize: UInt32
    var instanceAlignmentMask: UInt16
    var reserved: UInt16
    var classSize: UInt32
    var classAddressPoint: UInt32
    var typeDescriptor: UnsafeMutableRawPointer var iVarDestroyer: UnsafeRawPointer
}

运行代码

struct HeapObject{
    var metadata: UnsafeRawPointer
    var refcounted1: UInt32
    var refcounted2: UInt32
}

struct Metadata{
    var kind: Int
    var superClass: Any.Type
    var cacheData: (Int, Int)
    var data: Int
    var classFlags: Int32
    var instanceAddressPoint: UInt32
    var instanceSize: UInt32
    var instanceAlignmentMask: UInt16
    var reserved: UInt16
    var classSize: UInt32
    var classAddressPoint: UInt32
    var typeDescriptor: UnsafeMutableRawPointer
    var iVarDestroyer: UnsafeRawPointer
}

class Person {
    var age: Int = 18
    var name: String = "tony"
    
    init(age: Int, metadataname: String) {
        self.age = age
        self.name = metadataname
    }
}
var p = Person(age: 18, metadataname: "tony")

let objcRawPtr = Unmanaged.passUnretained(p as AnyObject).toOpaque()
let objcPtr = objcRawPtr.bindMemory(to: HeapObject.self, capacity: 1)
print(objcPtr.pointee)

let metadata = objcPtr.pointee.metadata.bindMemory(to: Metadata.self, capacity: MemoryLayout<Metadata>.stride).pointee
print(metadata)
print("end")

<!--输出结果-->
HeapObject(metadata: 0x00000001000082b0, refcounted1: 3, refcounted2: 0)
Metadata(kind: 4295000696, superClass: _TtCs12_SwiftObject, cacheData: (6598086240, 140943646785536), data: 4302323938, classFlags: 2, instanceAddressPoint: 0, instanceSize: 40, instanceAlignmentMask: 7, reserved: 0, classSize: 168, classAddressPoint: 16, typeDescriptor: 0x0000000100003c78, iVarDestroyer: 0x0000000000000000)
end

----------------------------------------分割线-------------------------------------

值类型

前提:需要了解内存五大区,如下所示

1011

  • 栈空间 比 堆空间 大
  • 栈是从高地址->低地址,向下延伸,由系统自动管理,是一片连续的内存空间
  • 堆是从低地址->高地址,向上延伸,由程序员管理,堆空间结构类似于链表,是不连续的
  • 日常开发中的溢出是指堆栈溢出,可以理解为栈区与堆区边界碰撞的情况
  • 全局区、常量区都存储在Mach-O中的__TEXT cString段

我们通过一个例子来引入什么是值类型

func test(){
    //栈区声明一个地址,用来存储age变量
    var age = 18
    //传递的值
    var age2 = age
    //age、age2是修改独立内存中的值
    age = 30
    age2 = 45
    
    print("age=\(age),age2=\(age2)")
}
test()

从例子中可以得出,age存储在栈区

  • 查看age的内存情况,从图中可以看出,栈区直接存储的是值
获取age的栈区地址:
po withUnsafePointer(to: &age) { print($0) }
查看age内存情况:
(lldb) x/8g 0x000000016fdff3b8


(lldb) 
0x000000016fdff3b8
0 elements

(lldb) x/8g 0x000000016fdff3b8
0x16fdff3b8: 0x0000000000000012 0x0000000000000000
0x16fdff3c8: 0x0000000000000000 0x000000016fdff3e0
0x16fdff3d8: 0x0000000100003b98 0x000000016fdff400
0x16fdff3e8: 0x0000000183fcd430 0x0000000183fcd430
  • 查看age2的情况,从下图中可以看出,age2的赋值相当于将age中的值拿出来,赋值给了age2。其中age 与 age2 的地址 相差了8字节,从这里可以说明栈空间是连续的、且是从高到低的
(lldb) po withUnsafePointer(to: &age2) { print($0) }
0x000000016fdff3b0
0 elements

(lldb) x/8g 0x000000016fdff3b0
0x16fdff3b0: 0x0000000000000012 0x0000000000000012
0x16fdff3c0: 0x0000000000000000 0x0000000000000000
0x16fdff3d0: 0x000000016fdff3e0 0x0000000100003b98
0x16fdff3e0: 0x000000016fdff400 0x0000000183fcd430

所以,从上面可以说明,age就是值类型

值类型特点

  1. 地址中存储的是值
  2. 值类型的传递过程中,相当于传递了一个副本,也就是所谓的深拷贝
  3. 值传递过程中,并不共享状态

结构体

结构体的常用写法

//***** 写法一 *****
struct JXTeacher {
    var age: Int = 18
    
    func teach(){
        print("teach")
    }
}
var t1 = JXTeacher()

//***** 写法二 *****
struct JXTTeacher {
    var age: Int
    
    func teach(){
        print("teach")
    }
}
var t2 = JXTTeacher(age: 18)
  • 在结构体中,如果不给属性默认值,编译是不会报错的。即在结构体中属性可以赋值,也可以不赋值
  • init方法可以重写,也可以使用系统默认的

结构体的SIL分析

  • 如果没有init,系统会提供不同的默认初始化方法
struct JXTeacher {
  @_hasStorage @_hasInitialValue var age: Int { get set }
  func teach()
  init()
  init(age: Int = 18)
}
  • 如果提供了自定义的init,就只有自定义的
struct JXTTeacher {
  @_hasStorage var age: Int { get set }
  func teach()
  init(age: Int)
}

为什么结构体是值类型?

定义一个结构体,并进行分析

struct JXTeacher {
    var age: Int = 18
    var age2: Int = 20
}
var  t = JXTeacher()
print("end")  //断点
  • 打印t:po t,从下图中可以发现,t的打印直接就是值,没有任何与地址有关的信息
(lldb) po t
▿ JXTeacher
  - age : 18
  - age2 : 20
  • 获取t的内存地址,并查看其内存情况
获取age的栈区地址:
po withUnsafePointer(to: &t) { print($0) }
查看age内存情况:
(lldb) x/8g 0x0000000100008030

(lldb) po withUnsafePointer(to: &t) { print($0) }
0x0000000100008030
0 elements

(lldb) x/8g 0x0000000100008030
0x100008030: 0x0000000000000012 0x0000000000000014
0x100008040: 0x0000000000000000 0x0000000000000000
0x100008050: 0x0000000000000000 0x0000000000000000
0x100008060: 0x0000000000000000 0x0000000000000000

问题:此时将t赋值给t1,如果修改了t1,t会发生改变吗?

  • 直接打印t及t1,可以发现t并没有因为t1的改变而改变,主要是因为因为t1和t之间是值传递,即t1和t是不同内存空间,是直接将t中的值拷贝至t1中。t1修改的内存空间,是不会影响t的内存空间的
struct JXTeacher {
    var age: Int = 18
    var age2: Int = 20
}
var  t = JXTeacher()
var t1 = t
t1.age = 30
print("end")  //断点


(lldb) po t
▿ JXTeacher
  - age : 18
  - age2 : 20

(lldb) po t1
▿ JXTeacher
  - age : 30
  - age2 : 20

SIL验证 同样的,我们也可以通过分析SIL来验证结构体是值类型

  • 在SIL文件中,我们查看结构体的初始化方法,可以发现只有init,而没有malloc,在其中看不到任何关于堆区的分配
// main
sil @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 {
bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>):
  alloc_global @main.t : main.JXTeacher          // id: %2
  %3 = global_addr @main.t : main.JXTeacher : $*JXTeacher // users: %7, %10
  %4 = metatype $@thin JXTeacher.Type             // user: %6
  // function_ref JXTeacher.init()
  // 只有init方法,没有malloc
  %5 = function_ref @main.JXTeacher.init() -> main.JXTeacher : $@convention(method) (@thin JXTeacher.Type) -> JXTeacher // user: %6
  %6 = apply %5(%4) : $@convention(method) (@thin JXTeacher.Type) -> JXTeacher // user: %7
  store %6 to %3 : $*JXTeacher                    // id: %7
  ......
  %44 = integer_literal $Builtin.Int32, 0         // user: %45
  %45 = struct $Int32 (%44 : $Builtin.Int32)      // user: %46
  return %45 : $Int32                             // id: %46
} // end sil function 'main'



// JXTeacher.init()
sil hidden @main.JXTeacher.init() -> main.JXTeacher : $@convention(method) (@thin JXTeacher.Type) -> JXTeacher {
// %0 "$metatype"
bb0(%0 : $@thin JXTeacher.Type):
  // 默认alloc一个self,即结构体自身,其内存在栈区
  %1 = alloc_stack $JXTeacher, let, name "self"   // users: %6, %2, %11
  // 根据结构体首地址,拿到age的地址
  %2 = struct_element_addr %1 : $*JXTeacher, #JXTeacher.age // user: %5
  %3 = integer_literal $Builtin.Int64, 18         // user: %4
  %4 = struct $Int (%3 : $Builtin.Int64)          // users: %5, %10
  // 将值存储地址中
  store %4 to %2 : $*Int                          // id: %5
  %6 = struct_element_addr %1 : $*JXTeacher, #JXTeacher.age2 // user: %9
  %7 = integer_literal $Builtin.Int64, 20         // user: %8
  %8 = struct $Int (%7 : $Builtin.Int64)          // users: %9, %10
  store %8 to %6 : $*Int                          // id: %9
  %10 = struct $JXTeacher (%4 : $Int, %8 : $Int)  // user: %12
  dealloc_stack %1 : $*JXTeacher                  // id: %11
  // 返回结构体自身
  return %10 : $JXTeacher                         // id: %12
} // end sil function 'main.JXTeacher.init() -> main.JXTeacher'

总结

  • 结构体是值类型,且结构体的地址就是第一个成员的内存地址
  • 值类型
    • 在内存中直接存储值
    • 值类型的赋值,是一个值传递的过程,即相当于拷贝了一个副本,存入不同的内存空间,两个空间彼此间并不共享状态
    • 值传递其实就是深拷贝

引用类型

类的常用写法

//****** 写法一 *******
class JXTeacher {
    var age: Int = 18
    
    func teach(){
        print("teach")
    }
    init(_ age: Int) {
        self.age = age
    }
}
var t1 = JXTeacher.init(20)

//****** 写法二 *******
class JXTTeacher {
    var age: Int?
    
    func teach(){
        print("teach")
    }
    init(_ age: Int) {
        self.age = age
    }
}
var t2 = JXTTeacher.init(20)
  • 在类中,如果属性没有赋值,也不是可选项,编译会报错
  • 需要自己实现init方法

为什么类是引用类型?

定义一个类,通过一个例子来说明

struct JXTeacher {
    var age: Int = 18
    var age2: Int = 20
}
class JXTTeacher {
    var age: Int = 18
    var age2: Int = 20
}
var t1 = JXTeacher()
var t2 = JXTTeacher()
print("end")


(lldb) po t1
▿ JXTeacher
  - age : 18
  - age2 : 20

(lldb) po t2
<JXTTeacher: 0x1006072f0>

类初始化的对象t1,存储在全局区

  • 打印t1、t2: 从图中可以看出,t2内存空间中存放的是地址,t1中存储的是值
  • 获取t2变量的地址,并查看其内存情况
获取t2指针地址
po withUnsafePointer(to: &t2) { print($0) }
查看t2全局区地址内存情况: x/8g 0x0000000100008220
查看t2地址中存储的堆区地址内存情况: x/8g 0x00000001006072f0


(lldb) po withUnsafePointer(to: &t2) { print($0) }
0x0000000100008220
0 elements

(lldb) x/8g 0x0000000100008220
0x100008220: 0x00000001006072f0 0x0000000000000000
0x100008230: 0x0000000000000000 0x0000000000000000
0x100008240: 0x0000000000000000 0x0000000000000000
0x100008250: 0x0000000000000000 0x0000000000000000
(lldb) x/8g 0x00000001006072f0
0x1006072f0: 0x0000000100008178 (metadata) 0x0000000200000003 (refCounts)
0x100607300: 0x0000000000000012 (age的值18) 0x0000000000000014 (age2的值20)
0x100607310: 0x0000000000000000 0x0000000000000000
0x100607320: 0x0000000189570002 0x000200018a5bd650

引用类型特点

  1. 地址中存储的是堆区地址
  2. 堆区地址中存储的是值

问题1:此时将t2赋值给t3,如果修改了t3,会导致t2修改吗?

  • 通过lldb调试得知,修改了t3,会导致t3改变,主要是因为t2、t3地址中都存储的是 同一个堆区地址,如果修改,修改是同一个堆区地址,所以修改t2会导致t1一起修改,即浅拷贝
class JXTTeacher {
    var age: Int = 18
    var age2: Int = 20
}
var t2 = JXTTeacher()
var t3 = t2
t3.age = 30
print("end")



(lldb) po withUnsafePointer(to: &t2) { print($0) }
0x0000000100008220
0 elements

(lldb) po withUnsafePointer(to: &t3) { print($0) }
0x0000000100008228
0 elements

(lldb) x/8g 0x0000000100008220
0x100008220: 0x000000010603e2a0(堆区地址) 0x000000010603e2a0
0x100008230: 0x0000000000000000 0x0000000000000000
0x100008240: 0x0000000000000000 0x0000000000000000
0x100008250: 0x0000000000000000 0x0000000000000000
(lldb) x/8g 0x0000000100008228
0x100008228: 0x000000010603e2a0(堆区地址, 跟上面是同一个)  0x0000000000000000
0x100008238: 0x0000000000000000 0x0000000000000000
0x100008248: 0x0000000000000000 0x0000000000000000
0x100008258: 0x0000000000000000 0x0000000000000000
(lldb) x/8g 0x000000010603e2a0
0x10603e2a0: 0x0000000100008178 0x0000000200000003
0x10603e2b0: 0x000000000000001e 0x0000000000000014
0x10603e2c0: 0x00000009a0080001 0x00000001e36b3d48
0x10603e2d0: 0x0000000000000000 0x00000001ef3e7bc0
(lldb) po t2.age
30

(lldb) po t3.age
30

问题2:如果结构体中包含类对象,此时如果修改t2中的实例对象属性,t2会改变吗?

代码如下所示

struct JXTeacher {
    var age: Int = 18
    var age2: Int = 20
    var teacher: JXTTeacher = JXTTeacher()
}
class JXTTeacher {
    var age: Int = 18
    var age2: Int = 20
}

var t = JXTeacher()
var t1 = t
t1.teacher.age = 30
print("end")

(lldb) po t1.teacher.age
30

(lldb) po t.teacher.age
30

从打印结果中可以看出,如果修改t1中的实例对象属性,会导致t中实例对象属性的改变。虽然在结构体中是值传递,但是对于teacher,由于是引用类型,所以传递的依然是地址

同样可以通过lldb调试验证

(lldb) po withUnsafePointer(to: &t) { print($0) }
0x0000000100008210
0 elements

(lldb) x/8g 0x0000000100008210
0x100008210: 0x0000000000000012(age:18) 0x0000000000000014(age2:20)
0x100008220: 0x000000010058c920(teacher地址) 0x0000000000000012
0x100008230: 0x0000000000000014 0x000000010058c920
0x100008240: 0x0000000000000000 0x0000000000000000
(lldb) x/8g 0x000000010058c920 (打印teacher内存情况)
0x10058c920: 0x0000000100008178 0x0000000200000003
0x10058c930: 0x000000000000001e(age:30) 0x0000000000000014(age2:20)
0x10058c940: 0x0000000000000000 0x0000000000000000
0x10058c950: 0x000000006fdf0006 0x000000016fdff0d0

注意 在编写代码过程中,应该尽量避免值类型包含引用类型

查看当前的SIL文件,尽管JXTTeacher是放在值类型中的,在传递的过程中,不管是传递还是赋值,teacher都是按照引用计数进行管理的

// main
sil @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 {
bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>):
  alloc_global @main.t : main.JXTeacher          // id: %2
  %3 = global_addr @main.t : main.JXTeacher : $*JXTeacher // users: %7, %10
  %4 = metatype $@thin JXTeacher.Type             // user: %6
  // function_ref JXTeacher.init()
  %5 = function_ref @main.JXTeacher.init() -> main.JXTeacher : $@convention(method) (@thin JXTeacher.Type) -> @owned JXTeacher // user: %6
  %6 = apply %5(%4) : $@convention(method) (@thin JXTeacher.Type) -> @owned JXTeacher // user: %7
  store %6 to %3 : $*JXTeacher                    // id: %7
  alloc_global @main.t1 : main.JXTeacher         // id: %8
  %9 = global_addr @main.t1 : main.JXTeacher : $*JXTeacher // users: %13, %11
  %10 = begin_access [read] [dynamic] %3 : $*JXTeacher // users: %12, %11
  copy_addr %10 to [initialization] %9 : $*JXTeacher // id: %11
  end_access %10 : $*JXTeacher                    // id: %12
  %13 = begin_access [read] [dynamic] %9 : $*JXTeacher // users: %17, %14
  
  // 对JXTTeacher的引用计数+1
  %14 = struct_element_addr %13 : $*JXTeacher, #JXTeacher.teacher // user: %15
  %15 = load %14 : $*JXTTeacher                   // users: %22, %20, %21, %16
  strong_retain %15 : $JXTTeacher                 // id: %16
  
  end_access %13 : $*JXTeacher                    // id: %17
  ......
} // end sil function 'main'


// JXTeacher.teacher.getter
sil hidden [transparent] @main.JXTeacher.teacher.getter : main.JXTTeacher : $@convention(method) (@guaranteed JXTeacher) -> @owned JXTTeacher {
// %0 "self"                                      // users: %2, %1
bb0(%0 : $JXTeacher):
  debug_value %0 : $JXTeacher, let, name "self", argno 1 // id: %1
  %2 = struct_extract %0 : $JXTeacher, #JXTeacher.teacher // users: %4, %3
  // 引用计数+1
  strong_retain %2 : $JXTTeacher                  // id: %3
  return %2 : $JXTTeacher                         // id: %4
} // end sil function 'main.JXTeacher.teacher.getter : main.JXTTeacher'


// JXTeacher.teacher.setter
sil hidden [transparent] @main.JXTeacher.teacher.setter : main.JXTTeacher : $@convention(method) (@owned JXTTeacher, @inout JXTeacher) -> () {
// %0 "value"                                     // users: %11, %8, %4, %2
// %1 "self"                                      // users: %5, %3
bb0(%0 : $JXTTeacher, %1 : $*JXTeacher):
  debug_value %0 : $JXTTeacher, let, name "value", argno 1 // id: %2
  debug_value_addr %1 : $*JXTeacher, var, name "self", argno 2 // id: %3
  // 引用计数+1
  strong_retain %0 : $JXTTeacher                  // id: %4
  %5 = begin_access [modify] [static] %1 : $*JXTeacher // users: %10, %6
  %6 = struct_element_addr %5 : $*JXTeacher, #JXTeacher.teacher // users: %8, %7
  %7 = load %6 : $*JXTTeacher                     // user: %9
  store %0 to %6 : $*JXTTeacher                   // id: %8
  strong_release %7 : $JXTTeacher                 // id: %9
  end_access %5 : $*JXTeacher                     // id: %10
  strong_release %0 : $JXTTeacher                 // id: %11
  %12 = tuple ()                                  // user: %13
  return %12 : $()                                // id: %13
} // end sil function 'main.JXTeacher.teacher.setter : main.JXTTeacher'

可以通过打印teacher的引用计数来验证我们的说法,其中teacher的引用计数为3

(lldb) po CFGetRetainCount(t.teacher)
3

主要是是因为:

  • main中retain一次
  • teacher.getter方法中retain一次
  • teacher.setter方法中retain一次

mutaing

通过结构体定义一个栈,主要有push、pop方法,此时我们需要动态修改栈中的数组

  • 如果是以下这种写法,会直接报错,原因是值类型本身是不允许修改属性的
struct JXStack {
    var items: [Int] = []
    func push(_ item: Int) {
        items.append(item)
    }
}
报错: Cannot use mutating member on immutable value: 'self' is immutable
  • 将push方法改成下面的方式,查看SIL文件中的push函数
struct JXStack {
    var items: [Int] = []
    func push(_ item: Int) {
        print(item)
    }
}

// JXStack.push(_:)
sil hidden @main.JXStack.push(Swift.Int) -> () : $@convention(method) (Int, @guaranteed JXStack) -> () {
// %0 "item"                                      // users: %11, %2
// %1 "self"                                      // user: %3
bb0(%0 : $Int, %1 : $JXStack):
  // 此时的self是let类型,即是不允许修改的
  debug_value %0 : $Int, let, name "item", argno 1 // id: %2
  debug_value %1 : $JXStack, let, name "self", argno 2 // id: %3
  %4 = integer_literal $Builtin.Word, 1           // user: %6
  ......
}

从图中可以看出,push函数除了item,还有一个默认参数self,self是let类型,表示不允许修改

  • 尝试1:如果将push函数修改成下面这样,可以添加进去吗?
struct JXStack {
    var items: [Int] = []
    func push(_ item: Int) {
        var s = self
        s.items.append(item)
    }
}
var s = JXStack()
s.push(1)
print(s.items)

打印: []

以得出上面的代码并不能将item添加进去,因为s是另一个结构体对象,相当于值拷贝,此时调用push是将item添加到s的数组中了

  • 根据前文中的错误提示,给push添加mutaing,发现可以添加到数组了
struct JXStack {
    var items: [Int] = []
    mutating func push(_ item: Int) {
        items.append(item)
    }
}
var s = JXStack()
s.push(1)
print(s.items)

打印: [1]

查看其SIL文件,找到push函数,发现与之前有所不同,push添加mutaing(只用于值类型)后,本质上是给值类型函数添加了inout关键字,相当于在值传递的过程中,传递的是引用(即地址)

// JXStack.push(_:)
// mutaing的本质:添加了inout输入输出
sil hidden @main.JXStack.push(Swift.Int) -> () : $@convention(method) (Int, @inout JXStack) -> () {
// %0 "item"                                      // users: %5, %2
// %1 "self"                                      // users: %6, %3
bb0(%0 : $Int, %1 : $*JXStack):
  debug_value %0 : $Int, let, name "item", argno 1 // id: %2
  // self是var类型,可以修改,而且这里访问的地址,并不是原始的值
  debug_value_addr %1 : $*JXStack, var, name "self", argno 2 // id: %3
  %4 = alloc_stack $Int                           // users: %5, %11, %9
  store %0 to %4 : $*Int                          // id: %5
  %6 = begin_access [modify] [static] %1 : $*JXStack // users: %10, %7
  %7 = struct_element_addr %6 : $*JXStack, #JXStack.items // user: %9
  // function_ref Array.append(_:)
  %8 = function_ref @Swift.Array.append(__owned A) -> () : $@convention(method) <τ_0_0> (@in τ_0_0, @inout Array<τ_0_0>) -> () // user: %9
  %9 = apply %8<Int>(%4, %7) : $@convention(method) <τ_0_0> (@in τ_0_0, @inout Array<τ_0_0>) -> ()
  end_access %6 : $*JXStack                       // id: %10
  dealloc_stack %4 : $*Int                        // id: %11
  %12 = tuple ()                                  // user: %13
  return %12 : $()                                // id: %13
} // end sil function 'main.JXStack.push(Swift.Int) -> ()'

inout关键字

一般情况下,在函数的声明中,默认的参数都是不可变的,如果想要直接修改,需要给参数加上inout关键字

  • 未加inout关键字,给参数赋值,编译报错
func swap(_ a: Int, _ b: Int) {
    let temp = a
    a = b
    b = temp
}

Cannot assign to value: 'a' is a 'let' constant
Cannot assign to value: 'b' is a 'let' constant
  • 添加inout关键字,可以给参数赋值
func swap(_ a: inout Int, _ b: inout Int) {
    let temp = a
    a = b
    b = temp
}
var a = 8
var b = 9
swap(&a, &b)
print("end")

(lldb) po a
9

(lldb) po b
8

总结

  1. 结构体中的函数如果想修改其中的属性,需要在函数前加上mutating,而类则不用
  2. mutating本质也是加一个 inout修饰的self
  3. inout相当于取地址,可以理解为地址传递,即引用
  4. mutating修饰方法,而inout 修饰参数

总结

通过上述LLDB查看结构体 & 类的内存模型,有以下总结:

  • 值类型,相当于一个本地excel,当我们通过QQ传给你一个excel时,就相当于一个值类型,你修改了什么我们这边是不知道的
  • 引用类型,相当于一个在线表格,当我们和你共同编辑一个在先表格时,就相当于一个引用类型,两边都会看到修改的内容
  • 结构体中函数修改属性, 需要在函数前添加mutating关键字,本质是给函数的默认参数self添加了inout关键字,将self从let常量改成了var变量

方法调度

通过上面的分析,我们有以下疑问:结构体和类的方法存储在哪里?下面来一一进行分析

静态派发

值类型对象的函数的调用方式是静态调用,即直接地址调用,调用函数指针,这个函数指针在编译、链接完成后就已经确定了,存放在代码段,而结构体内部并不存放方法。因此可以直接通过地址直接调用

struct JXTeacher {
    var age: Int = 18
    
    func teach(){
        print("teach")
    }
}
var t1 = JXTeacher()

print("end")
  • 结构体函数调试如下所示
TT`main:
    0x100003c00 <+0>:   sub    sp, sp, #0x40             ; =0x40 
    0x100003c04 <+4>:   stp    x29, x30, [sp, #0x30]
    0x100003c08 <+8>:   add    x29, sp, #0x30            ; =0x30 
    0x100003c0c <+12>:  bl     0x100003e10               ; TT.JXTeacher.init() -> TT.JXTeacher at main.swift:11
    0x100003c10 <+16>:  mov    x8, x0
    0x100003c14 <+20>:  adrp   x9, 5
    0x100003c18 <+24>:  str    x9, [sp]
    0x100003c1c <+28>:  adrp   x0, 5
    0x100003c20 <+32>:  add    x0, x0, #0x40             ; =0x40 
    0x100003c24 <+36>:  str    x8, [x9, #0x40]
    0x100003c28 <+40>:  add    x1, sp, #0x18             ; =0x18 
    0x100003c2c <+44>:  str    x1, [sp, #0x8]
    0x100003c30 <+48>:  mov    w8, #0x20
    0x100003c34 <+52>:  mov    x2, x8
    0x100003c38 <+56>:  mov    x3, #0x0
    0x100003c3c <+60>:  bl     0x100003e80               ; symbol stub for: swift_beginAccess
    0x100003c40 <+64>:  ldr    x8, [sp]
    0x100003c44 <+68>:  ldr    x0, [sp, #0x8]
    0x100003c48 <+72>:  ldr    x8, [x8, #0x40]
    0x100003c4c <+76>:  str    x8, [sp, #0x10]
    0x100003c50 <+80>:  bl     0x100003e98               ; symbol stub for: swift_endAccess
    0x100003c54 <+84>:  ldr    x0, [sp, #0x10]
    直接地址调用,即静态派发
->  0x100003c58 <+88>:  bl     0x100003c98               ; TT.JXTeacher.teach() -> () at main.swift:14
    0x100003c5c <+92>:  mov    w0, #0x0
    0x100003c60 <+96>:  ldp    x29, x30, [sp, #0x30]
    0x100003c64 <+100>: add    sp, sp, #0x40             ; =0x40 
    0x100003c68 <+104>: ret    

  • 打开打开demo的Mach-O可执行文件,其中的__text段,就是所谓的代码段,需要执行的汇编指令都在这里 Section64 (__TEXT, __text) -> Assembly

301

对于上面的分析,还有个疑问:直接地址调用后面是符号,这个符号哪里来的?

302

是从Mach-O文件中的符号表Symbol Tables,但是符号表中并不存储字符串,字符串存储在String Table(字符串表,存放了所有的变量名和函数名,以字符串形式存储),然后根据符号表中的偏移值到字符串中查找对应的字符,然后进行命名重整:工程名+类名+函数名,如下所示

303

304

  • Symbol Table:存储符号位于字符串表的位置
    • Dynamic Symbol Table:动态库函数位于符号表的偏移信息

还可以通过终端命令nm,获取项目中的符号表

  • 查看符号表:
nm mach-o文件路径

// 获取所有符号
nm /Users/jxwbjmac0003/Library/Developer/Xcode/DerivedData/TT-gdrpjudaexihasfhedwqvhmxjtal/Build/Products/Debug/TT 
  • 通过命令还原符号名称:
xcrun swift-demangle 符号

// 还原第一个符号
xcrun swift-demangle s2TT9JXTeacherV5teachyyF
$s2TT9JXTeacherV5teachyyF ---> TT.JXTeacher.teach() -> ()
  • 如果将edit scheme -> run中的debug改成release,编译后查看,在可执行文件目录下,多一个后缀为dSYM的文件,此时,再去Mach-O文件中查找teach,发现是找不到,其主要原因是因为静态链接的函数,实际上是不需要符号的,一旦编译完成,其地址确定后,当前的符号表就会删除当前函数对应的符号,在release环境下,符号表中存储的只是不能确定地址的符号

  • 对于不能确定地址的符号,是在运行时确定的,即函数第一次调用时(相当于懒加载),例如print,是通过dyld_stub_bind确定地址的(这个在最新版的12.2中通过断点调试并未找到,后续待继续验证,有不同见解的,欢迎留言指出)

    0x100003d10: adr    x17, #0x4478              ; _dyld_private
    0x100003d14: nop    
    0x100003d18: stp    x16, x17, [sp, #-0x10]!
    0x100003d1c: nop    
    0x100003d20: ldr    x16, #0x2f0               ; (void *)0x00000001895cbbd8: dyld_stub_binder(懒加载)
    0x100003d24: br     x16
    0x100003d28: ldr    w16, 0x100003d30
    0x100003d2c: b      0x100003d10

函数符号命名规则

  • 对于C函数来说,命名的重整规则就是在函数名之前加_(注意:C中不允许函数重载,因为没有办法区分)
#include <stdio.h>
void test(){}

305

  • 对于OC来说,也不支持函数重载,其符号命名规则是-[类名 函数名]
@interface OCTest : NSObject
- (void)ocTest;
@end

312

  • 对于Swift来说,是函数重载,主要是因为swift中的重整命名规则比较复杂,可以确保函数符号的唯一性

补充:ASLR

下面是针对函数地址的一个验证

  • 通过运行发现,Mach-O中的地址与调试时直接获取的地址是由一定偏差的,其主要原因是实际调用时地址多了一个ASLR(地址空间布局随机化 address space layout randomizes)
    0x1000039bc <+100>: ldr    x0, [sp, #0x18]
    0x1000039c0 <+104>: bl     0x100003aa8               ; SwiftTest.JXTeacher.teach() -> () at main.swift:15
    0x1000039c4 <+108>: ldr    x1, [sp, #0x30]
    0x1000039c8 <+112>: mov    w8, #0x1
    0x1000039cc <+116>: mov    x0, x8
    0x1000039d0 <+120>: bl     0x100003d48               ; symbol stub for: Swift._allocateUninitializedArray<τ_0_0>(Builtin.Word) -> (Swift.Array<τ_0_0>, Builtin.RawPointer)

313

  • 可以通过image list查看,其中0x0000000100000000是程序运行的首地址,后8位是随机偏移00000000(即ASLR)
(lldb) image list
[  0] 2D7B9C51-B077-3AAA-8D1C-28C9404C814F 0x0000000100000000 /Users/jxwbjmac0003/Library/Developer/Xcode/DerivedData/SwiftTest-ezgreflbigvpqjcibzqxeikfdqsy/Build/Products/Debug/SwiftTest 
[  1] 38657979-1ABE-3C9A-BF64-EF3B746216AB 0x0000000100014000 /usr/lib/dyld 
[  2] A23D1D3A-AD28-3AC2-AEAF-53F4B7A5B2F5 0x000000018a3f2000 /System/Library/Frameworks/Foundation.framework/Versions/C/Foundation
[  3] 252C93CC-2D39-3C15-87F6-1336658B2F49 0x0000000189441000 /usr/lib/libobjc.A.dylib 
[  4] 1E75FCDF-2357-30FE-AAAD-5290BA722464 0x000000019304a000 /usr/lib/libSystem.B.dylib 
[  5] 1FC1BD60-DC83-3CC7-89AC-D734DC18473A 0x000000018962a000 /System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation 
[  6] 9CA57826-61D7-3095-9476-AE0FE45A3805 0x0000000195244000 /usr/lib/swift/libswiftCore.dylib 
  • 将Mach-O中的文件地址0x0000000100003C98 + 0x00000000 = 0x100003C98,正好对应上面调用的地址

动态派发

汇编指令补充

  • blr:带返回的跳转指令,跳转到指令后边跟随寄存器中保存的地址
  • mov:将某一寄存器的值复制到另一寄存器(只能用于寄存器与起存起或者 寄存器与常量之间 传值,不能用于内存地址)
    • mov x1, x0 将寄存器x0的值复制到寄存器x1中
  • ldr:将内存中的值读取到寄存器中
    • ldr x0, [x1, x2] 将寄存器x1和寄存器x2 相加作为地址,取该内存地址的值翻入寄存器x0中
  • str:将寄存器中的值写入到内存中
    • str x0, [x0, x8] 将寄存器x0的值保存到内存[x0 + x8]处
  • bl:跳转到某地址

探索class的调度方式

首先介绍下V_Table在SIL文件中的格式

VTables
~~~~~~~
::
  // 声明sil vtable关键字
  decl ::= sil-vtable
  // sil vtable中包含 关键字、标识(即类名)、所有的方法
  sil-vtable ::= 'sil_vtable' identifier '{' sil-vtable-entry* '}'

  // 方法中包含了声明以及函数名称
  sil-vtable-entry ::= sil-decl-ref ':' sil-linkage? sil-function-name

例如,以CJLTacher为例,其SIL中的v-table如下所示

class JXTeacher{
    func teach(){}
    func teach2(){}
    func teach3(){}
    @objc deinit{}
    init(){}
}

307

  • sil_vtable:关键字
  • JXTeacher:表示是JXTeacher类的函数表
  • 其次就是当前方法的声明对应着方法的名称
  • 函数表 可以理解为 数组,声明在 class内部的方法在不加任何关键字修饰的过程中,是连续存放在我们当前的地址空间中的。这一点,可以通过断点来印证,

309

  • register read x20,此时的地址和 实例对象的地址是相同的,其中x8 实例对象地址,即首地址

310

观察这几个方法的偏移地址,可以发现方法是连续存放的,正好对应V-Table函数表中的排放顺序,即是按照定义顺序排放在函数表中

311

函数表源码探索

  • 源码中搜索initClassVTable,并加上断点,然后写上源码进行调试
/// Using the information in the class context descriptor, fill in in the
/// immediate vtable entries for the class and install overrides of any
/// superclass vtable entries.
static void initClassVTable(ClassMetadata *self) {
  const auto *description = self->getDescription();
  auto *classWords = reinterpret_cast<void **>(self);

  if (description->hasVTable()) {
    auto *vtable = description->getVTableDescriptor();
    auto vtableOffset = vtable->getVTableOffset(description);
    auto descriptors = description->getMethodDescriptors();
    for (unsigned i = 0, e = vtable->VTableSize; i < e; ++i) {
      auto &methodDescription = descriptors[i];
      swift_ptrauth_init_code_or_data(
          &classWords[vtableOffset + i], methodDescription.Impl.get(),
          methodDescription.Flags.getExtraDiscriminator(),
          !methodDescription.Flags.isAsync());
    }
  }

  if (description->hasOverrideTable()) {
    auto *overrideTable = description->getOverrideTable();
    auto overrideDescriptors = description->getMethodOverrideDescriptors();

    for (unsigned i = 0, e = overrideTable->NumEntries; i < e; ++i) {
      auto &descriptor = overrideDescriptors[i];

      // Get the base class and method.
      auto *baseClass = cast_or_null<ClassDescriptor>(descriptor.Class.get());
      auto *baseMethod = descriptor.Method.get();

      // If the base method is null, it's an unavailable weak-linked
      // symbol.
      if (baseClass == nullptr || baseMethod == nullptr)
        continue;

      // Calculate the base method's vtable offset from the
      // base method descriptor. The offset will be relative
      // to the base class's vtable start offset.
      auto baseClassMethods = baseClass->getMethodDescriptors();

      // If the method descriptor doesn't land within the bounds of the
      // method table, abort.
      if (baseMethod < baseClassMethods.begin() ||
          baseMethod >= baseClassMethods.end()) {
        fatalError(0, "resilient vtable at %p contains out-of-bounds "
                   "method descriptor %p\n",
                   overrideTable, baseMethod);
      }

      // Install the method override in our vtable.
      auto baseVTable = baseClass->getVTableDescriptor();
      auto offset = (baseVTable->getVTableOffset(baseClass) +
                     (baseMethod - baseClassMethods.data()));

      swift_ptrauth_init_code_or_data(&classWords[offset],
                                      descriptor.Impl.get(),
                                      baseMethod->Flags.getExtraDiscriminator(),
                                      !baseMethod->Flags.isAsync());
    }
  }
}

其内部是通过for循环编码,然后offset+index偏移,然后获取method,将其存入到偏移后的内存中,从这里可以印证函数是连续存放的

对于class中函数来说,类的方法调度是通过V-Table,其本质就是一个连续的内存空间(数组结构)

问题:如果更改方法声明的位置呢?例如extension中的函数,此时的函数调度方式还是函数表调度吗?

通过以下代码验证

  • 定义一个JXTeacher的extension
extension JXTeacher {
    func teach4() {print("teach4")}
}
  • 在定义一个子类JXStudent继承自JXTeacher,查看SIL中的V-Table
class JXStudent: JXTeacher{
}
  • 查看SIL文件,发现子类只继承了class中定义的函数,即函数表中的函数
sil_vtable JXTeacher {
  #JXTeacher.teach: (JXTeacher) -> () -> () : @main.JXTeacher.teach() -> ()	// JXTeacher.teach()
  #JXTeacher.teach2: (JXTeacher) -> () -> () : @main.JXTeacher.teach2() -> ()	// JXTeacher.teach2()
  #JXTeacher.teach3: (JXTeacher) -> () -> () : @main.JXTeacher.teach3() -> ()	// JXTeacher.teach3()
  #JXTeacher.init!allocator: (JXTeacher.Type) -> () -> JXTeacher : @main.JXTeacher.__allocating_init() -> main.JXTeacher	// JXTeacher.__allocating_init()
  #JXTeacher.deinit!deallocator: @main.JXTeacher.__deallocating_deinit	// JXTeacher.__deallocating_deinit
}

sil_vtable JXStudent {
  #JXTeacher.teach: (JXTeacher) -> () -> () : @main.JXTeacher.teach() -> () [inherited]	// JXTeacher.teach()
  #JXTeacher.teach2: (JXTeacher) -> () -> () : @main.JXTeacher.teach2() -> () [inherited]	// JXTeacher.teach2()
  #JXTeacher.teach3: (JXTeacher) -> () -> () : @main.JXTeacher.teach3() -> () [inherited]	// JXTeacher.teach3()
  #JXTeacher.init!allocator: (JXTeacher.Type) -> () -> JXTeacher : @main.JXStudent.__allocating_init() -> main.JXStudent [override]	// JXStudent.__allocating_init()
  #JXStudent.deinit!deallocator: @main.JXStudent.__deallocating_deinit	// JXStudent.__deallocating_deinit
}

其原因是因为子类将父类的函数表全部继承了,如果此时子类增加函数,会继续在连续的地址中插入,假设extension函数也是在函数表中,则意味着子类也有,但是子类无法并没有相关的指针记录函数 是父类方法 还是 子类方法,所以不知道方法该从哪里插入,导致extension中的函数无法安全的放入子类中。所以在这里可以侧面证明extension中的方法是直接调用的,且只属于类,子类是无法继承的

开发注意点:

  • 继承方法和属性,不能写extension中。
  • 而extension中创建的函数,一定是只属于自己类,但是其子类也有其访问权限,只是不能继承和重写,如下所示
class JXTeacher{
    func teach2(){print("teach2")}
    func study() {}
    @objc deinit{}
    init(){}
}

extension JXTeacher{
    var age: Int{
        get{
            return 18
        }
    }
    func teach(){
        print("teach")
    }
}

class JXMiddleTeacher: JXTeacher{
    override func study() {
        print("JXMiddleTeacher study")
    }
}

var t = JXMiddleTeacher()
//子类有父类extension中方法的访问权限,只是不能继承和重写
t.teach()
t.study()
print(t.age)

<!--运行结果-->
teach
JXMiddleTeacher study
18

final、@objc、dynamic修饰函数

final 修饰

  • final 修饰的方法是 直接调度的,可以通过SIL验证 + 断点验证
class JXTeacher {
    final func teach(){ print("teach") }
    func teach2(){ print("teach2") }
    func teach3(){ print("teach3") }
    func teach4(){ print("teach4") }
    @objc deinit{}
    init(){}
}

class ViewController: UIViewController {

    override func viewDidLoad() { //断点
        super.viewDidLoad()
        let t1 = JXTeacher()
        t1.teach()
        t1.teach2()
        t1.teach3()
        t1.teach4()
    }
}

SIL验证

sil_vtable JXTeacher {
  #JXTeacher.teach2: (JXTeacher) -> () -> () : @main.JXTeacher.teach2() -> ()	// JXTeacher.teach2()
  #JXTeacher.teach3: (JXTeacher) -> () -> () : @main.JXTeacher.teach3() -> ()	// JXTeacher.teach3()
  #JXTeacher.teach4: (JXTeacher) -> () -> () : @main.JXTeacher.teach4() -> ()	// JXTeacher.teach4()
  #JXTeacher.init!allocator: (JXTeacher.Type) -> () -> JXTeacher : @main.JXTeacher.__allocating_init() -> main.JXTeacher	// JXTeacher.__allocating_init()
  #JXTeacher.deinit!deallocator: @main.JXTeacher.__deallocating_deinit	// JXTeacher.__deallocating_deinit
}

308

@objc 修饰

使用@objc关键字是将swift中的方法暴露给OC

// JXTeacher.swift
class JXTeacher: NSObject {
    @objc func teach(){ print("teach") }
    func teach2(){ print("teach2") }
    func teach3(){ print("teach3") }
    @objc func teach4(){
        print("teach4")
        let person: JXPerson = JXPerson()
        person.teach5();
    }
    @objc deinit{}
    override init(){}
}

//  JXPerson.h
@interface JXPerson : NSObject
- (void)teach5;
@end

// JXPerson.m
#import "JXPerson.h"
@implementation JXPerson
- (void)teach5 {
    NSLog(@"%s", __func__);
}
@end

// OCDemo-Bridging-Header.h
#import "JXPerson.h"


<!--OC调用-->
#import "OCDemo-Swift.h"

- (void)viewDidLoad {
    [super viewDidLoad];
    JXTeacher *t = [[JXTeacher alloc] init];
    [t teach];
}

输出:
teach
teach4
2021-11-25 17:04:07.519649+0800 OCDemo[79235:1700332] -[JXPerson teach5]

查看SIL文件发现被@objc修饰的函数声明有两个:swift + OC(内部调用的swift中的teach函数)

swiftc -emit-sil JXTeacher.swift | xcrun swift-demangle >> ./jxteacher.sil && open jxteacher.sil
// JXTeacher.teach() swift中的函数
sil hidden @JXTeacher.JXTeacher.teach() -> () : $@convention(method) (@guaranteed JXTeacher) -> () {
// %0 "self"                                      // user: %1
bb0(%0 : $JXTeacher):
......
}

// @objc JXTeacher.teach()  OC中的函数,实际内部调用swift中的函数
sil hidden [thunk] @@objc JXTeacher.JXTeacher.teach() -> () : $@convention(objc_method) (JXTeacher) -> () {
// %0                                             // users: %4, %3, %1
bb0(%0 : $JXTeacher):
  strong_retain %0 : $JXTeacher                   // id: %1
  // function_ref JXTeacher.teach()
  %2 = function_ref @JXTeacher.JXTeacher.teach() -> () : $@convention(method) (@guaranteed JXTeacher) -> () // user: %3
  %3 = apply %2(%0) : $@convention(method) (@guaranteed JXTeacher) -> () // user: %5
  strong_release %0 : $JXTeacher                  // id: %4
  return %3 : $()                                 // id: %5
} // end sil function '@objc JXTeacher.JXTeacher.teach() -> ()'

即在SIL文件中生成了两个方法

  • swift原有的函数
  • @objc标记暴露给OC来使用的函数: 内部调用swift的

dynamic 修饰

以下面代码为例,查看dynamic修饰的函数的调度方式

class JXTeacher: NSObject {
    dynamic func teach(){ print("teach") }
    func teach2(){ print("teach2") }
    func teach3(){ print("teach3") }
    func teach4(){ print("teach4") }
    @objc deinit{}
    override init(){}
}

其中teach函数的调度还是 函数表调度,可以通过断点调试验证,使用dynamic的意思是可以动态修改,意味着当类继承自NSObject时,可以使用method-swizzling

@objc + dynamic

class JXTeacher: NSObject {
    @objc dynamic func teach(){ print("teach") }
    func teach2(){ print("teach2") }
    func teach3(){ print("teach3") }
    func teach4(){ print("teach4") }
    @objc deinit{}
    override init(){}
}

通过断点调试,走的是objc_msgSend流程,即 动态消息转发

OCDemo`-[ViewController viewDidLoad]:
    0x1023784c4 <+0>:   sub    sp, sp, #0x40             ; =0x40 
    0x1023784c8 <+4>:   stp    x29, x30, [sp, #0x30]
    0x1023784cc <+8>:   add    x29, sp, #0x30            ; =0x30 
    0x1023784d0 <+12>:  stur   x0, [x29, #-0x8]
    0x1023784d4 <+16>:  stur   x1, [x29, #-0x10]
    0x1023784d8 <+20>:  ldur   x8, [x29, #-0x8]
    0x1023784dc <+24>:  add    x0, sp, #0x10             ; =0x10 
    0x1023784e0 <+28>:  str    x8, [sp, #0x10]
    0x1023784e4 <+32>:  adrp   x8, 9
    0x1023784e8 <+36>:  ldr    x8, [x8, #0x5e8]
    0x1023784ec <+40>:  str    x8, [sp, #0x18]
    0x1023784f0 <+44>:  adrp   x8, 9
    0x1023784f4 <+48>:  ldr    x1, [x8, #0x5a0]
    0x1023784f8 <+52>:  bl     0x10237a0fc               ; symbol stub for: objc_msgSendSuper2
    0x1023784fc <+56>:  adrp   x8, 9
    0x102378500 <+60>:  ldr    x0, [x8, #0x5d0]
    0x102378504 <+64>:  bl     0x10237a0a8               ; symbol stub for: objc_alloc_init
    0x102378508 <+68>:  add    x8, sp, #0x8              ; =0x8 
    0x10237850c <+72>:  str    x8, [sp]
    0x102378510 <+76>:  str    x0, [sp, #0x8]
->  0x102378514 <+80>:  ldr    x0, [sp, #0x8]
    0x102378518 <+84>:  adrp   x8, 9
    0x10237851c <+88>:  ldr    x1, [x8, #0x5a8]
    0x102378520 <+92>:  bl     0x10237a0f0               ; symbol stub for: objc_msgSend
    0x102378524 <+96>:  ldr    x0, [sp]
    0x102378528 <+100>: mov    x1, #0x0
    0x10237852c <+104>: bl     0x10237a144               ; symbol stub for: objc_storeStrong
    0x102378530 <+108>: ldp    x29, x30, [sp, #0x30]
    0x102378534 <+112>: add    sp, sp, #0x40             ; =0x40 
    0x102378538 <+116>: ret  

场景:swift中实现方法交换

在swift中的需要交换的函数前,使用dynamic修饰,然后通过:@_dynamicReplacement(for: 函数符号)进行交换,如下所示

class JXTeacher: NSObject {
    dynamic func teach(){ print("teach") }
    func teach2(){ print("teach2") }
    func teach3(){ print("teach3") }
    func teach4(){ print("teach4") }
    @objc deinit{}
    override init(){}
}

extension JXTeacher {
    @_dynamicReplacement(for: teach)
    func teach5() {
        print("teach5")
    }
}

let t = JXTeacher()
t.teach()
t.teach2()
t.teach3()
t.teach4()
print("end")

输出:
teach5
teach2
teach3
teach4
end
  • 如果teach没有实现 / 如果去掉dynamic修饰符,会报错

总结

  • struct是值类型,其中函数的调度属于直接调用地址,即静态调度
  • class是引用类型,其中函数的调度是通过V-Table函数表来进行调度的,即动态调度
  • extension中的函数调度方式是直接调度
  • final修饰的函数调度方式是直接调度
  • @objc修饰的函数调度方式是函数表调度,如果OC中需要使用,class还必须继承NSObject
  • dynamic修饰的函数的调度方式是函数表调度,使函数具有动态性
  • @objc + dynamic 组合修饰的函数调度,是执行的是objc_msgSend流程,即 动态消息转发

补充

内存插件libfooplugin

主要补充内存插件libfooplugin.dylib安装及使用

方法一 在通过lldb调试的时候,

直接输入 plugin load libfooplugin.dylib路径
如: plugin load /Users/jxwbjmac0003/Documents/M1_libfooplugin/libLGCatAddress.dylib

(lldb) cat address 0x0000000100008240

方法二 在跟目下创建.lldbinit文件

vim /.lldbinit

然后输入 plugin load libfooplugin.dylib路径

(lldb) cat address 地址

方法三 在任意目录创建文件,然后在文件中输入

plugin load libfooplugin.dylib路径

打开你需要使用libfooplugin的工程,然后依次点击

Product -> Scheme -> Edit Scheme

1010