Swift进阶:类、对象、属性

2020-12-18  本文已影响0人  YY323

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函数:

 xcrun swift-demangle s4main1tAA9YYTeacherCvp

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

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

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

vscode中搜索_swift_allocObject,可以看出:

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属性

SIL文件中也可以看到:

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

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

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

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

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

class YYTeacher {
    // 属性观察者
    var name : String = "YY" {
        // 新值存储之前调用
        willSet {
            print("willSet newValue = \(newValue)")
        }
        // 新值存储之后调用
        didSet {
            print("didSet oldValue = \(oldValue)")
        }
    }
}

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

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

可知:

注意:在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)")
        }
    }
}
 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
}

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

从上图可以看出:延迟存储属性本质上是一个可选类型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赋值第二次,所以延迟存储属性并不能保证只被初始化一次

class YYTeacher {
    static var age : Int = 10
}

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

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

+ (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
上一篇 下一篇

猜你喜欢

热点阅读