Swift进阶-类与结构体

Swift进阶-类与结构体
Swift-函数派发
Swift进阶-属性
Swift进阶-指针
Swift进阶-内存管理
Swift进阶-TargetClassMetadata和TargetStructMetadata数据结构源码分析
Swift进阶-Mirror解析
Swift进阶-闭包
Swift进阶-协议
Swift进阶-泛型
Swift进阶-String源码解析
Swift进阶-Array源码解析

类与结构体的异同点

// 定义一个类或结构体
class/struct Person {
    var name: String
    var age: Int
    
    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
  
    deinit { // class
    }
}
相同点:
  • 定义存储的属性
  • 定义方法
  • 定义下标,使用下标语法(subscript)提供对其值的访问
  • 定义初始化器(init)
  • 使用extension来拓展功能
  • 遵循Protocol来提供某种功能
不同点:
  • class有继承的特性,struct没有继承特性
  • 类型转换使您能在运行时检查和解释class的实例对象的类型
  • class有析构函数用来释放其占用的资源
  • 引用计数允许对一个class实例有多个引用
  • class是引用类型,struct是值类型
  • 一般情况下,class存储在堆区;struct存储在栈区
引用类型
class Person {
    var name: String
    init(name: String) {
        self.name = name
    }
}
image.png

这里我们借助两个指令来查看当前变量的内存结构:

p / po 的区别在于:
po 只会输出对应的值;
p 则会返回值的类型以及命令结果的引用名。
x/8g (16进制地址): 读取内存中的值(8g: 8字节格式输出)

引用类型特征图

看到p1p2变量都引用了同一个Person的实例地址。
所以引用类型存储的是实例内存地址的引用。

p1p2两个变量本身的地址是不一样的,而他俩内存是挨着的,刚好相差8个字节,这两变量的内存地址存储的就是当前实例对象的内存地址:

image.png
值类型
struct Person {
    var name: String
    init(name: String) {
        self.name = name
    }
}
image.png

看到输出的内容就和class的不一样了。
值类型存储的就是具体实例(或者说具体的值)。

值类型特征图

引用类型 和 值类型 的存储位置

一般情况下,值类型存储在栈上,引用类型存储在堆上。
先来了解一下我们的内存模型:

内存分区

我们把系统分配给app的可操作性的内存空间人为地分成五大区:指令区、常量区、全局(静态)区、堆区、栈区

栈区(stack): 局部变量和函数运行过程中的上下文
堆区(Heap): 存储所有对象
Global: 存储全局变量;常量;代码区

Segment & Section: Mach-O 文件有多个段( Segment ),每个段有不同的功能。然后每 个段又分为很多小的 Section

name value
TEXT.text 机器码
TEXT.cstring 硬编码的字符串
TEXT.const 初始化过的常量
DATA.data 初始化过的可变的(静态/全局)数据
DATA.const 没有初始化过的常量
DATA.bss 没有初始化的(静态/全局)变量
DATA.common 没有初始化过的符号声明
LLDB调试内存分布
frame variable -L xxx

结构体的内存分布:

struct Person {
    var age = 18
    var name = "a"
}
frame variable -L 指令调试

struct是值类型,所以存放第一个首地址是指向age的,并且很明显struct存储在栈区,因为地址是连续的,age到name刚好中间相差8个字节。

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

如果struct的成员包含了一个引用类型呢?

struct Person {
    var age: Int = 18
    var name: String = "a"
    var t = Teacher()
}

class Teacher {
    var age = 10
    var name = "tearcher"
}
image.png

age、name和t在栈区这没有问题,而Teacher开辟的地址0x0000600000ec1080在堆区。

类的内存分布:

class Person {
    var age = 18
    var name = "a"
}
frame variable -L 指令调试

我们知道p1存储在方法栈上,Person实例的地址是0x00006000024a9b90,仍然要在堆区中开辟内存空间。

当前类的实例在内存中分布示意图

类在内存分配的时候,会在堆空间上找到合适的内存区域,找到和内存区域后就会把这个内存地址拷贝到堆,然后栈区的内存地址指向这个当前的堆区。
离开作用域的时候,势必要回收内存空间,这个时候先查找类的内存空间,并把内存块归重新插入到堆空间中,栈区地址不再指向堆区。

对于引用类型来说,创建和销毁都必须有一个查找的过程,会有时间和速度上的损耗,并且对于引用计数的计算也是消耗性能的

举例:
一个聊天室创建一个聊天气泡(makeBalloon),通过Color、Orientation、Tail来作为字符串的key从缓存中取出气泡img

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) {
    let key = "\(color):\(orientation):\(tail)"
    if let image = cache[key] {
        return image
    }
    ...
}

上面这段代码虽然我们做了image的缓存,但是key是一个字符串(是一个表型为值类型的引用类型)存储在堆区,在每次调用makeBalloon时候,仍然要从堆空间中不断地分配/销毁内存。

优化后的代码:

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

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

func makeBalloon(_ ballon: Ballon) {
    if let image = cache[ballon] {
        return image
    }
    ...
}

在我们实际开发当中尽可能地使用struct代替class,如果诸如继承这些关系,那可以选用class。

初始化器

结构体不需要声明初始化器,系统默认自动提供成员变量初始化器。

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

类在声明的时候必须给予一个指定初始化器,同时我们也可以提供便捷初始化器可失败初始化器必要初始化器

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(age, "名称")
     }
}

class Son: Person {
      var subName: String
      init(_ subName: String) {
          self.subName = subName
          super.init(18, "wj")
      }
}

可失败初始化器:

class Person {
     var  age: Int
     var name: String 
     init?(_ age: Int, _ name: String) {
          if age < 18 {return nil}
          self.age = age
          self.name = name
     }

     convenience init?(_ age: Int) {
        self.init(age, "名称")
     }
}

必要初始化器(继承下去的子类必须实现该初始化器):

class Person {
     var  age: Int
     var name: String 
     required init(_ age: Int, _ name: String) {
          self.age = age
          self.name = name
     }

     convenience init(_ age: Int) {
        self.init(age, "名称")
     }
}

类的生命周期

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

image.png

OC 通过 clang 编译器编译成 IR,然后再生成可执行文件 .o(这里也就是我们的机器码);
Swift 则是通过 Swift 编译器编译成 IR,然后在生成可执行文件。

Swift编译过程
// 语法分析分析输出AST(抽象语法树)
swiftc main.swift -dump-parse 

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

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

// 生成swift中间体语言(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文件 (x86、arm64....)
swiftc -o main.o main.swift

可以通过上面的命令自行尝试编译过程。

// 还原类名
xcrun swift-demangle xxx   // xxx是经过混写规则的类名
class Person{
    var age = 18
    var name = "LGMan"
}

var p = Person()

Person()创建的时候打个断点调试,Debug->Debug Workflow -> Always Show Disassembly

SwiftTest.Person.__allocating_init()

Person是纯swift类,在创建实例的时候,会调用SwiftTest.Person.__allocating_init(); 底层会调用swift_allocObjectSwiftTest.Person.init()

Person是继承NSObject的类,在创建实例的时候,会调用SwiftTest.Person.__allocating_init();底层会调用objc_allocWithZoneobjc_msgSend 发送init消息

来看swift源码 swift_allocObject 底层调用分配内存

全局搜索swift_allocObject,在HeapObject.cpp文件找到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_slowAlloc函数返回了一个 HeapObject 泛型对象,全局搜索这个函数,来到Heap.cpp里的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__) && SWIFT_STDLIB_HAS_DARWIN_LIBMALLOC
    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;
}

看到这行熟悉代码 p = malloc(size); 进行了内存分配,最后并返回了p

Swift 对象内存分配__allocating_init -> swift_allocObject -> _swift_allocObject_ -> swift_slowAlloc -> malloc

再回来 _swift_allocObject_ 函数看接下来的逻辑

_swift_allocObject_

对object进行初始化,可以来看看HeapObject的结构:

  constexpr HeapObject(HeapMetadata const *newMetadata) 
    : metadata(newMetadata)
    , refCounts(InlineRefCounts::Initialized)
  { }

Swift 对象的内存结构: HeapObject (OC objc_object) ,有两个属性各占8字节: MetadataRefCount,默认占用 16 字节大小。

RefCount是一个64位的引用计数,那么HeapMetadata到底是什么呢?

源码分析class的数据结构

HeapMetadata

HeapMetadata

HeapMetadata 起了别名 TargetHeapMetadata

Metadata.h

所以swift类的 HeapMetadata / TargetHeapMetadata 是通过 kind进行初始化的。

MetadataKind的定义:

name value
Class 0x0
Struct 0x200
Enum 0x201
Optional 0x202
ForeignClass 0x203
Opaque 0x300
Tuple 0x301
Function 0x302
Existential 0x303
Metatype 0x304
ObjCClassWrapper 0x305
ExistentialMetatype 0x306
HeapLocalVariable 0x400
HeapGenericLocalVariable 0x500
ErrorObject 0x501
LastEnumerated 0x7FF

TargetMetadata里面有一个函数:

image.png

这里的this就是TargetMetadata实例,可以看成objc里的isa,而
TargetClassMetadata是swift所有类型元类的最终基类(OC objc_class)

/// The structure of all class metadata.  This structure is embedded
/// directly within the class's heap metadata structure and therefore
/// cannot be extended without an ABI break.
///
/// Note that the layout of this type is compatible with the layout of
/// an Objective-C class.
template <typename Runtime>
struct TargetClassMetadata : public TargetAnyClassMetadata<Runtime> {
  using StoredPointer = typename Runtime::StoredPointer;
  using StoredSize = typename Runtime::StoredSize;

  TargetClassMetadata() = default;
  constexpr TargetClassMetadata(const TargetAnyClassMetadata<Runtime> &base,
             ClassFlags flags,
             ClassIVarDestroyer *ivarDestroyer,
             StoredPointer size, StoredPointer addressPoint,
             StoredPointer alignMask,
             StoredPointer classSize, StoredPointer classAddressPoint)
    : TargetAnyClassMetadata<Runtime>(base),
      Flags(flags), InstanceAddressPoint(addressPoint),
      InstanceSize(size), InstanceAlignMask(alignMask),
      Reserved(0), ClassSize(classSize), ClassAddressPoint(classAddressPoint),
      Description(nullptr), IVarDestroyer(ivarDestroyer) {}

  // The remaining fields are valid only when isTypeMetadata().
  // The Objective-C runtime knows the offsets to some of these fields.
  // Be careful when accessing them.

  /// Swift-specific class flags.
  ClassFlags Flags;

  /// The address point of instances of this type.
  uint32_t InstanceAddressPoint;

  /// The required size of instances of this type.
  /// 'InstanceAddressPoint' bytes go before the address point;
  /// 'InstanceSize - InstanceAddressPoint' bytes go after it.
  uint32_t InstanceSize;

  /// The alignment mask of the address point of instances of this type.
  uint16_t InstanceAlignMask;

  /// Reserved for runtime use.
  uint16_t Reserved;

  /// The total size of the class object, including prefix and suffix
  /// extents.
  uint32_t ClassSize;

  /// The offset of the address point within the class object.
  uint32_t ClassAddressPoint;

  // Description is by far the most likely field for a client to try
  // to access directly, so we force access to go through accessors.
private:
  /// An out-of-line Swift-specific description of the type, or null
  /// if this is an artificial subclass.  We currently provide no
  /// supported mechanism for making a non-artificial subclass
  /// dynamically.
  TargetSignedPointer<Runtime, const TargetClassDescriptor<Runtime> * __ptrauth_swift_type_descriptor> Description;

public:
  /// A function for destroying instance variables, used to clean up after an
  /// early return from a constructor. If null, no clean up will be performed
  /// and all ivars must be trivial.
  TargetSignedPointer<Runtime, ClassIVarDestroyer * __ptrauth_swift_heap_object_destructor> IVarDestroyer;

  // After this come the class members, laid out as follows:
  //   - class members for the superclass (recursively)
  //   - metadata reference for the parent, if applicable
  //   - generic parameters for this class
  //   - class variables (if we choose to support these)
  //   - "tabulated" virtual methods

  using TargetAnyClassMetadata<Runtime>::isTypeMetadata;

  ConstTargetMetadataPointer<Runtime, TargetClassDescriptor>
  getDescription() const {
    assert(isTypeMetadata());
    return Description;
  }

  typename Runtime::StoredSignedPointer
  getDescriptionAsSignedPointer() const {
    assert(isTypeMetadata());
    return Description;
  }

  void setDescription(const TargetClassDescriptor<Runtime> *description) {
    Description = description;
  }

  // [NOTE: Dynamic-subclass-KVO]
  //
  // Using Objective-C runtime, KVO can modify object behavior without needing
  // to modify the object's code. This is done by dynamically creating an
  // artificial subclass of the the object's type.
  //
  // The isa pointer of the observed object is swapped out to point to
  // the artificial subclass, which has the following properties:
  // - Setters for observed keys are overridden to additionally post
  // notifications.
  // - The `-class` method is overridden to return the original class type
  // instead of the artificial subclass type.
  //
  // For more details, see:
  // https://www.mikeash.com/pyblog/friday-qa-2009-01-23.html

  /// Is this class an artificial subclass, such as one dynamically
  /// created for various dynamic purposes like KVO?
  /// See [NOTE: Dynamic-subclass-KVO]
  bool isArtificialSubclass() const {
    assert(isTypeMetadata());
    return Description == nullptr;
  }
  void setArtificialSubclass() {
    assert(isTypeMetadata());
    Description = nullptr;
  }

  ClassFlags getFlags() const {
    assert(isTypeMetadata());
    return Flags;
  }
  void setFlags(ClassFlags flags) {
    assert(isTypeMetadata());
    Flags = flags;
  }

  StoredSize getInstanceSize() const {
    assert(isTypeMetadata());
    return InstanceSize;
  }
  void setInstanceSize(StoredSize size) {
    assert(isTypeMetadata());
    InstanceSize = size;
  }

  StoredPointer getInstanceAddressPoint() const {
    assert(isTypeMetadata());
    return InstanceAddressPoint;
  }
  void setInstanceAddressPoint(StoredSize size) {
    assert(isTypeMetadata());
    InstanceAddressPoint = size;
  }

  StoredPointer getInstanceAlignMask() const {
    assert(isTypeMetadata());
    return InstanceAlignMask;
  }
  void setInstanceAlignMask(StoredSize mask) {
    assert(isTypeMetadata());
    InstanceAlignMask = mask;
  }

  StoredPointer getClassSize() const {
    assert(isTypeMetadata());
    return ClassSize;
  }
  void setClassSize(StoredSize size) {
    assert(isTypeMetadata());
    ClassSize = size;
  }

  StoredPointer getClassAddressPoint() const {
    assert(isTypeMetadata());
    return ClassAddressPoint;
  }
  void setClassAddressPoint(StoredSize offset) {
    assert(isTypeMetadata());
    ClassAddressPoint = offset;
  }

  uint16_t getRuntimeReservedData() const {
    assert(isTypeMetadata());
    return Reserved;
  }
  void setRuntimeReservedData(uint16_t data) {
    assert(isTypeMetadata());
    Reserved = data;
  }

  /// Get a pointer to the field offset vector, if present, or null.
  const StoredPointer *getFieldOffsets() const {
    assert(isTypeMetadata());
    auto offset = getDescription()->getFieldOffsetVectorOffset();
    if (offset == 0)
      return nullptr;
    auto asWords = reinterpret_cast<const void * const*>(this);
    return reinterpret_cast<const StoredPointer *>(asWords + offset);
  }

  uint32_t getSizeInWords() const {
    assert(isTypeMetadata());
    uint32_t size = getClassSize() - getClassAddressPoint();
    assert(size % sizeof(StoredPointer) == 0);
    return size / sizeof(StoredPointer);
  }

  /// Given that this class is serving as the superclass of a Swift class,
  /// return its bounds as metadata.
  ///
  /// Note that the ImmediateMembersOffset member will not be meaningful.
  TargetClassMetadataBounds<Runtime>
  getClassBoundsAsSwiftSuperclass() const {
    using Bounds = TargetClassMetadataBounds<Runtime>;

    auto rootBounds = Bounds::forSwiftRootClass();

    // If the class is not type metadata, just use the root-class bounds.
    if (!isTypeMetadata())
      return rootBounds;

    // Otherwise, pull out the bounds from the metadata.
    auto bounds = Bounds::forAddressPointAndSize(getClassAddressPoint(),
                                                 getClassSize());

    // Round the bounds up to the required dimensions.
    if (bounds.NegativeSizeInWords < rootBounds.NegativeSizeInWords)
      bounds.NegativeSizeInWords = rootBounds.NegativeSizeInWords;
    if (bounds.PositiveSizeInWords < rootBounds.PositiveSizeInWords)
      bounds.PositiveSizeInWords = rootBounds.PositiveSizeInWords;

    return bounds;
  }

#if SWIFT_OBJC_INTEROP
  /// Given a statically-emitted metadata template, this sets the correct
  /// "is Swift" bit for the current runtime. Depending on the deployment
  /// target a binary was compiled for, statically emitted metadata templates
  /// may have a different bit set from the one that this runtime canonically
  /// considers the "is Swift" bit.
  void setAsTypeMetadata() {
    // If the wrong "is Swift" bit is set, set the correct one.
    //
    // Note that the only time we should see the "new" bit set while
    // expecting the "old" one is when running a binary built for a
    // new OS on an old OS, which is not supported, however we do
    // have tests that exercise this scenario.
    auto otherSwiftBit = (3ULL - SWIFT_CLASS_IS_SWIFT_MASK);
    assert(otherSwiftBit == 1ULL || otherSwiftBit == 2ULL);

    if ((this->Data & 3) == otherSwiftBit) {
      this->Data ^= 3;
    }

    // Otherwise there should be nothing to do, since only the old "is
    // Swift" bit is used for backward-deployed runtimes.
    
    assert(isTypeMetadata());
  }
#endif

  bool isStaticallySpecializedGenericMetadata() const {
    auto *description = getDescription();
    if (!description->isGeneric())
      return false;

    return this->Flags & ClassFlags::IsStaticSpecialization;
  }

  bool isCanonicalStaticallySpecializedGenericMetadata() const {
    auto *description = getDescription();
    if (!description->isGeneric())
      return false;

    return this->Flags & ClassFlags::IsCanonicalStaticSpecialization;
  }

  static bool classof(const TargetMetadata<Runtime> *metadata) {
    return metadata->getKind() == MetadataKind::Class;
  }
};
using ClassMetadata = TargetClassMetadata<InProcess>;

TargetClassMetadata的父类TargetAnyClassMetadata就有熟悉的感觉了:

/// The portion of a class metadata object that is compatible with
/// all classes, even non-Swift ones.
template <typename Runtime>
struct TargetAnyClassMetadata : public TargetHeapMetadata<Runtime> {
  using StoredPointer = typename Runtime::StoredPointer;
  using StoredSize = typename Runtime::StoredSize;

#if SWIFT_OBJC_INTEROP
  constexpr TargetAnyClassMetadata(TargetAnyClassMetadata<Runtime> *isa,
                                   TargetClassMetadata<Runtime> *superclass)
    : TargetHeapMetadata<Runtime>(isa),
      Superclass(superclass),
      CacheData{nullptr, nullptr},
      Data(SWIFT_CLASS_IS_SWIFT_MASK) {}
#endif

  constexpr TargetAnyClassMetadata(TargetClassMetadata<Runtime> *superclass)
    : TargetHeapMetadata<Runtime>(MetadataKind::Class),
      Superclass(superclass)
#if SWIFT_OBJC_INTEROP
      , CacheData{nullptr, nullptr},
      Data(SWIFT_CLASS_IS_SWIFT_MASK)
#endif
      {}

#if SWIFT_OBJC_INTEROP
  // Allow setting the metadata kind to a class ISA on class metadata.
  using TargetMetadata<Runtime>::getClassISA;
  using TargetMetadata<Runtime>::setClassISA;
#endif

  // Note that ObjC classes do not have a metadata header.

  /// The metadata for the superclass.  This is null for the root class.
  TargetSignedPointer<Runtime, const TargetClassMetadata<Runtime> *
                                   __ptrauth_swift_objc_superclass>
      Superclass;

#if SWIFT_OBJC_INTEROP
  /// The cache data is used for certain dynamic lookups; it is owned
  /// by the runtime and generally needs to interoperate with
  /// Objective-C's use.
  TargetPointer<Runtime, void> CacheData[2];

  /// The data pointer is used for out-of-line metadata and is
  /// generally opaque, except that the compiler sets the low bit in
  /// order to indicate that this is a Swift metatype and therefore
  /// that the type metadata header is present.
  StoredSize Data;
  
  static constexpr StoredPointer offsetToData() {
    return offsetof(TargetAnyClassMetadata, Data);
  }
#endif

  /// Is this object a valid swift type metadata?  That is, can it be
  /// safely downcast to ClassMetadata?
  bool isTypeMetadata() const {
#if SWIFT_OBJC_INTEROP
    return (Data & SWIFT_CLASS_IS_SWIFT_MASK);
#else
    return true;
#endif
  }
  /// A different perspective on the same bit
  bool isPureObjC() const {
    return !isTypeMetadata();
  }
};
using AnyClassMetadata =
  TargetAnyClassMetadata<InProcess>;

using ClassIVarDestroyer =
  SWIFT_CC(swift) void(SWIFT_CONTEXT HeapObject *);

TargetAnyClassMetadata的数据结构里有我们所熟悉的 SuperclassClassISACacheDataData

这里面的数据结构就是我们的最终答案
经过源码分析我们不难得出 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
 }

异变方法

swift中class和struct都能定义方法func。但是区别在于:
默认情况下,值类型的属性不能被自身的实例方法修改的
来看看下面这个案例:

值类型改变自身错误演示

此时self就是结构体自己,指代x, y。当调用moveBy方法时候,就相当于p在修改自己,此时是不被允许的。

改变自身的实例方法前面需要添加mutating修饰,此时才能编译成功:

struct Point {
    var x = 0.0, y = 0.0  
    mutating func moveBy(x: Double, y: Double) {
        self.x += x
        self.y += y
    }
}

一个用mutating修饰,一个没有的情况,编译成swift中间代码 sil 来看一下:

struct Point {
    var x = 0.0, y = 0.0
    
    func test() {
        let tmp = self.x
    }
    
    mutating func moveBy(x: Double, y: Double) {
        self.x += x
        self.y += y
    }
}

上面已有编译命令,自行使用。下面这个是输出sil的Point结构体:

Point结构体 sil
sil的Point的关注的方法

swift中函数的参数默认在最后是传递self的,而objective-c是在方法列表前面默认传递selfcmd

找到testmoveBy方法,可以看出moveBy方法前加了mutating修饰其参数后面加了个@inout

SIL 文档的解释 @inout
An @inout parameter is indirect. The address must be of an initialized object.(当前参数 类型是间接的,传递的是已经初始化过的地址

也就是说moveBy的默认参数 @inout Point 其实是一个地址,而test的默认参数就是一个结构体的值。

再来关注这两句代码
test:

  debug_value %0 : $Point, let, name "self", argno 1 // id: %1

相当于是伪代码:let self = Point,let在swift中是不可修改的,并且self取的是一个值。

moveBy:

  debug_value_addr %2 : $*Point, var, name "self", argno 3 // id: %5

相当于是伪代码:var self = &Point,var在swift中是可修改的,并且self取的地址。

区别举例:

区别举例

值类型的实例方法前面添加了mutating关键字,其默认参数会增加一个@inout,而这个@inout修饰的参数相当于传递一个地址。

案例一:

var age = 10

func modify(_ age: inout Int) {
    var tmp = age 
    tmp += 1
}

modify(&age)
print(age)  // 10

我们传递了age地址进去了,此时的age打印的还是10,为什么?

方法体里的age是一个值,这个值取的是外部变量age的地址的值。所以 var tmp = age 是值类型的赋值,并不会外部变量age那个地址存储的值。

编译后的sil

伪代码:
var age = &age
var tmp = (withUnsafePoint(to: &age) {return $0}).pointee

案例二:

func modify(_ age: inout Int) {
    age += 1
}

modify(&age)
print(age) // 11

此时age += 1就变得好使了,age是11,因为内部age取的是外部age的地址,可以改变地址的值。

方法调度


类的方法调度

objective-c的方法调度是以消息发送的方式。swift的方法调度是以什么方式?

首先我们来了解一下常见汇编指令,然后再汇编下调试方法调度的过程。

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

add: 将某一寄存器的值和另一寄存器的值 相加 并将结果保存在另一寄存器中,如:
add x0, x1, x2 // 将寄存器x1和x2的值相加后,保存到x0中

sub: 将某一寄存器的值和另一寄存器的值 相减 并将结果保存在另一寄存器中:
sub x0, x1, x2 // 将寄存器x1和x2的 值相减后,保存到x0中

and: 将某一寄存器的值和另一寄存器的值 按位与 并将结果保存到另一寄存器中,如:
and x0, x0, #0x1  // 将寄存器x0的值和常量1 按位 与 之后保存到寄存器x0中

orr: 将某一寄存器的值和另一寄存器的值 按位或 并将结果保存到另一寄存器中,如:
orr x0, x0, #0x1 // 将寄存器x0的值和常量1 按位 或 之后保存到集成器x0中

str : 将寄存器中的值写入到内存中,如:
str x0, [x0, x8]  // 将寄存器x0的值保存到栈内存 [x0 + x8]处

ldr: 将内存中的值读取到寄存器中,如:
ldr x0, [x1, x2] 将寄存器x1和x2的值相加作为地址,取该内存地址的值,放入寄存器x0中

cbz: 和 0 比较,如果结果为零就转移(只能跳到后面的指令)
cbnz: 和非 0 比较,如果结果非零就转移(只能跳到后面的指令)
cmp: 比较指令
blr: (branch)跳转到某地址(无返回)
bl: 跳转到某地址(有返回)
ret: 子程序(函数调用)返回指令,返回地址已默认保存在寄存器 lr (x30) 中

小tip:在看方法调用的时候,关注bl和blr。

新建一个工程,在ViewController.swift

// ViewController.swift
import UIKit
class Teacher {
    func teach() {
        print("teach")
    }
}

class ViewController: UIViewController{
    override func viewDidLoad() {
        let t = LGTeacher()
        t.teach()
    }
}

设置汇编调试,然后在真机上运行代码(arm64的汇编):

设置汇编调试

断点打在 t.teach() 来看看汇编

汇编

__allocating_init()swift_release之间的 blr x8 就是teach方法的调用,然后我们来看看blr x8的调用里面是啥,推测对不对:

teach汇编内部调用

那么swift在方法调用的时候是怎么调用的呢,我这里给Teacher扩充两个方法:

class Teacher{
    func teach() {
        print("teach")
    }
    func teach1(){
        print("teach1")
    }
    func teach2(){
        print("teach2")
    }
}

在viewDidLoad都去调用运行,看汇编找到这三个方法调用

image.png
image.png

可以看到三个函数的内存是连续的,并且都相差8个字节。
分析第一个teach:

分析第一个teach

__allocating_init 的返回值放在x0寄存器里,它现在存的是实例对象

mov x20, x0  // 将x0的值赋值给x20
str x20, [sp, #0x8] // #0x8入栈,将x20的值保存到栈内存
str x20, [sp, #0x10] // #0x10入栈,将x20的值保存到栈内存
ldr x8, [x20]  // 取x20地址的值给到 x8寄存器,这里[x20]取地址就是对象的前8个字节:metadata
ldr x8, [x8, #0x50] // 寄存器x8(metadata address)和地址#0x50的值相加,取地址存放到x8

ldr x8, [x20] 这里[x20]取地址就是对象的前8个字节:metadata
执行后 x8到底是不是metadata:

image.png

验证结果是metadata 接着再执行 ldr x8, [x8, #0x50] 相当于是 (metadata address value) + (0x50 value) = teach。最后就是执行teach了。
ps:0x50是编译的时候,系统就确定了的。
而三个teach函数在方法栈的内存中是连续内存空间,并且刚好相差了8个字节(函数指针的大小):0x50、0x58、0x60

所以teach方法的调用过程:找到Metadata,确定函数地址(metadata + 偏移量),执行函数。

验证函数表调度

上面可以看出,swift其中的一种方法调用方式:函数表调度
把上面那个ViewController.swift编译成sil文件,打开并拖拽文件最后:

ViewController.sil

这个sil_vtable就是class自己的函数表。不相信?好吧,继续验证。
上面分析出Metadata的数据结构是这样的:

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
 }

其中需要关注typeDescriptor,不管是class/struct/enum都有自己的Descriptor,它就是对类的一个详细描述。

找到swift源码 TargetClassMetadata

image.png

找到 Description 成员变量

image.png

TargetClassDescriptor就是上面说的描述,经过分析得出其数据结构:

struct TargetClassDescriptor{ 
      var flags: UInt32 
      var parent: UInt32 
      var name: Int32   // class/struct/enum 的名称
      var accessFunctionPointer: Int32 
      var fieldDescriptor: Int32 
      var superClassType: Int32 
      var metadataNegativeSizeInWords: UInt32 
      var metadataPositiveSizeInWords: UInt32 
      var numImmediateMembers: UInt32 
      var numFields: UInt32 
      var fieldOffsetVectorOffset: UInt32 
      var Offset: UInt32 
      // var size: UInt32 
      // V-Table  (methods) 
}

TargetClassDescriptor本身的结构是没有 V-Table 的,进而我从源码里面找推测出来的,下面就开始推测流程:
继续找全局搜TargetClassDescriptor呗:

using ClassDescriptor = TargetClassDescriptor<InProcess>;

再全局搜这个别名ClassDescriptor,定位到一个类描述生成器里面:

ClassContextDescriptorBuilder

ClassContextDescriptorBuilder这个类是用来创建当前的Matedata和Descriptor用的。然后找到layout函数:

layout

首先来看看 super::layout() 做了啥:

super::layout()

这里的各种add是不是与上面的TargetClassDescriptor数据结构有点类似了,这个layout就是在创建Descriptor进行赋值操作!

再回来看看ClassContextDescriptorBuilderlayout函数:
addVTable(); 添加虚函数表

addVTable();

addOverrideTable(); 添加重载虚函数表

addOverrideTable();

此时此刻在源码中剖析的TargetClassDescriptor数据结构里有V-Table也只是猜测,接下来我从Match-O文件进行验证。

Mach-O介绍

Mach-O 其实是Mach Object文件格式的缩写,是 mac 以及 iOS 上可执行文件的格式。常见的 .o,.a .dylib Framework,dyld .dsym

Mach-O文件格式
  • 文件头Header,表明该文件是 Mach-O 格式,指定目标架构,还有一些其他的文件属性信息,文件头信息影响后续的文件结构安排。
  • Load commands是一张包含很多内容的表。内容包括区域的位置、符号表、动态符号表 等。
Load commands
  • Data 区主要就是负责代码和数据记录的。Mach-O 是以 Segment 这种结构来组织数据的,一个 Segment 可以包含 0 个或多个Section。根据 Segment 是映射的哪一个 Load CommandSegmentsection 就可以被解读为是是代码,常量或者一些其他的数据类型。在装载在内存中时,也是根据 Segment 做内存映射的。

拿到Mach-O文件的步骤:
1.编译工程,找到Products目录里

image.png

2.找到应用程序,右键显示包内容,找到可执行文件exec

image.png

3.打开软件 MachOView 将可执行文件拖拽进去后

MachOView
从Match-O验证TargetClassDescriptor结构里有V-Table

案例代码 ViewController.swift:

class Teacher{
    func teach() {
        print("teach")
    }
    func teach1(){
        print("teach1")
    }
    func teach2(){
        print("teach2")
    }
}

class ViewController: UIViewController{
    override func viewDidLoad() {
        let t = Teacher()
        t.teach()
        t.teach1()
        t.teach2()
        //metadata + offset
    }
}

编译后把可执行文件拖拽到MachOView

image.png

在Mach-O的data区里的__TEXT,__swift5_types就是存放 所有的struct/enum/类的Descriptor的地址信息;以每4个字节来做区分。第一个4字节就是Teacher的Descriptor

Teacher的Descriptor在mach-o上的偏移量

掏出计算器,所以Teacher的Descriptor在mach-o上的地址:

0xFFFFFBF0 + 0xBC58 = 0x10000B848 // Descriptor

而0x100000000是Mach-O文件叫虚拟内存的基地址,在Mach-O中也能找到:

虚拟内存的基地址

所以Descriptor在mach-o的data区的偏移量:

0x10000B848 - 0x100000000 = 0xB848

然后再data区找到 __TEXT,__const里边,去找0xB848的位置:

image.png

在0xB848后面开始算起就是Teacher的Descriptor的内容(到哪里结束先不关心),再回来看Descriptor的数据结构:

struct TargetClassDescriptor{ 
      var flags: UInt32 
      var parent: UInt32 
      var name: Int32   // class/struct/enum 的名称
      var accessFunctionPointer: Int32 
      var fieldDescriptor: Int32 
      var superClassType: Int32 
      var metadataNegativeSizeInWords: UInt32 
      var metadataPositiveSizeInWords: UInt32 
      var numImmediateMembers: UInt32 
      var numFields: UInt32 
      var fieldOffsetVectorOffset: UInt32 
      var Offset: UInt32 
      // var size: UInt32 
      // V-Table  (methods) 
}

可以看出它的成员已有12个,所以我们再向后偏移12个4字节,再往后的4个字节里的内容就是size

image.png

所以size后面的8个就是teach()方法的内容,再往后8个就是teach1()方法的内容,再往后8个就是teach2()的内容:

image.png

来验证一下红色画线的地方就是teach()方法的内容,而我们红色画线的开始位置是0xB87C,就是我们程序运行时在内存时的偏移量,它需要加上程序运行的基地址。

获取程序运行的基地址:

程序运行的基地址

0x000000010297c000 是我们当前的程序运行时候的基地址。
而上线红色画线的teach方法的内容在内存中的地址:

0x000000010297c000 + 0xB87C = 0x10298787C

0x10298787C就是teach()方法的内容TargetMethodDescriptor,那么方法里的内容有什么呢?
来看看swift源码里方法在内存中的数据结构

方法在内存中的数据结构

flags是标识这个方法是一个什么方法,是初始化方法还是getter还是什么方法,它占用4个字节。
另外一个是imp的指针,如果我们要找到imp指针那还需要偏移;
首先把 0x10298787C 偏移flags那4个字节

0x10298787C + 0x4 = 0x102987880

注意imp指针其实是一个相对指针,它存储的其实是offset,所以我们还需要用 0x102987880加上offset,得到的就是teach的实际的方法实现!!
那这个offset是什么呢:

image.png
0x102987880 + 0xFFFFB9D8 = 0x202983258

注意0x202983258 还需减去 Mach-O的基地址:

0x202983258 - 0x100000000 = 0x102983258

0x102983258就是teach()方法的imp在内存中的地址。
继续回到我们的工程,输出teach()的内存地址,如果他俩匹配,所以上面的猜测验证成功!

从刚才的工程运行的断点,我们进入汇编调试Alaways Show Disassembly进入到汇编,找到__allocating_init()后面的第一个blr,打印那个寄存器的地址 register read x8 一起见证时刻

image.png

所以验证了我们的猜想:
**TargetClassDescriptor数据结构里有 sizeV-Table **

来swift源码里看看V-Table是怎么创建的:

创建V-Table

通过Metadata来获取当前的描述Descriptor,把Descriptormethod加载到了对应的位置。这个vtableOffset是程序编译的时候就决定了。

swift里类的默认调度方式是函数表调度
函数表调度的实质:就是Metadata + offset

但是swift里类在extension里的派发方式是直接派发

个人理解(官方没有声明):因为当前的类已经生成的VTable,此时如果要从extension里的方法添加到VTable的话,需要通过大量的计算offset,这样会大量浪费cpu资料,没有这个必要,所以苹果自动给优化成了extension的方法调度是直接派发。


结构体的方法调度

struct Teacher{
    func teach() {
        print("teach")
    }
    func teach1(){
        print("teach1")
    }
    func teach2(){
        print("teach2")
    }
}

class ViewController: UIViewController{
    override func viewDidLoad() {
        let t = Teacher()
        t.teach()
        t.teach1()
        t.teach2()
    }
}

在 t.teach()处断点进入汇编调试,运行程序,看看和class的有什么不一样的

struct汇编调试

struct在方法调用的时候直接拿到方法地址直接调用了。

结构体的方法调用方式是直接派发

默认方法调度方式总结:

Swift默认派发
影响函数派发的方式
  • final:允许类里面的函数使用直接派发,这个修饰符会让函数失去动态性。任何函数都可以使用这个修饰符,就算是 extension 里本来就是直接派发的函数。这也会让 Objective-C 的运行时获取不到这个函数,不会生成相应的 selector。

  • dynamic:可以让类里面的函数使用消息机制派发。使用 dynamic必须导入 Foundation 框架,里面包括了 NSObject 和 Objective-C 的运行时。dynamic 可以让声明在 extension 里面的函数能够被 override。dynamic 可以用在所有 NSObject 的子类和 Swift 的原声类。

  • @objc 或 @nonobjc:都可以显式地声明了一个函数是否能被 Objective-C 的运行时捕获到。但使用 @objc 的典型例子就是给 selector 一个命名空间 @objc(abc_methodName),让这个函数可以被 Objective-C 的运行时调用。@nonobjc会改变派发的方式,可以用来禁止消息机制派发这个函数,不让这个函数注册到 Objective-C 的运行时里。我不确定这跟 final 有什么区别,因为从使用场景来说也几乎一样。我个人来说更喜欢 final,因为意图更加明显。

  • final 与 @objc同时使用:可以在标记为 final 的同时,也使用 @objc 来让函数可以使用消息机制派发。这么做的结果就是,调用函数的时候会使用直接派发,但也会在 Objective-C 的运行时里注册响应的 selector。函数可以响应 perform(selector:) 以及别的 Objective-C 特性,但在直接调用时又可以有直接派发的性能。

  • @inline:Swift 也支持 @inline,告诉编译器可以使用直接派发。有趣的是,dynamic @inline(__always) func dynamicOrDirect() {} 也可以通过编译!但这也只是告诉了编译器而已,实际上这个函数还是会使用消息机制派发。这样的写法看起来像是一个未定义的行为,应该避免这么做。

将确保有时内联函数。这是默认行为,我们无需执行任何操作. Swift 编译器可能会自动内 联函数作为优化。
always - 将确保始终内联函数。通过在函数前添加@inline(__always) 来实现此行为
never - 将确保永远不会内联函数。这可以通过在函数前添加@inline(never) 来实现。
如果函数很长并且想避免增加代码段大小,请使用@inline(never)

如果对象只在声明的文件中可见,可以用 private 或 fileprivate 进行修饰。编译器会对 privatefileprivate 对象进行检查,确保没有其他继承关系的情形下,自动打上 final 标记,进而使得 对象获得静态派发的特性
(fileprivate: 只允许在定义的源文件中访问,private : 定义的声明 中访问)

关键字影响函数派发方式

可见的都会被优化 (Visibility Will Optimize)

Swift 会尽最大能力去优化函数派发的方式. 例如, 如果你有一个函数从来没有 override, Swift 就会检车并且在可能的情况下使用直接派发. 这个优化大多数情况下都表现得很好, 但对于使用了 target / action 模式的 Cocoa 开发者就不那么友好了.

另一个需要注意的是, 如果你没有使用 dynamic 修饰的话,这个优化会默认让 KVO 失效。如果一个属性绑定了 KVO 的话,而这个属性的 getter 和 setter 会被优化为直接派发,代码依旧可以通过编译,不过动态生成的 KVO 函数就不会被触发。

class Teacher {
    dynamic func teach() {
        print("teach")
    }
}
extension Teacher {
    @_dynamicReplacement(for: teach)
   func teach1() {
        print("teach1")
    }
}

let t = Teacher()
t.teach1()    // 实际调用teach,而调用teach()还是打印teach

派发总结 (Dispatch Summary):

image.png

对于函数派发这里仅仅只做一些粗浅的总结。
如果您想要了解详细的函数派发,可以看看我之前分享的一片文章:Swift的函数派发
里面有更详细的demo演示举例

喜欢的老铁❤一个,感谢支持!

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

推荐阅读更多精彩内容

  • 一.初始类与结构体 了解类与结构体的异同点 结构体和类的主要共同点有: 定义存储值的属性 定义方法 定义下标以使用...
    刘国强阅读 417评论 0 1
  • 我的博客[https://dengfeng520.github.io/] 1、值类型和引用类型 在iOS中虚拟内存...
    小時間光阅读 666评论 0 2
  • 在面向过程的语言中,要想实现类似类的功能只能借助结构体,其实从OC源码也能看出来,类的组成本就是复杂的结构体实现的...
    如风如花不如你阅读 8,166评论 2 6
  • 1、类和结构体 1.1 基础认知 类和结构体十分相似,如: 相同点: 定义存储值的属性、方法、初始化器、以及下标以...
    Jacky_夜火阅读 413评论 0 1
  • 类和结构体对比 Swift 中类和结构体有很多共同点。共同处在于: 定义属性用于存储值 定义方法用于提供功能 定义...
    小驴拉磨阅读 125评论 0 0