原创:知识点总结性文章
创作不易,请珍惜,之后会持续更新,不断完善
个人比较喜欢做笔记和写总结,毕竟好记性不如烂笔头哈哈,这些文章记录了我的IOS成长历程,希望能与大家一起进步
温馨提示:由于简书不支持目录跳转,大家可通过command + F 输入目录标题后迅速寻找到你所需要的内容
目录
- 一、存储 Memory
- 1、设备自身的存储方式
- 2、代码在内存中的存储
- 3、区分 Struct 与 Class 用法上的差异
- 二、CPU 的运作方式
- 1、中央处理器
- 2、中央调度系统
- Demo
- 参考文献
一、存储 Memory
1、设备自身的存储方式
电子设备,无论是身形较小的 Apple Watch、主流的 iPhone、还是大屏的 Mac, 都具备两种硬件来存储信息,这两种硬件一个叫做硬盘、另一个叫做内存。如下图所示,硬盘负责永久存储信息,内存用于临时存储信息。
一般来说,当用户下载你的应用程序后,应用程序中所包含的所有代码、设计素材等都会被存放在设备的硬盘中。这些信息不会因为设备开关机而丢失,所以硬盘这种存储介质也被大家称作永久存储 Permanent Storage
。
与之相对应的,内存中所存放的信息,会在设备关机后彻底丢失,因此也被叫做临时存储 Temporary Storage
。与用户硬盘的的较大空间不同,内存的空间则相对较小,以 iPhone 12 Pro 为例,其硬盘的大小可能为 512 GB,而内存则仅有 6 GB。
内存的空间有限,因此仅被用来临时存放正在运行的应用。比如你的应用叫「睡眠助手」,只有当用户启动该应用时,系统才会将「睡眠助手」应用的全部信息从硬盘挪到内存中。若用户同时在几个应用程序中来回切换,操作系统会尽可能地让这些应用都放在内存中。在察觉到内存即将耗尽时,操作系统会自动将用户最早使用的应用移出内存。
2、代码在内存中的存储
进一步讲,我们学习的所有 Swift 代码,比如整数 Int
、浮点数 Double
、类 Class
、数组 Array
、结构 Struct
等,实际都运行在设备的内存中。你可以将内存想象为线性排列且数量众多的小格子,我们所编写的代码便存储在格子中。
对于Swift 中的绝大多数类型,如 Int
、Double
、Bool
、Enum
、Struct
、Array
、Dictionary
,其使用内存的方式都很直接,就是直接将信息存储在下图的小格子中。对于这些类型的常量或变量来说,其指代便是内存中的信息本身,因此以上类型也被称作值类型 Value Type
。比如下图中的常量 number
,实际存储的信息便是内存格子中的值 12345。
然而对于某些特殊的类型,如 Class
的常量或变量,它使用小格子的方法有些区别。假如我们把内存中的小格子当作一栋公寓楼并为其标上门牌号 A - J
。Class
的常量或变量存储的不是小格子中的信息的值,而是信息所对应值的门牌号,因此 Class
也被称为引用类型 Reference Type
。比如下图中的类实体 civicCar
,当把值 Car(brand: "Honda")
放在小格子中后,civicCar
存储的信息实际上是门牌号 G
。
总的来说,在 Swift 编程语言中,你看到的绝大多数类型都是值类型。如 Struct
的实体实际存储的是内存中对应的值。与之相反,引用类型如 Class
,其实体所存储的是内存中值所对应的门牌号。
3、区分 Struct 与 Class 用法上的差异
上文中,你了解到了 struct
和 class
分别作为值类型和引用类型在存储逻辑上的差异。本小节中,我将使用一个车辆颜色的案例,让你看看这两种存储方式在实际使用中的差异。
如下所示,用 struct
和 class
分别创建新类型车辆颜色 CarColor
,其只存储一个字符串颜色 color
。为帮助你区分二者,我在命名上标注出了其归属,其中 struct
创建的新类型叫做 CarColorByStruct
,class
创建的新类型叫做 CarColorByClass
。class
要求我们必须提供初始化器,因此下图使用 init(color:)
写明,而 struct
默认自带这个初始化器,因此没有写。总的来说,下面的两个类型除 struct
及 class
关键词不同外完全一致。
struct CarColorByStruct
{
var color: String
}
class CarColorByClass
{
var color: String
init(color: String) { self.color = color}
}
Struct 版本
首先我们先来看值类型 struct
实体的表现。如下所示,创建了一个新实体 carOne
,并将其颜色赋值为黑色。之后创建一个新变量 carTwo
,并将 carOne
的值赋给 carTwo
。最后将 carTwo
的颜色更改为白色 carTwo.color = "白色"
,因 carTwo
与 carOne
这两个变量分别代表内存中的两个格子,互不干扰,因此修改完 carTwo
颜色为白色后,carOne
的颜色仍旧为黑色,没有被改变。
// carOne 赋值为黑色
var carOne = CarColorByStruct(color: "黑色")
print("carOne 所具备的颜色是: \(carOne.color)")
// 将 carOne 赋值给 carTwo,并修改 carTwo 颜色
var carTwo = carOne
carTwo.color = "白色"
// 修改后,carOne 颜色没有被改变,仍为黑色
print("carOne 所具备的颜色是: \(carOne.color)")
输出结果为:
carOne 所具备的颜色是: 黑色
carOne 所具备的颜色是: 黑色
用内存的格子来阐释上述 struct
代码的过程如下图所示。第一步创建了 carOne
,其格子中的值为黑色。第二步将 carOne
的值赋给新变量 carTwo
,carTwo
拥有了自己的格子,值与 carOne
相同。第三步将 carTwo
的颜色修改,对 carOne
没有影响。
Class 版本
与上文中完全相同的流程,若采用引用类型的 class
实体,效果如下。在下面中,创建了一个新实体 carThree
,并将其颜色设置为黑色。然后将 carThree
的值赋值给 carFour
,仅修改 carFour
的颜色为白色后,你会发现 carThree
的颜色也被修改。这是因为 carThree
和 carFour
存储的是内存中的同一个门牌号。
// carThree 赋值为黑色
var carThree = CarColorByClass(color: "黑色")
print("carThree 所具备的颜色是: \(carThree.color)")
// 将 carFour 赋值给 carThree,并修改 carFour 颜色
var carFour = carThree
carFour.color = "白色"
// 修改后,carThree 被改变,变为白色
print("carThree 所具备的颜色是: \(carThree.color)")
输出结果为:
carThree 所具备的颜色是: 黑色
carThree 所具备的颜色是: 白色
用内存的格子来阐释上述 class
代码的过程如下图所示。第一步创建了 carThree
,其格子中放置了颜色为黑色。第二步将 carThree
赋值给新变量 carFour
,此时被赋值的并非 carThree
的值,而是 carThree
所指代的门牌号 E
。第三步将 carFour
的颜色修改,carThree
的值也同时发生改变。这是因为本质上来说,carThree
和 carFour
都指的是门牌号 E
,修改的是门牌号 E
对应的同一个格子的内容。
上文所述的门牌号,用计算机学科的专业术语来讲,叫做指针 Pointer
。鉴于引用类型对比指针的特殊性,Swift 专门给了特殊的运算符用于对比指针。在 Swift 语言中,符号 ==
或 !=
用于判定运算符两边的值是否相同,符号 ===
或 !==
用于判断运算符两边的指针是否相同。如下所示,carThree
和 carFour
指针的指向位置相同。
print("是否指向同一个门牌号:\(carThree === carFour)")
输出结果为:
是否指向同一个门牌号:true
什么时候选用 struct 或 class?
对于 Apple 官方框架来说,适用于反复使用的框架一般定义为 class
。如 Message UI 框架,其用途是提供一个发送短信或邮件的窗口,定义为 class
的好处便是整个应用程序中只需要使用一个该 class
的实体 instance
,程序中用到的所有实体 instance
都可以指向该门牌号,从而避免重复占用过多内存而导致资源浪费。
同样是 Apple 的官方框架,不适合反复使用的实体常被定义为 struct
,如 SwiftUI 框架的文本 Text
。对于 UI 视图Text
来说,应用界面中的文本很少相同,且不存在一个文本继承另一个文本的情况,无需用到继承的属性,因此定义为 struct
更合理。
在书写你的应用的过程中,Apple 官方文档的建议是当需要创建一个新的自定义类别时,可以先将其定义为 struct
。只有你需要用到 class
继承的特性,或者是作为引用类型的特性时,再将其关键词换为 class
。
二、CPU 的运作方式
1、中央处理器
CPU 也叫中央处理器,是 A 系列芯片 Soc 上负责逻辑运算的重要硬件,我们写的代码主要通过设备中的 CPU 来运行。设备中的 CPU 通常又进一步分为性能核心和低功耗核心,操作系统会根据应用的实际需求来自动决定其运行场所,以达到最优的能耗表现。以下图中 iPhone 12 上的 A14 芯片为例,它包含 2 个高性能核心,以及 4 个低功耗核心。
2、中央调度系统
你可以将我们的应用程序想象成无数个需要完成的子任务,这些任务由操作系统的工作人员,也叫做 GCD 来负责分配。GCD 的全称是 Grand Central Dispatch
,翻译过来是中央调度系统,其任务便是将代码自动在恰当的时机分配给 CPU 中的不同核心来处理。
通常来说,为避免打乱应用程序的运行逻辑,GCD 会让所有代码在按其编写的先后顺序 Serial
运行。虽然没有明说,但目前为止,我们所写的所有代码都会被 GCD 运行在「主队列 main
」中。其使用方法是 DispatchQueue.main.async {}
,如下所示。
DispatchQueue.main.async
{
print("默认情况下,即使不写明,所有代码都运行在这里")
}
将以上信息用图表的方式展示出来,主队列有点像银行排着的长队,队伍中的每个人分别肩负着不同任务,这些任务可能是你的一个个函数,也可能是等着要求 CPU 将其存放在内存中的常量与变量等等。随着时间的推移,GCD 这个工作人员会让队伍中的每个人依次在 CPU 那里办理业务。
这种方法看似非常完美,但还存在一些问题。依旧以银行为例,有时候会有顾客办一个特别冗长的贷款业务,这个业务可能一办就是几个小时,以至于后面排队的人都被它堵在后面了。比如下图中的序号 5,办理的就是这种贷款业务,把后面要办理业务的 6,7,8,9 全都堵住了。
一个人办复杂业务卡住所有人的情况在应用程序中也时有发生。比如你在制作一款图片处理应用,你想给图片加上一个复杂的滤镜,而滤镜运算需要时间,因此就会堵住后面的人办事。比如你在制作一款需要读取网站文章的客户端,发送网络请求的过程可能会由于网速过慢,而等待很久才能完成。
更可怕的是,应用代码中的所有 UI 代码也在主队列中运行,如果主队列被某个任务堵住了,应用程序就好像卡死了一样。这种卡死并非是你的应用罢工了,而是某个任务运行太慢而卡住了主队列,导致后续 UI 代码无法运行。然而用户无法得知你的应用程序是真卡死了还是只是在运行较慢的任务,若他们看见应用程序按钮没反应了,就会下意识地认为它坏了。
为解决某个复杂业务堵住主队列的问题,GCD 给出的方法是加开银行窗口。这种加开窗口的办法与顺序 Serial
运行相反,也叫做并发运行 Concurrent
,它指的是在你的明确指示下,让复杂任务到别的窗口运行,不要卡在主队伍中。比如让办理贷款业务的客户去专门的大客户柜台办理。在 Swift 中,默认提供的并发队列叫做全局队列 Global
。如下图中的复杂任务 A 和复杂任务 B,分别交给了两个新窗口「全局队列」负责处理。
把任务从主队列分配给全局队列很可能只是因为它需要的时间更长而已,不一定意味着该任务不着急执行。就像银行中提供加急业务一样,我们不能只是单纯地把任务分配出去,还需要考虑任务的重要性。负责告知全局队列中任务优先级的参数是 Quality of Service
,简称 QOS
。全局队列的优先级 QOS
共有 4 种,如下图所示。
QOS
优先级从最着急要到慢慢来的排序下:.userInteractive (UI)
、.userInitiated (用户发起的任务)
、.utility (杂项)
、.background (后台)
。
如果你希望分配到全局队列的任务与 UI 相关,应将 QOS
设置为 UI 级,也就是 userInteractive
。此时 GCD 会将任务分配到高性能核心上尽快完成。反之若该项任务不急着要,比如只是在例行备份一些资料,也可以把 QOS
设置为后台级 .background
慢慢备份,执行代码如下。这时候 GCD 分工时就会看到开发者不急着要这里面的结果,因此若需要省电时则这部分内容可以放在低功耗核心上运算。若有其它更紧要的任务则可以把低优先级的任务先缓一缓,紧着最着急的来。
DispatchQueue.global(qos: .background).async
{
print("执行复杂任务")
}
Demo
Demo在我的Github上,欢迎下载。
SwiftUIDemo