Swift进阶01:类、对象、属性

第一节课:类、对象、属性

准备工具:编译过的Swift源码、Vscode、Xcode、终端

主要内容:

Swift编译简介(了解)

SIL分析(掌握)

类结构探索

Swift属性

Swift编译简介

创建一个项目,写一个类,并通过默认的初始化,创建一个实例对象赋值给t。

class HZMTeacher{
 var age: Int = 18
 var name: String = "HZM"
}

let t = HZMTeacher()

接下来主要看默认的初始化器到底做了什么操作
我们使用SIL(Swift intermediate language)来查看分析。
首先了解下什么是SIL
iOS开发语言不管是OC还是Swift后端都是通过LLVM进行编译的,如下图所示:


01.png

可以看到:
OC通过clang编译器,编译成IR,然后再生成可执行文件.o(这里也就是我们的机械码)

Swift则是通过Swift编译器编译成IR,然后再生成可执行文件。

我们再来看一下,一个swift文件的编译过程都经历了什么步骤:

02.png

swift在编译过程中使用的前端编译器是swiftc,和我们在OC中使用的Clang是有区别的
我们可以通过如下命令查看swiftc都能做什么样的事情:
swiftc -h

04.png

以上只是作为了解,扩充一下自己知识点,了解下Swift底层的原理,其实第一次看起来一脸懵,后面再结合分析代码多看看了解下就好

SIL分析(要求掌握)

首先我们简单写一段代码


SIL分析01.png

然后通过终端执行 swiftc -dump-ast main.swift 查看抽象语法树

SIL分析02.png

这里我们简单把之前项目通过命令生成SIL文件并打开:swiftc -emit-sil main.swift >> ./main.sil && open main.sil

SIL分析03.png

  • 在SIL文件中搜索s4main10HZMTeacherCACycfC,其内部实现主要是分配内存+初始化变量
  • allocing_ref实际创建一个HZMTeacher的实例对象,当前实例对象的引用计数为1
  • 调用init方法
// HZMTeacher.__allocating_init()
sil hidden [exact_self_class] @$s4main10HZMTeacherCACycfC : $@convention(method) (@thick HZMTeacher.Type) -> @owned HZMTeacher {
// %0 "$metatype"
bb0(%0 : $@thick HZMTeacher.Type):
  // 堆上分配内存空间
  %1 = alloc_ref $HZMTeacher                      // user: %3
  // function_ref HZMTeacher.init() 初始化当前变量
  %2 = function_ref @$s4main10HZMTeacherCACycfc : $@convention(method) (@owned HZMTeacher) -> @owned HZMTeacher // user: %3
  %3 = apply %2(%1) : $@convention(method) (@owned HZMTeacher) -> @owned HZMTeacher // user: %4
  // 返回
  return %3 : $HZMTeacher                         // id: %4
} // end sil function '$s4main10HZMTeacherCACycfC'

SIL语言对于Swift源码的分析是非常重要的,关于其更多的语法信息,可以在这个网站进行查询

swift_allocObject 源码分析

swift_allocObject的源码如下,主要有以下几部分:

  • 通过swift_slowAlloc分配内存,并进行内存字节对齐
  • 通过new + HeapObject + metadata初始化一个实例对象
  • 函数的返回值是HeapObject类型,所以当前对象的内存结构就是
  • HeapObject的内存结构
                                       size_t requiredSize,
                                       size_t requiredAlignmentMask) {
  assert(isAlignmentMask(requiredAlignmentMask));
  auto object = reinterpret_cast(
      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函数,其内部主要是通过malloc中分配size大小的内存空间,并返回内存地址,主要是用于存储实例变量
  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);// 堆中创建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内存分配过程中发生的事情:

  1. _allocating_init --> swift_allocObject --> _ swift_allocObject --> swift_slowAlloc --> Malloc
  2. Swift对象的内存结构HrepObject,有两个属性:一个是Metadata,一个是Refcount,默认占用16字节大小,就是对象中没有任何东西也是16字节。OC中实例对象的本质是结构体,是以objc_object为模板继承的,其中有一个isa指针,占8字节。Swift比OC中多了一个refCounted引用计数大小,也就是多了8字节。
  3. init在这里扮演了初始化变量的职责,这和我们OC中的认知是一样的。

类结构探索

看了上面的内存分配之后,我们应该注意到了一个Metadata,它的类型是HeapMetadata,我们来看下它的具体内存结构是什么?

进入HeapMetadata定义,是TargetHeapMetaData类型的别名,接收了一个参数Inprocess
using HeapMetadata = TargetHeapMetaData;
进入TargetHeapMetaData定义,其本质是一个模板类型,其中定义了一些所需的数据结构。这个结构体中没有属性,只有初始化方法,传入了一个MetadataKind类型的参数(该结构体没有,那么只有在父类中了)这里的kind就是传入的Inprocess

template 
struct TargetHeapMetadata : TargetMetadata {
  using HeaderType = TargetHeapMetadataHeader;

  TargetHeapMetadata() = default;
  //初始化方法
  constexpr TargetHeapMetadata(MetadataKind kind)
    : TargetMetadata(kind) {}
#if SWIFT_OBJC_INTEROP
  constexpr TargetHeapMetadata(TargetAnyClassMetadata *isa)
    : TargetMetadata(isa) {}
#endif
};

进入TargetMetaData定义,有一个kind属性,kind的类型就是之前传入的Inprocess。从这里可以得出,对于kind,其类型就是unsigned long,主要用于区分是哪种类型的元数据

struct TargetMetaData{
   using StoredPointer = typename Runtime: StoredPointer;
    ...
    
    StoredPointer kind;
}

//******** Inprocess 定义 ********
struct Inprocess{
    ...
    using StoredPointer = uintptr_t;
    ...
}

//******** uintptr_t 定义 ********
typedef unsigned long uintptr_t;

TargetHeapMetadataTargetMetaData定义中,均可以看出初始化方法中参数kind的类型是MetadataKind

进入MetadataKind定义,里面有一个#include "MetadataKind.def",点击进入,其中记录了所有类型的元数据,所以kind种类总结如下:

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结构体定义中,找方法getClassObject,在该方法中去匹配kind返回值是TargetClassMetadata类型

如果是Class,则直接对this(当前指针,即metadata)强转为ClassMetadata

 
//******** 具体实现 ********
template<> inline const ClassMetadata *
  Metadata::getClassObject() const {
    //匹配kind
    switch (getKind()) {
      //如果kind是class
    case MetadataKind::Class: {
      // Native Swift class metadata is also the class object.
      //将当前指针强转为ClassMetadata类型
      return static_cast(this);
    }
    case MetadataKind::ObjCClassWrapper: {
      // Objective-C class objects are referenced by their Swift metadata wrapper.
      auto wrapper = static_cast(this);
      return wrapper->Class;
    }
    // Other kinds of types don't have class objects.
    default:
      return nullptr;
    }
  }

这一点,我们可以通过lldb来验证


源码分析.png

po metadata->getKind(),得到其kind是Class
po metadata->getClassObject()、x/8g 0x0000000110efdc70,这个地址中存储的是元数据信息!

所以,TargetMetadataTargetClassMetadata 本质上是一样的,因为在内存结构中,可以直接进行指针的转换,所以可以说,我们认为的结构体,其实就是TargetClassMetadata

进入TargetClassMetadata定义,继承自TargetAnyClassMetadata,有以下这些属性,这也是类结构的部分

struct TargetClassMetadata : public TargetAnyClassMetadata {
    ...
    //swift特有的标志
    ClassFlags Flags;
    //实力对象内存大小
    uint32_t InstanceSize;
    //实例对象内存对齐方式
    uint16_t InstanceAlignMask;
    //运行时保留字段
    uint16_t Reserved;
    //类的内存大小
    uint32_t ClassSize;
    //类的内存首地址
    uint32_t ClassAddressPoint;
  ...
}

进入TargetAnyClassMetadata定义,继承自TargetHeapMetadata

template 
struct TargetAnyClassMetadata : public TargetHeapMetadata {
    ...
    ConstTargetMetadataPointer Superclass;
    TargetPointer CacheData[2];
    StoredSize Data;
    ...
}

总结:

当前类返回的实际类型是 TargetClassMetadata,而TargetMetaData中只有一个属性kindTargetAnyClassMetaData中有3个属性,分别是kindsuperclasscacheData

当前Class在内存中所存放的属性TargetClassMetadata属性 + TargetAnyClassMetaData属性 + TargetMetaData属性 构成,所以得出的metadata的数据结构体如下所示

    void *kind;//相当于OC中的isa,kind的实际类型是unsigned long
    void *superClass;
    void *cacheData;
    void *data;
    uint32_t flags; //4字节
    uint32_t instanceAddressOffset;//4字节
    uint32_t instanceSize;//4字节
    uint16_t instanceAlignMask;//2字节
    uint16_t reserved;//2字节
    
    uint32_t classSize;//4字节
    uint32_t classAddressOffset;//4字节
    void *description;
    ...
}

Swift属性

  • 存储属性:
    常量: let 修饰
    变量: var 修饰
 let age: Int = 18
 var name: String = "HZM"
}
 let t = HZMTeacher()

对于上面的age、name来说,都是我们的变量存储属性,这点我们在SIL文件中可以看出来


属性01.png

特征:会占用实例对象分配的内存空间

  • 计算属性:不占用空间,本质是get、set方法
    验证方式:使用print(class_getInstanceSize(HZMTeacher.self))来打印输出
  • 属性观察者:
    willSet:新值存储之前调用oldValue
    didSet:新值存储之后调用newValue


    属性02.png

*注意:在init()方法中调用修改属性不触发属性观察者


属性03.png

注:子类与父类的调用顺序如上图:子父父子(听到一个挺恰当的比喻,儿子从父亲那继承了遗产,但是想变卖的时候还是需要跟父亲说一声,父亲操作一番之后再告诉儿子进行变卖)

  • 延迟存储属性:

    1. 使用lazy修饰的存储属性
    class HZMTeacher{
      lazy var age: Int = 10
    }
    
    1. 延迟属性必须有一个默认的初始值


      懒加载01.png
  1. 延迟存储在第一次访问的时候才被赋值


    懒加载02.png

从图中我们可以看出,第一个断点的时候第一次访问时值为0 第二个断点的时候代表访问过后,进行了赋值

  1. 延迟存储属性并不能保证线程安全
    我们也可以通过sil文件来查看,这里可以在生成sil文件时,加上还原swift中混淆名称的命令(即xcrun swift-demangle):swiftc -emit-sil main.swift | xcrun swift-demangle >> ./main.sil && open main.sil
    懒加载03.png

通过分析SIL文件我们可以看到,最终走分支的时候是需要判断值的,假定有两个线程进入,第一个因为值为空走了bb2,然后第二个线程进来的时候还没有赋值,判断还是走了bb2流程。
所以,在此时,线程1会走一遍赋值,线程2也会走一遍赋值,并不能保证属性只初始化了一次

  1. 延迟存储属性对实例对象大小的影响


    懒加载04.png

从打印类的内存大小可以看出,使用懒加载后内存变大(空间换时间)

  • 类型属性:
    类型属性属于这个类的本身,不管有多少个实例,类型属性只有一份,我们使用static来修饰一个类型属性
  1. 使用static修饰,且是全局变量
  2. 类型属性必须有一个默认初始值
  3. 类型属性只会被初始化一次

你可能感兴趣的:(Swift进阶01:类、对象、属性)