Swift-进阶 02:类、对象、属性

Swift 进阶之路 文章汇总

本文主要介绍以下几点

  • 通过SIL来理解对象的创建

  • Swift类结构分析

  • 存储属性 & 计算属性

  • 延迟存储属性 & 单例创建方式

SIL

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

SIL-1

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

SIL-2

注意:这里需要说明一下,Swift与OC的区别在于 Swift生成了高级的SIL

我们可以通过swiftc -h终端命令,查看swiftc的所有命令

SIL-3

例如:在main.swift文件定义如下代码

class CJLTeacher{
    var age: Int = 18
    var name: String = "CJL"
}

var t = CJLTeacher()
  • 查看抽象语法树:swiftc -dump-ast main.swift
    SIL-4
  • 生成SIL文件:swiftc -emit-sil main.swift >> ./main.sil && code main.sil,其中main的入口函数如下
// main
//`@main`:标识当前main.swift的`入口函数`,SIL中的标识符名称以`@`作为前缀
sil @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 {
//`%0、%1` 在SIL中叫做寄存器,可以理解为开发中的常量,一旦赋值就不可修改,如果还想继续使用,就需要不断的累加数字(注意:这里的寄存器,与`register read`中的寄存器是有所区别的,这里是指`虚拟寄存器`,而`register read`中是`真寄存器`)
bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>):
//`alloc_global`:创建一个`全局变量`,即代码中的`t`
  alloc_global @$s4main1tAA10CJLTeacherCvp        // id: %2
//`global_addr`:获取全局变量地址,并赋值给寄存器%3
  %3 = global_addr @$s4main1tAA10CJLTeacherCvp : $*CJLTeacher // user: %7
//`metatype`获取`CJLTeacher`的`MetaData`赋值给%4
  %4 = metatype $@thick CJLTeacher.Type           // user: %6
//将`__allocating_init`的函数地址赋值给 %5
  // function_ref CJLTeacher.__allocating_init()
  %5 = function_ref @$s4main10CJLTeacherCACycfC : $@convention(method) (@thick CJLTeacher.Type) -> @owned CJLTeacher // user: %6
//`apply`调用 `__allocating_init` 初始化一个变量,赋值给%6
  %6 = apply %5(%4) : $@convention(method) (@thick CJLTeacher.Type) -> @owned CJLTeacher // user: %7
//将%6的值存储到%3,即全局变量的地址(这里与前面的%3形成一个闭环)
  store %6 to %3 : $*CJLTeacher                   // id: %7
//构建`Int`,并`return`
  %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'

注意:code命令是在.zshrc中做了如下配置,可以在终端中指定软件打开相应文件

$ open .zshrc
//****** 添加以下别名
alias subl='/Applications/SublimeText.app/Contents/SharedSupport/bin/subl'
alias code='/Applications/Visual\ Studio\ Code.app/Contents/Resources/app/bin/code'

//****** 使用
$ code main.sil

//如果想SIL文件高亮,需要安装插件:VSCode SIL
  • 从SIL文件中,可以看出,代码是经过混淆的,可以通过以下命令还原,以s4main1tAA10CJLTeacherCvp为例:xcrun swift-demangle s4main1tAA10CJLTeacherCvp

    SIL-5

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

    • allocing_ref:创建一个CJLTeacher的实例对象,当前实例对象的引用计数为1
    • 调用init方法
//********* main入口函数中代码 *********
%5 = function_ref @$s4main10CJLTeacherCACycfC : $@convention(method) (@thick CJLTeacher.Type) -> @owned CJLTeacher 

// s4main10CJLTeacherCACycfC 实际就是__allocating_init()
// CJLTeacher.__allocating_init()
sil hidden [exact_self_class] @$s4main10CJLTeacherCACycfC : $@convention(method) (@thick CJLTeacher.Type) -> @owned CJLTeacher {
// %0 "$metatype"
bb0(%0 : $@thick CJLTeacher.Type):
// 堆上分配内存空间
  %1 = alloc_ref $CJLTeacher                      // user: %3
  // function_ref CJLTeacher.init() 初始化当前变量
  %2 = function_ref @$s4main10CJLTeacherCACycfc : $@convention(method) (@owned CJLTeacher) -> @owned CJLTeacher // user: %3
  // 返回
  %3 = apply %2(%1) : $@convention(method) (@owned CJLTeacher) -> @owned CJLTeacher // user: %4
  return %3 : $CJLTeacher                         // id: %4
} // end sil function '$s4main10CJLTeacherCACycfC'

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

符号断点调试

  • 在demo中设置_allocing_init符号断点
    SIL-6

    发现其内部调用的是swift_allocObject
    SIL-7

源码调试

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

  • REPL(命令交互行,类似于python的,可以在这里编写代码)中编写如下代码(也可以拷贝),并搜索swift_allocObject函数加一个断点,然后定义一个实例对象t

    SIL-8

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


    SIL-9
  • 其中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
    源码分析-1
    • 其中metadata类型是HeapMetadata,是一个指针类型,占8字节
    • refCounts(引用计数,类型是InlineRefCounts,而InlineRefCounts是一个类RefCounts的别名,占8个字节),swift采用arc引用计数
      源码分析-2

总结

  • 对于实例对象t来说,其本质是一个HeapObject 结构体,默认16字节内存大小(metadata 8字节 + refCounts 8字节),与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方法获取类的内存大小

源码分析-3

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

对于IntString类型,进入其底层定义,两个都是结构体类型,那么是否都是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模板继承过来的,具体的参考这篇文章iOS-底层原理 08:类 & 类结构分析

而在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种类总结如下
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
 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,这个地址中存储的是元数据信息!
    源码分析-4

所以,TargetMetadataTargetClassMetadata 本质上是一样的,因为在内存结构中,可以直接进行指针的转换,所以可以说,我们认为的结构体,其实就是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;
    ...
}

总结

综上所述,当metadatakind为Class时,有如下继承链:

源码分析-5

  • 当前类返回的实际类型是 TargetClassMetadata,而TargetMetaData中只有一个属性kindTargetAnyClassMetaData中有4个属性,分别是kind, superclass,cacheData、data(图中未标出)
  • 当前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中的方法存储在 metadata 元数据中

  • 引用计数
    • OC中的ARC维护的是散列表

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

Swift属性

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

  • 存储属性

  • 计算属性

  • 延迟存储属性

  • 类型属性

存储属性

存储属性,又分两种:

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

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

定义如下代码

class CJLTeacher{
    var age: Int = 18
    var name: String = "CJL"
}

let t = CJLTeacher()

其中代码中的age、name来说,都是变量存储属性,这一点可以在SIL中体现

class CJLTeacher {
    //_hasStorage 表示是存储属性
  @_hasStorage @_hasInitialValue var age: Int { get set }
  @_hasStorage @_hasInitialValue var name: String { get set }
  @objc deinit
  init()
}

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

下面我们同断点调试来验证

  • po t

  • x/8g 内存地址,即HeapObject存储的地址


    属性-1

    属性-2

计算属性

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

我们通过一个demo来说明,以下写法正确吗?

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

在实际编程中,编译器会报以下警告,其意思是在age的set方法中又调用了age.set

属性-3

然后运行发现崩溃了,原因是age的set方法中调用age.set导致了循环引用,即递归
属性-4

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

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方法
    属性-5

属性观察者(didSet、willSet)

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

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

验证

  • 可以通过demo来验证
class CJLTeacher{
    var name: String = "测试"{
        //新值存储之前调用
        willSet{
            print("willSet newValue \(newValue)")
        }
        //新值存储之后调用
        didSet{
            print("didSet oldValue \(oldValue)")
        }
    }
}
var t = CJLTeacher()
t.name = "CJL"

//**********打印结果*********
willSet newValue CJL
didSet oldValue 测试
  • 也可以通过编译来验证,将main.swift编译成mail.sil,在sil文件中找nameset方法
    属性-6

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

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

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

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

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

问题2:哪里可以添加属性观察者?

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

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


class CJLMediumTeacher: CJLTeacher{
    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)")
        }
    }
}

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

有以下代码,其调用顺序是什么?

class CJLTeacher{
    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 CJLMediumTeacher: CJLTeacher{
    override var age: Int{
        //新值存储之前调用
        willSet{
            print("子类 newValue \(newValue)")
        }
        //新值存储之后调用
        didSet{
            print("子类 didSet oldValue \(oldValue)")
        }
    }
    
}

var t = CJLMediumTeacher()
t.age = 20

运行结果如下:


属性-7

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

问题4:子类调用了父类的init,是否会触发观察属性?

在问题3的基础,修改CJLMediumTeacher

class CJLMediumTeacher: CJLTeacher{
    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、延迟存储属性对实例对象大小的影响

下面来一一进行分析

1、使用lazy修饰的存储属性

class CJLTeacher{
    lazy var age: Int = 18
}

2、延迟属性必须有一个默认的初始值

如果定义为可选类型,则会报错,如下所示


属性-8

3、延迟存储在第一次访问的时候才被赋值
可以通过调试,来查看实例变量的内存变化

  • age第一次访问前的内存情况:此时的age是没值的,为0x0
    属性-9
  • age第一次访问后的内存情况:此时age是有值的,为30
    属性-10

    从而可以验证,懒加载存储属性只有在第一次访问时才会被赋值

我们也可以通过sil文件来查看,这里可以在生成sil文件时,加上还原swift中混淆名称的命令(即xcrun swift-demangle):swiftc -emit-sil main.swift | xcrun swift-demangle >> ./main.sil && code main.sil,demo代码如下

class CJLTeacher{
    lazy var age: Int = 18
}

var t = CJLTeacher()
t.age = 30
  • 类+main:lazy修饰的存储属性在底层是一个optional类型

    属性-11

  • setter+getter:从getter方法中可以验证,在第一次访问时,就从没值变成了有值的操作

    属性-12

通过sil,有以下两点说明:

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

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

    • size:实际大小
    • stride:分配大小(主要是由于内存对齐)
print(MemoryLayout<Optional<Int>>.stride)
print(MemoryLayout<Optional<Int>>.size)

//*********** 打印结果 ***********
16
9

为什么实际大小是9Optional其本质是一个enum,其中Int8字节,另一个字节主要用于存储case值(这个后续会详细讲解)

4、延迟存储属性并不能保证线程安全

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

  • 线程1此时访问age,其age是没有值的,进入bb2流程

  • 然后时间片将CPU分配给了线程2,对于optional来说,依然是none,同样可以走到bb2流程

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

5、延迟存储属性对实例对象大小的影响
下面来继续看下不使用lazy的内存与使用lazy的内存是否有变化?

  • 不使用lazy修饰的情况,的内存大小是24
    属性-13
  • 使用lazy修饰的情况下,类的内存大小是32
    属性-14

从而可以证明,使用lazy和不使用lazy,其实例对象的内存大小是不一样的

类型属性

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

  • 1、使用关键字static修饰,且是一个全局变量

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

  • 3、类型属性只会被初始化一次

1、使用关键字static修饰

class CJLTeacher{
    static var age: Int = 18
}

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

生成SIL文件

  • 查看定义,发现多了一个全局变量,说以,类型属性是一个全局变量
    属性-15
  • 查看入口函数中age的获取


    属性-16
  • 查看age的getter方法


    属性-17
    • 其中 globalinit_029_12232F587A4C5CD8B1EEDF696793B2FC_func0是全局变量初始化函数
      属性-18
    • builtin "once" ,通过断点调试,发现调用的是swift_once,表示属性只初始化一次
      属性-19
  • 源码中搜索swift_once,其内部是通过GCDdispatch_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
}

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

如下图所示,如果没有给默认的初始值,会报错


属性-20

所以对于类型属性来说,一是全局变量,只初始化一次,二是线程安全的

单例的创建

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

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

总结

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

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

  • 属性观察者

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

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

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

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

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

  • 延迟存储属性

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

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

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

  • 类型属性

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

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

    • 用于创建单例对象:

      • 使用static + let创建实例变量

      • init方法的访问权限为private

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

推荐阅读更多精彩内容