Swift进阶:类、对象、属性

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

推荐阅读更多精彩内容