swift进阶 学习大纲
上一节,我们分析了属性
(存储性、计算型)和属性观察者
(willSet、didSet)
- Lazy 懒加载
- static 单例
- struct 结构体
- mutating & inout
- 静态函数调用
- 函数重载
- 静态寻址
准备工作:
MachoView
软件: 下载地址
MachoView
是查看机器执行文件
的工具
。苹果的应用经过LLVM编译处理
后,会输出Mach-O
格式(全称Mach Object)的可执行文件
。在这个文件中,我们可以查看APP
运行需要的代码资源
和执行指令
。
1. Lazy 懒加载
1.1 创建
swift
中懒加载
是使用Lazy
进行修饰
- 必须是
var
(可变存储属性),不可以是let
(不可变属性),也不能是option
(可选值)。
class HTPerson {
// 懒加载属性
lazy var name: String = "ht"
}
初始
时,没有值
。
首次访问
后,有值
所以
Lazy
修饰的属性,具备延时加载
功能。(首次访问
时才加载
)
1.2 大小
-
懒加载属性
的大小
,与本身属性
大小不同
:
swift中int
(64位系统)原本8字节
,但lazy修饰后
,就变成16字节
1.3 SIL分析
- 将
main.swift
输出SIL文件
,使用VSCode
打开SIL文件
:
swiftc -emit-sil main.swift >> ./main.sil
- 可以清晰看到:
懒加载属性
,创建时
,是可选值
。但是在首次访问
(getter)时,进行初始赋值
,返回非可选类型
的值。
注意
懒加载
是线程不安全
的。 读写未加锁
,多线程
同时访问
(getter)时,可能多次赋值
。
Q: 为何lazy修饰的Int属性是16字节:
- 因为
lazy修饰
的属性,会变成可选类型
。
(option
: 可选类型。本质是枚举
,值类型
)
包含some
和none
两个枚举类型。其中none
是0x0
。打印
- 其中:
none
占1字节
,some
占8字节
。所以实际大小
(size)为9字节
。- 对外遵循
align8
(8字节对齐)原则,系统
会开辟16字节空间
(8的倍数)来存储真实大小
为9字节
的数据
(align8原则
:为了避免五花八门
的空间大小
,增加系统读取数据
的困难性
。所以统一
以8字节
为一个单位
,进行一段一段
的截取
,提高读取效率
。)
lazy总结
lazy
必须修饰var
(可变
类型)存储
属性,- 必须有
默认初始值
,但初始值会延迟
到首次加载
时赋值
。
(所以lazy修饰
的属性
,叫延迟存储属性
,也叫懒加载
属性)延迟存储属性
是线程不安全
的(可能多次赋值)延迟存储属性
影响实例对象
的大小
2. static 单例
2. 1 类属性
-
类属性
使用static
修饰
class HTPerson {
static var age: Int = 18
}
print(HTPerson.age) // 打印18
print("end")
- 生成
SIL
文件,getter方法
调用了builtin "once"
,内部是调用swift_once
:
-
swift源码
查看swift_once
,内部调用gcd
的dispatch_once_f
,创建单例
,线程安全
(内部有锁,读写安全)。
2.2 OC & swift 单例
- OC单例:
使用gcd
创建,使用父类alloc
初始化,拦截alloc
,任何方式实例化
,返回
的都是单例对象
。
@implementation HTPerson
static HTPerson *sharedInstance = nil;
+ (instancetype)sharedInstance{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// 不使用alloc方法,而是调用[[super allocWithZone:NULL] init]
// 重载allocWithZone基本的对象分配方法,所以要借用父类(NSObject)的功能处理底层内存分配
// 此时不管外部使用设么方式创建对象,最终返回的都是单例对象
sharedInstance = [[super allocWithZone:NULL] init] ;
});
return sharedInstance;
}
+(id)allocWithZone:(struct _NSZone *)zone {
return [HTPerson sharedInstance] ;
}
-(id)copyWithZone:(NSZone *)zone {
return [HTPerson sharedInstance] ;
}
-(id)mutablecopyWithZone:(NSZone *)zone {
return [HTPerson sharedInstance] ;
}
@end
- Swift单例:
直接static
创建,将init方法
藏起来(private私有重写
)。
class HTPerson {
// 创建单例对象
static let sharedInstance = HTPerson()
// 重写init方法,设为私有方法
private init(){}
}
3. struct 结构体
对比struct
和class
:
init
初始化方法
struct
: 没有init时,默认生成
。创建init
后,使用自己创建的
。
class
: 必须手动创建
- 类型:
struct
:值类型
(不可更改
,分配在栈区
。copy是值拷贝
(深拷贝),不共享状态
)
class
:引用类型
(可更改
,分配在堆区
。copy是指针拷贝
(浅拷贝),共享状态
)
值类型
是直接存储值
,所以读取
和拷贝
都是值
。引用类型
是存储指针地址
,所以读取
和拷贝
都是指针地址
。
(需要通过指针地址
去取值
)检验:(
withUnsafeMutablePointer
函数读取对象
的指针地址
)
struct实例化
时,是栈区alloc_stack
使用let
创建并返回self
。(没有
看到malloc相关函数
,没有
在堆区
开辟空间)。读取值
是通过栈区地址偏移
,直接读取。
- 写时复制(Copy On Write)
- 当
struct对象
赋值给一个新对象
时,2个对象
指向的地址
是同一个
。- 只有当新对象
被引用
时,才会在新
的内存空间
,完整拷贝
一个对象
,并存储新值
。
目的:提升性能
,节约内存空间
(2个完全一样的对象,没必要占用2个内存空间,当它真正被使用时,再开辟空间)
struct值类型
中,应避免
包含引用类型
对象。因为保存的是引用类型
的地址
,同样会调用strong_retain
对引用类型
对象进行引用计数+1
。我们默认struct
内部都使用值类型
,减少
使用的困扰
(可使用CFGetRetainCount()
打印引用计数,进行观察)
4. mutating & inout
-
struct
属性不可修改
,除非有mutating
声明。
struct HTStack {
var items = [Int]()
// 使用 mutating 修饰函数
mutating func push(item: Int) {
self.items.append(item)
}
}
- 生成
SIL文件
,可以查看到mutating
修饰的函数,内部
使用@inout
声明了入参,读取的是入参地址
,而不是值
。所以可以更改
。
5. 静态函数调用
struct
是值类型
,属性
是直接读取
,那它怎么调用函数
呢?
创建
测试项目
,编译生成Demo
(.o可执行文件):
打开
终端
,输入nm
空格,拖入.o可执行文件
读取完整路径
,回车。
可以看到eat函数
编译后的符号名
(_$s4Demo8HTPersonV3eatyyF)。
使用
machoView
软件,打开编译好
的.o文件
,点击Assembly
,搜索eat
,比对函数名
,定位eat函数
在符号表
中的位置
:
选择
Symbol Table
下的Symbol
符号表,搜索eat
,比对Value
,确实可通过Assembly
中记录的地址
找到eat
函数在符号表
中的位置
。而String Table Index
记录了该符号
在字符串表
中的位置
。(第2位开始)
选择
String Table
字符串表,核对可发现,从第2个字符
开始,就是eat函数
命名重整后的符号名
:_$s4Demo8HTPersonV3eatyyF
- 总结:
- 项目
编译后
,machoView
查看.o文件
。c语言函数
和结构体函数
都是静态调用
(直接调用函数地址)静态函数
的调用
都在__TEXT段
中,__TEXT
中:记录符号
在Symbol符号表
中的位置
Symbol符号表
中:记录符号
在StringTable字符串表
中的位置
StringTable字符串表
:以字符串形式
,存储
所有变量名
和函数名
静态执行,执行效率非常高!(静态调用,就是地址调用)
DSYM文件
: 用于还原符号表
。捕获崩溃
,定位
线上BUG
- 选择
release
模式,编译
,会多生成DSYM文件
,使用MachoView
查看.O文件
。发现找不到eat
函数,比Debug模式下少很多
内容。
重点
静态链接函数
是不需要符号
的,一旦地址确定
,可直接调用
。在
Debug
调试环节可以便于开发
,但release模式
下(iOS项目),为了减小包
体积,strip
(剥掉)这些静态链接函数
的符号
。
release包
中存在的符号
,是不能
直接确定地址
的。
如:Lazy Symbol Pointers
(懒加载的符号):在首次被调用
时才会生成地址
,所以不能strip掉
。
再比如:外部库
的函数
调用:运行时
断点,在汇编
中可以看到
,调用了dyld_stub_binder
。 因为不在当前函数库
中,编译文件记录
了print函数所在的库
,并进行了动态绑定
。 在调用
这个函数时
,需要沿
着这个路径
去找库
,找
到它真正的地址
。再
进行调用
。
静态链接函数
的名称
和地址
,在编译期
就已经确定
,可优化
,直接使用地址
。
(可多次编译查看,会发现代码没改变时,名称和地址都是不会变的)
而动态库
的链接
,是dyld
在运行时
去动态查找
的,无法
直接确定地址
。所以需要符号表
记录它存放
在哪个库
的哪个地址
,再顺应摸瓜
去找到
并调用
它。
6. 函数重载
函数重载
: 使用相同
的函数名
,但入参不一样
的函数
。
struct HTPerson {
// 函数名都是eat,但参数类型不一样。
func eat(num: Int){
print("吃\(num)个")
}
func eat(name: String) {
print("吃\(name)")
}
}
Q: 为什么C
、OC
语言不支持
函数重载,而C++
、swift
支持函数重载?
区别: 是否有
命名重整
规则。C
和OC
没有,C++
和swift
有。
- 通过打印
MachO可执行文件
的符号表
。就清楚了:
【方法】 打开
终端
,输入nm
,拖入MachO可执行文件
读取完整路径,回车
:
- 我们以
各类语言
的test函数
为例:func test() { }
【C语言】函数名:
_test
【OC语言】函数名:
-[HTPerson test]
【C++语言】函数名:
__Z4testv
【Swift语言】函数名:
_$s4Demo4testyyF
这就是
C++
、swift
支持函数重载
,而C
、OC
语言不支持
的原因。
- 因为他们
函数符号
(名称)没处理
,相同命名
函数,无法区分
。
拓展
可以通过xcrun swift-demangle s4Demo4testyyF
,将swift
的test函数
符号名还原
:
(其中s4Demo4testyyF
是命名重整后
的test函数
)
7. 静态寻址
-
新建一个iOS项目,以
test函数
为例,在调用
test函数处加断点
。
-
运行代码
,打开汇编模式
,可以看到test函数
在运行时
的调用地址
为0x10cbd51e0
-
编译后
,用MachoView
中打开.o文件
,在__TEXT
中搜索tes
,找到编译后
的test函数地址
:0x1000041D0
发现test函数调用地址
在编译期
和运行时
有偏差
(ASLR
随机地址偏移)。
ASLR:随机地址偏移
- 为
保证
APP的数据安全
,每次APP启动
时,都会随机生成
一个地址偏移值
。
运行时
查找所有符号
,必须在编译期
确定的符号地址
上,加上随机生成
的ASLR
偏移值,才是运行时
正确的符号地址
。
验证
公式:
ASLR随机偏移值
=运行时基地址
-编译期基地址
运行时函数地址
=编译期函数地址
+ASLR随机偏移值
获取信息:(每次APP启动,运行时的数据都会变)
程序运行
到断点
,输入image list
打印镜像文件的地址。第一个
镜像文件地址就是运行时
的基地址
。
【运行时基地址】:0x000000010897f000
【运行时函数地址】0x1089831d0
【编译期函数地址】
0x1000041D0
【编译期基地址】
0x100000000
(Load Comand
的_TEXT
中找到VM Address
)
【计算】:
ASLR偏移值 = 【运行时基地址】:
0x000000010897f000
- 【编译期基地址】0x100000000
=0x0x000000000897f000
运行时函数地址 = 【编译期函数地址】
0x1000041D0
+ ASLR偏移值0x0x000000000897f000
= 0x00000001089831d0
与我们打印的【运行时函数地址】
0x1089831d0
一抹抹一样样。完美!!
- 至此。我相信你对
ASLR随机偏移值
,静态寻址
都十分熟悉了。