Swift进阶(一):类和属性

1、类

SIL

在底层流程中,OC代码和SWift代码时通过不同的编译器进行编译,然后通过LLVM,生成.o可执行文件,如下所示
在这里插入图片描述

  • OC中通过clang编译器,编译成IR,然后再生成可执行文件.o(即机器码)
  • swift中通过swiftc编译器,编译成IR,然后再生成可执行文件

下面是Swift中的编译流程,其中SIL(Swift Intermediate Language),是Swift编译过程中的中间代码,主要用于进一步分析和优化Swift代码。如下图所示,SIL位于在AST和LLVM IR之间
在这里插入图片描述

我们可以通过swiftc -h终端命令,查看swiftc的所有命令
在这里插入图片描述

通过SIL查看swift类的结构

  • 在main.swift文件定义如下代码
class test1{
    
    var age = 18
    var nikeName = "180"
}

let t = test1()
  • 生成SIL文件:swiftc -emit-sil main.swift >> ./main.sil && code main.sil,其中main的入口函数如下
    在这里插入图片描述

  • 从SIL文件中,可以看出,代码是经过混淆的,可以通过以下命令还原,以s4main5test1CACycfC为例:xcrun swift-demangle s4main5test1CACycfC
    在这里插入图片描述

  • 在SIL文件中搜索s4main10CJLTeacherCACycfC,其内部实现主要是分配内存+初始化变量

    • allocing_ref:创建一个CJLTeacher的实例对象,当前实例对象的引用计数为1
    • 调用init方法

在这里插入图片描述

源码分析

源码调试

下面我们就通过swift_allocObject来探索swift中对象的创建过程

  • 在REPL(命令交互行,类似于python的,可以在这里编写代码)中编写如下代码(也可以拷贝),并搜索swift_allocObject函数加一个断点,然后定义一个实例对象t
    在这里插入图片描述
    (注:由于本机环境没有配置成功,所以使用了style_月月的图)

  • 断点断住,查看左边local有详细的信息

在这里插入图片描述

  • 其中requiredSize是分配的实际内存大小,为40

  • requiredAlignmentMask是swift中的字节对齐方式,这个和OC中是一样的,必须是8的倍数,不足的会自动补齐,目的是以空间换时间,来提供内存操作效率

swift_allocObject 源码分析

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

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

进入HeapObject初始化方法,需要两个参数:metadata、refCounts
在这里插入图片描述

其中metadata类型是HeapMetadata,是一个指针类型,占8字节
refCounts(引用计数,类型是InlineRefCounts,而InlineRefCounts是一个类RefCounts的别名,占8个字节),swift采用arc引用计数

在这里插入图片描述

总结

  • 对于实例对象t来说,其本质是一个HeapObject 结构体,默认16字节内存大小(metadata 8字节 + refCounts8字节),与OC的对比如下

    • OC中实例对象的本质是结构体,是以objc_object为模板继承的,其中有一个isa指针,占8字节

    • Swift中实例对象,默认的比OC中多了一个refCounted引用计数大小,默认属性占16字节

  • Swift中对象的内存分配流程是:__allocating_init --> swift_allocObject --> _swift_allocObject --> swift_slowAlloc --> malloc

  • init在其中的职责就是初始化变量,这点与OC中是一致的

针对上面的分析,我们还遗留了两个问题:metadata是什么,40是怎么计算的?下面来继续探索

在demo中,我们可以通过Runtime方法获取类的内存大小
在这里插入图片描述

这点与在源码调试时左边local的requiredSize值是相等的,从HeapObject的分析中我们知道了,一个类在没有任何属性的情况下,默认占用16字节大小,

对于Int、String类型,进入其底层定义,两个都是结构体类型,那么是否都是8字节呢?可以通过打印其内存大小来验证

//********* Int底层定义 *********
@frozen public struct Int : FixedWidthInteger, SignedInteger {...}

//********* String底层定义 *********
@frozen public struct String {...}

//********* 验证 *********
print(MemoryLayout<Int>.stride)
print(MemoryLayout<String>.stride)

//********* 打印结果 *********
8
16

从打印的结果中可以看出,Int类型占8字节,String类型占16字节(后面文章会进行详细讲解),这点与OC中是有所区别的

所以这也解释了为什么CJLTeacher的内存大小等于40,即40 = metadata(8字节) +refCount(8字节)+ Int(8字节)+ String(16字节)

这里验证了40的来源,但是metadata是什么还不知道,继续往下分析

探索Swift中类的结构

在OC中类是从objc_class模板继承过来的,具体的参考这篇文章OC底层探索(五) 类的结构分析

而在Swift中,类的结构在底层是HeapObject,其中有 metadata + refCounts

HeapMetadata类型分析

下面就来分析metadata,看看它到底是什么?

  • 进入HeapMetadata定义,是TargetHeapMetaData类型的别名,接收了一个参数Inprocess
using HeapMetadata = TargetHeapMetaData<Inprocess>;
  • 进入TargetHeapMetaData定义,其本质是一个模板类型,其中定义了一些所需的数据结构。这个结构体中没有属性,只有初始化方法,传入了一个MetadataKind类型的参数(该结构体没有,那么只有在父类中了)这里的kind就是传入的Inprocess
//模板类型
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
};
  • 进入TargetMetaData定义,有一个kind属性,kind的类型就是之前传入的Inprocess。从这里可以得出,对于kind,其类型就是unsigned
    long,主要用于区分是哪种类型的元数据
//******** TargetMetaData 定义 ********
struct TargetMetaData{
   using StoredPointer = typename Runtime: StoredPointer;
    ...
    
    StoredPointer kind;
}

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

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

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

  • 进入MetadataKind定义,里面有一个#include "MetadataKind.def",点击进入,其中记录了所有类型的元数据,所以kind种类总结如下
namevalue
Class0x0
Struct0x200
Enum0x201
Optional0x202
ForeignClass0x203
Opaque0x300
Tuple0x301
Function0x302
Existential0x303
Metatype0x304
ObjCClassWrapper0x305
ExistentialMetatype0x306
HeapLocalVariable0x400
HeapGenericLocalVariable0x500
ErrorObject0x501
LastEnumerated0x7FF
  • 回到TargetMetaData结构体定义中,找方法getClassObject,在该方法中去匹配kind返回值是TargetClassMetadata类型

    • 如果是Class,则直接对this(当前指针,即metadata)强转为ClassMetadata
 const TargetClassMetadata<Runtime> *getClassObject() const;
 
//******** 具体实现 ********
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<const ClassMetadata *>(this);
    }
    case MetadataKind::ObjCClassWrapper: {
      // Objective-C class objects are referenced by their Swift metadata wrapper.
      auto wrapper = static_cast<const ObjCClassWrapperMetadata *>(this);
      return wrapper->Class;
    }
    // Other kinds of types don't have class objects.
    default:
      return nullptr;
    }
  }

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

  • po metadata->getKind(),得到其kind是Class

  • po metadata->getClassObject()、x/8g 0x0000000110efdc70,这个地址中存储的是元数据信息!
    在这里插入图片描述
    所以,TargetMetadata 和 TargetClassMetadata 本质上是一样的,因为在内存结构中,可以直接进行指针的转换,所以可以说,我们认为的结构体,其实就是TargetClassMetadata

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

template <typename Runtime>
struct TargetClassMetadata : public TargetAnyClassMetadata<Runtime> {
    ...
    //swift特有的标志
    ClassFlags Flags;
    //实力对象内存大小
    uint32_t InstanceSize;
    //实例对象内存对齐方式
    uint16_t InstanceAlignMask;
    //运行时保留字段
    uint16_t Reserved;
    //类的内存大小
    uint32_t ClassSize;
    //类的内存首地址
    uint32_t ClassAddressPoint;
  ...
}
  • 进入TargetAnyClassMetadata定义,继承自TargetHeapMetadata
template <typename Runtime>
struct TargetAnyClassMetadata : public TargetHeapMetadata<Runtime> {
    ...
    ConstTargetMetadataPointer<Runtime, swift::TargetClassMetadata> Superclass;
    TargetPointer<Runtime, void> CacheData[2];
    StoredSize Data;
    ...
}

总结

综上所述,当metadata的kind为Class时,有如下继承链:
在这里插入图片描述

  • 当前类返回的实际类型是
    TargetClassMetadata,而TargetMetaData中只有一个属性kind,TargetAnyClassMetaData中有3个属性,分别是kind,superclass,cacheData

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

struct swift_class_t: NSObject{
    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;
    ...
}

与OC对比

  • 实例对象 & 类

    • OC中的实例对象本质是结构体,是通过底层的objc_object模板创建,类是继承自objc_class

    • Swift中的实例对象本质也是结构体,类型是HeapObject,比OC多了一个refCounts

  • 方法列表

    • OC中的方法存储在objc_class结构体class_rw_tmethodList

    • swift中的方法存储在 sil_vtable

  • 引用计数

    • OC中的ARC维护的是散列表

    • Swift中的ARC是对象内部有一个refCounts属性

2、Swift属性

在swift中,属性主要分为以下几种

  • 存储属性

  • 计算属性

  • 延迟存储属性

  • 类型属性

存储属性

存储属性,又分两种:

  • 要么是常量存储属性,即let修饰

  • 要么是变量存储属性,即var修饰

定义如下代码

class test1{
    
    var age = 18
    var nikeName = "180"
}

let t = test1()

其中代码中的age、nikeName来说,都是变量存储属性,这一点可以在SIL中体现
在这里插入图片描述

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

计算属性

计算属性:是指不占用内存空间,本质是set/get方法的属性

以下写法正确吗?

class test1{
    var age: Int{
        get{
            return 18
        }
        set{
            age = newValue
        }
    }
}

在实际编程中,编译器会报以下警告,其意思是在age的set方法中又调用了age.set
在这里插入图片描述
然后运行会崩溃。原因是age的set方法中调用age.set导致了循环引用,即递归了。

验证:不占内存
对于其不占用内存空间这一特征,我们可以通过以下案例来验证,打印以下类的内存大小

class Square{
    var width: Double = 8.0
    var area: Double{
        get{
            //这里的return可以省略,编译器会自动推导
            return width * width
        }
        set{
            width = sqrt(newValue)
        }
    }
}

print(class_getInstanceSize(Square.self))

//********* 打印结果 *********
24

从结果可以看出类Square的内存大小是24,等于 (metadata + refCounts)类自带16字节 + width(8字节) = 24,是没有加上area的。从这里可以证明 area属性没有占有内存空间。

验证:本质是set/get方法

  • 将main.swift转换为SIL文件:swiftc -emit-sil main.swift >> ./main.sil
  • 查看SIL文件,对于存储属性,有_hasStorage的标识符
class Square {
  @_hasStorage @_hasInitialValue var width: Double { get set }
  var area: Double { get set }
  @objc deinit
  init()
}

对于计算属性,SIL中只有setter、getter方法

在这里插入图片描述

属性观察者(didSet、willSet)

  • willSet:新值存储之前调用 newValue
  • didSet:新值存储之后调用 oldValue

验证
可以通过demo来验证

class test1{
    
    var name: String = "测试"{
           //新值存储之前调用
           willSet{
               print("willSet newValue \(newValue)")
           }
           //新值存储之后调用
           didSet{
               print("didSet oldValue \(oldValue)")
           }
       }
}

let t = test1()
t.name = "测试111"

//**********打印结果*********
willSet newValue 测试111
didSet oldValue 测试

也可以通过编译来验证,将main.swift编译成mail.sil,在sil文件中找name的set方法
在这里插入图片描述

在init方法中是否会触发属性观察者?
以下代码中,init方法中设置name,是否会触发属性观察者?

class test1{
    var name: String = "测试"{
        //新值存储之前调用
        willSet{
            print("willSet newValue \(newValue)")
        }
        //新值存储之后调用
        didSet{
            print("didSet oldValue \(oldValue)")
        }
    }
    
    init() {
        self.name = "测试111"
    }
}

运行结果发现,并没有willSetdidSet中的打印方法,所以有以下结论:

  • 在init方法中,如果调用属性,是不会触发属性观察者

  • init中主要是初始化当前变量,除了默认的前16个字节,其他属性会调用memset清理内存空间(因为有可能是脏数据,即被别人用过),然后才会赋值

【总结】:初始化器(即init方法设置)和定义时设置默认值(即在didSet中调用其他属性值)都不会触发

哪里可以添加属性观察者?

主要有以下三个地方可以添加:

  • 类中定义的存储属性

  • 通过类继承的存储属性

class test1Child: test1{
    override var age: Int{
        //新值存储之前调用
        willSet{
            print("willSet newValue \(newValue)")
        }
        //新值存储之后调用
        didSet{
            print("didSet oldValue \(oldValue)")
        }
    }
}
  • 通过类继承的计算属性
class test1{
    var age: Int = 18
    
    var age2: Int {
        get{
            return age
        }
        set{
            self.age = newValue
        }
    }
}
var t = test1()


class test1Child: test1{
    override var age: Int{
        //新值存储之前调用
        willSet{
            print("willSet newValue \(newValue)")
        }
        //新值存储之后调用
        didSet{
            print("didSet oldValue \(oldValue)")
        }
    }
    
    override var age2: Int{
        //新值存储之前调用
        willSet{
            print("willSet newValue \(newValue)")
        }
        //新值存储之后调用
        didSet{
            print("didSet oldValue \(oldValue)")
        }
    }
}

子类和父类的计算属性同时存在didset、willset时,其调用顺序是什么?

class test1{
    var age: Int = 18{
        //新值存储之前调用
        willSet{
            print("父类 willSet newValue \(newValue)")
        }
        //新值存储之后调用
        didSet{
            print("父类 didSet oldValue \(oldValue)")
        }
    }
    
    var age2: Int {
        get{
            return age
        }
        set{
            self.age = newValue
        }
    }
}


class test1Child: test1{
    override var age: Int{
        //新值存储之前调用
        willSet{
            print("子类 newValue \(newValue)")
        }
        //新值存储之后调用
        didSet{
            print("子类 didSet oldValue \(oldValue)")
        }
    }
    
}

var t = test1Child()
t.age = 20

运行结果如下:
在这里插入图片描述

结论:对于一个属性,子类类都有属性观察者,其顺序是:先子类willset后父类willset,再父类didset子类的didset,即:子父 父子

子类调用了父类的init,是否会触发观察属性?
在上个问题的基础,修改test1Child类

class test1Child: test1{
    override var age: Int{
        //新值存储之前调用
        willSet{
            print("子类 willSet newValue \(newValue)")
        }
        //新值存储之后调用
        didSet{
            print("子类 didSet oldValue \(oldValue)")
        }
    }
    
    override init() {
        super.init()
        self.age = 20
    }
}

//****** 打印结果 ******
子类 willSet newValue 20
父类 willSet newValue 20
父类 didSet oldValue 18
子类 didSet oldValue 18

结论:从打印结果发现,会触发属性观察者,主要是因为子类调用了父类的init,已经初始化过了,而初始化流程保证了所有属性都有值(即super.init确保变量初始化完成了),所以可以观察属性了

延迟存储属性

延迟属性主要有以下几点说明:

  1. 使用lazy修饰的存储属性
  2. 延迟属性必须有一个默认的初始值
  3. 延迟存储在第一次访问的时候才被赋值
  4. 延迟存储属性并不能保证线程安全
  5. 延迟存储属性对实例对象大小的影响

下面来一一进行分析

  • 使用lazy修饰的存储属性
class test1{
    lazy var age: Int = 18
}
  • 延迟属性必须有一个默认的初始值
    如果定义为可选类型,则会报错,如下所示
    在这里插入图片描述

  • 延迟存储在第一次访问的时候才被赋值
    通过SIL查看
    在这里插入图片描述

  • setter+getter:从getter方法中可以验证,在第一次访问时,就从没值变成了有值的操作
    在这里插入图片描述
    通过sil,有以下两点说明:

  • lazy修饰的属性,在底层默认是optional,在没有被访问时,默认是nil,在内存中的表现就是0x0。在第一次访问过程中,调用的是属性的getter方法,其内部实现是通过当前enum的分支,来进行一个赋值操作

  • 可选类型是16字节吗?可以通过MemoryLayout打印

    • size:实际大小

    • stride:分配大小(主要是由于内存对齐)

print(MemoryLayout<Optional<Int>>.stride)
print(MemoryLayout<Optional<Int>>.size)

//*********** 打印结果 ***********
16
9
  • 延迟存储属性并不能保证线程安全

继续分析3中sil文件,主要是查看age的getter方法,如果此时有两个线程:

 - 线程1此时访问age,其age是没有值的,进入bb2流程
 
- 然后时间片将CPU分配给了线程2,对于optional来说,依然是none,同样可以走到bb2流程

 - 所以,在此时,线程1会走一遍赋值,线程2也会走一遍赋值,并不能保证属性只初始化了一次

类型属性

类型属性,主要有以下几点说明:

  1. 使用关键字static修饰,且是一个全局变量
  2. 类型属性必须有一个默认的初始值
  3. 类型属性只会被初始化一次

使用关键字static修饰

class test1{
    static var age: Int = 18
}

// **** 使用 ****
var age = test1.age

生成SIL文件

  • 查看定义,发现多了一个全局变量,说以,类型属性是一个全局变量
    在这里插入图片描述

  • 查看入口函数中age的获取
    在这里插入图片描述

  • 查看age的getter方法
    在这里插入图片描述

  • 其中 globalinit_029_12232F587A4C5CD8B1EEDF696793B2FC_func0是全局变量初始化函数
    在这里插入图片描述

  • builtin “once” ,通过断点调试,发现调用的是swift_once,表示属性只初始化一次

  • 源码中搜索swift_once,其内部是通过GCD的dispatch_once_f 单例实现。从这里可以验证上面的第3点

void swift::swift_once(swift_once_t *predicate, void (*fn)(void *),
                       void *context) {
#if defined(__APPLE__)
  dispatch_once_f(predicate, context, fn);
#elif defined(__CYGWIN__)
  _swift_once_f(predicate, context, fn);
#else
  std::call_once(*predicate, [fn, context]() { fn(context); });
#endif
}

类型属性必须有一个默认的初始值

如下图所示,如果没有给默认的初始值,会报错
在这里插入图片描述
所以对于类型属性来说,一是全局变量,只初始化一次,二是线程安全的

单例的创建

//****** Swift单例 ******
class test1{
    //1、使用 static + let 创建声明一个实例对象
    static let shareInstance = test1.init()
    //2、给当前init添加private访问权限
    private init(){ }
}
//使用(只能通过单例,不能通过init)
var t = CJLTeacher.shareInstance

//****** OC单例 ******
@implementation test1
+ (instancetype)shareInstance{
    static test1 *shareInstance = nil;
    dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        shareInstance = [[test1 alloc] init];
    });
    return shareInstance;
}
@end

总结

  • 存储属性会占用实例变量的内存空间

  • 计算属性不会占用内存空间,其本质是set/get方法

属性观察者

  • willset:新值存储之前调用,先通知子类,再通知父类(因为父类中可能需要做一些额外的操作),即子父

  • didSet:新值存储完成后,先告诉父类,再通知子类(父类的操作优先于子类),即父子

  • 类中的init方法赋值不会触发属性观察

  • 属性可以添加在 类定义的存储属性、继承的存储属性、继承的计算属性中

  • 子类调用父类的init方法,会触发观察属性

延迟存储属性

  • 使用lazy修饰存储属性,且必须有一个默认值

  • 只有在第一次被访问时才会被赋值,且是线程不安全的

  • 使用lazy和不使用lazy,会对实例对象的内存大小有影响,主要是因为lazy在底层是optional类型,optional的本质是enum,除了存储属性本身的内存大小,还需要一个字节用于存储case

类型属性

  • 使用static 修饰,且必须有一个默认初始值

  • 是一个全局变量,只会被初始化一次,是线程安全的

  • 用于创建单例对象:

    • 使用static + let创建实例变量
    • init方法的访问权限为private
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值