美文网首页
Swift进阶:类、对象、属性

Swift进阶:类、对象、属性

作者: YY323 | 来源:发表于2020-12-18 22:48 被阅读0次

Swift编译简介

首先需要了解的是,iOS开发的语言不管是OC还是Swift,后期都是通过LLVM进行编译的,如下图:

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

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

swiftc -h

如下图

可以看出:
swift文件在被编译成可执行文件之前,会先被编译成SIL (Swift intermediate language)文件。

分析SIL文件之前,先新建一个class:

class YYTeacher {
    var age : Int = 20
    var name : String = "YY"
}

var t = YYTeacher()

通过SIL文件来分析Swift对象

var t = YYTeacher()这句代码类比OC来说,实际做了两件事情:
alloc --> 内存分配
init --> 初始化操作
那么对于Swift来说,做了什么事情呢?下面我们通过SIL文件来观察一下。

  • 打开终端进入项目所在目录,输入命令(二选一,建议使用第二条命令):
swiftc -emit-sil main.swift >> ./main.sil && open main.sil
# 用这个命令SIL文件更清晰
swiftc -emit-sil main.swift | xcrun swift-demangle >> ./main.sil && open main.sil

如果打开sil文件失败,如图:

则自行到sil所在目录手动选择使用vscode打开,如图:

接下来看一下sil文件里面main函数:

  • %0, %1...在SIL中也叫寄存器,可以理解为日常开发中的常量,一旦赋值后就不可再修改,如果SIL中还要继续使用,就需要不断累加数字。这里说的寄存器是虚拟的,最终运行到机器上会使用真的寄存器。
  • alloc_global后面的参数s4main1tAA9YYTeacherCvp可以通过以下命令看出是什么:
 xcrun swift-demangle s4main1tAA9YYTeacherCvp

其实就是经过swift混写后的字符串,如图

可以看出:s4main1tAA9YYTeacherCvp实际就是YYTeacher里面的实例对象t.

这里也可以通过符号断点vscode源码调试的方法来看一下Swift内存分配过程中发生了什么?

vscode中搜索_swift_allocObject,可以看出:

  • 综上可总结出Swift内存分配过程:
    __allocating_init ----> swift_allocObject ----> _swift_allocObject_ ----> swift_showAlloc ----> malloc
  • 实例对象t本质就是_swift_allocObject_的返回类型HeapObject
  • Swift对象的内存结构HeapObject有两个属性:
struct HeapObject {
# 指针 默认占8字节
  HeapMetadata const *metadata;
# InlineRefCounts是 class RefCounts,所以RefCounts是一个对象,默认占8字节
  InlineRefCounts refCounts;
}

可看出HeapObject默认占8 + 8 = 16字节
OC中,实例对象本质则是objc_object,里面有一个class_isa,默认8字节。

通过SIL文件来分析Swift类结构

通过在vscode源码分析可得如下图关系所示:

可得出当前metadata的数据结构:

struct swift_class_t {
    // 如果要与OC交互(继承NSObject),则kind则等同于void *isa;
    void kind;

    void *superClass
    void *cacheData
    void *data

    uint32_t flags
    uint32_t instanceAddressOffset
    uint32_t instanceSize
    uint16_t instanceAlignMask
    uint16_t reserved 
    uint32_t classSize
    uint32_t classAddressOffset
    void *description
    void * IVarDestroyer
// ...
};

Swift属性

  • 存储属性占用内存空间的属性
    在上面例子class中,默认声明的属性agename就是存储属性,通过var(变量)或者let(常量)来修饰。

SIL文件中也可以看到:

通过查看内存地址也可以看出:

可见agename都占用了内存空间。

  • 计算属性:只有getset方法,不存储值在内存中

如下图:area则为计算属性,不占用内存空间

SIL中也可以看出area不占用内存空间

计算属性的本质getset方法,方法存放在metadata元数据中(OC中则存放在objc_classMethod_list里面)

  • 属性观察者willSetdidSet,作用是监听属性的变化
class YYTeacher {
    // 属性观察者
    var name : String = "YY" {
        // 新值存储之前调用
        willSet {
            print("willSet newValue = \(newValue)")
        }
        // 新值存储之后调用
        didSet {
            print("didSet oldValue = \(oldValue)")
        }
    }
}

var t = YYTeacher()
t.name = "newYY"

通过查看SIL文件中nameset方法:

可知:

  • willSet中可访问到newValueself
  • didSet中可访问到oldValueself

注意:在init方法中调用属性不会触发属性观察者的,以下面特殊情况为例。

class YYTeacher {
    var age : Int = 20
    
    var name : String = "YY" {
        // 新值存储之前调用
        willSet {
            print("willSet newValue = \(newValue)")
        }
        // 新值存储之后调用
        didSet {
            print("didSet oldValue = \(oldValue)")
        }
    }
    
    // 初始化当前变量
    init() {
        // 不会触发属性观察者 
        self.name = "newYY"
        self.age = 18
    }
}

var t = YYTeacher()

属性观察者可以定义在哪些地方呢?

  • 定义的存储属性
  • 继承的存储属性
class YYMathTeacher: YYTeacher {
    override var age: Int {
        willSet {
            print("willSet newValue = \(newValue)")
        }
        didSet {
            print("didSet oldValue = \(oldValue)")
        }
    }
}
  • 继承的计算属性
    YYTeacher中计算属性age2
 var age2 : Int {
        get {
            return age
        }
        set {
            self.age = newValue
        }
    }
class YYMathTeacher: YYTeacher {
    override var age2: Int {
        willSet {
            print("willSet newValue = \(newValue)")
        }
        didSet {
            print("didSet oldValue = \(oldValue)")
        }
    }
}

注意:定义的计算属性里面不能添加属性观察者,因为get和set自己都已经实现了,想要通知外界完全可以在自己的get和set方法里面操作。

如果父类和子类中的同一属性的属性观察者同时存在,那么调用顺序是怎样的?

注意:在子类的init方法中调用继承的属性会调用属性观察者,因为在调用之前先调用了super.init(),确保父类变量已经初始化完成

  • 延迟存储属性
class YYTeacher {
   lazy var age : Int = 12
}
  • lazy修饰的存储属性
  • 延迟存储属性必须有一个默认的初始值,可选类型 ? 和隐式可选类型 ! 都不行
  • 延迟存储属性在第一次访问的时候才会被赋值

被第一次访问后,查看内存:

  • 延迟存储属性的本质:可选类型Optional

从上图可以看出:延迟存储属性本质上是一个可选类型Optional,在没有被访问之前值为nilget方法中通过switch枚举值,跳转分支来进行赋值操作。

  • 延迟存储属性对类的内存大小的影响
    如图
普通存储属性的内存大小 延迟存储属性的内存大小

通过上面了解到,延迟存储属性的本质是一个可选类型,所以来研究一下可选类型的内存大小

通过控制台打印得出:
MemoryLayout<Optional<Int>>.stride = 16---> 在内存分配的过程中,为了让它的地址是偶地址,字节对齐后,系统实际分配的内存大小。(字节对齐:以空间换取时间,提高访问效率)
MemoryLayout<Optional<Int>>.size = 9 --- > 从存储开始到存储结束占用的字节大小,即实际占用的内存大小

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

通过上面SIL中的get方法可以看到:如果有两个线程同时访问get方法,假如CPU线程1刚执行到bb2时就把时间片分给了线程2线程2也刚刚执行到bb2的时候又将时间片分给线程1,这时线程1执行完bb2赋值第一次,然后线程2执行完bb2赋值第二次,所以延迟存储属性并不能保证只被初始化一次

  • 类型属性
  • 使用关键字static修饰
  • 类型属性必须有一个默认初始值
class YYTeacher {
    static var age : Int = 10
}

上面例子中age是一个类型属性,通过YYTeacher.age来访问它。

  • 类型属性只会被初始化一次
    通过SIL可以看出通过static修饰的属性是一个全局属性

通过上图可以看出:通过static修饰的类型属性可以保证该属性只被初始化一次。相比lazy来说,static声明的类型属性是:

  • 全局
  • 赋值过程是一个线程安全的过程
    同时,可延伸出单例的正确写法:
    OC中单例写法
+ (instancetype)sharedInstance {
        static Thread *sharedInstance = nil;
        static dispatch_once_t onceToken;

        dispatch_once(&onceToken, ^{
              sharedInstance = [[Thread alloc] init];
        });
        return sharedInstance;
}

Swift2.0以后的单例写法

class YYTeacher {
    // 使用static let创建声明一个实例对象
    static let sharedInstance : YYTeacher = YYTeacher();
    // 给当前init添加访问控制权限,不能再通过var t = YYTeacher()这种方式创建实例对象
    private init(){}
}
// 只能通过这种方式获取实例变量
var t = YYTeacher.sharedInstance

相关文章

网友评论

      本文标题:Swift进阶:类、对象、属性

      本文链接:https://www.haomeiwen.com/subject/vznugktx.html