延迟存储属性
- 用
lazy修饰的存储属性 - 延迟存储属性必须有一个默认的初始值
- 在第一次访问的时候才会被赋值
- 并不能保证线程的安全
- 对实例对象大小的影响
必须有初始值,不能标为可选值,否则会报错
从上图可以看出当走到第一个断点时内存里还没有值,前面两个是
metadata和refCounts
当走入下一个断点时,再去打印内存信息,查看到才有值,所以只有当第一次去访问age这个延迟存储属性时,才会进行赋值的操作
从上面两张图可以看到
加了lazy和不加lazy后的内存大小是有变化的,具体来看下sil文件
所以
lazy var age: Int = 18在底层是一个可选类型(默认是nil,内存0x0),在第一次访问他的时候访问的是getter方法,根据枚举值的分支,判断没有值,进入相应的分支来进行赋值的操作
查看Optional的内存
根据上面可得知在底层中经过lazy修饰后的int类型是Optional可选类型,查看Optional他的本质是
enum枚举,里面有none some两个枚举值,其中none没有值,占1个字节, some有值占8个字节所以Optional就占有9个字节,但根据字节对其的原则,必须要是8的倍数来进行字节对齐,所以就补齐到16字节;so经过前面的可得知
metadata(8) +refCounts(8)+Optional(16) = 32
所以经过lazy修饰过后的存储属性的大小是有影响的
接着回到上面的sil文件的getter方法中查看
两条线程
同时访问getter方法,线程1查看到没有值,跳转bb2,此时此刻cpu的时间片分给了线程2,现在线程1停在那里还没执行完,当前的Optional依然是nil,线程2依然会跳转到bb2这条分支,这个时候时间片再分给线程1去执行,执行完bb2,时间片再分给线程2,又去执行一遍bb2,所以这个lazy属性并不能保证只能被初始化一次,可能在这个过程中被赋值两次
类型属性
- 使用 static 关键字修饰
- 类型属性必须又一个默认的初始值
- 类型属性只会被初始化一次
查看sil文件,因为是调用了age的getter方法所以直接看getter方法
查看
swift_once
调用的是GCD的
dispatch_once_f来保证线程安全
那可以把方法改写成swift的单例形式
sharedInstance是全局的常量,并且只会被初始化一次,再把init方法设置成private在外部就可以直接调用shareInstance
结构体
Class和Struct在初始化的不同
从图中可以看到在创建结构体实例的时候是默认帮我们提示出赋值参数的,查看sil文件
struct默认是帮我们生成init函数的,而class是需要我们手动创建
Struct类型
值类型
Struct是值类型
直接存储的是值,拷贝的只是值,
状态不共享
Class是引用类型
对于
引用类型来说地址里面存的是地址,而不是值,如果把这个堆空间上的地址拷贝出去,其他变量也会访问到堆地址里对应的值,他们的状态是共享的。
mutating和inout
在结构体里写一个类似于栈结构的方法,提示报错,暂时先把append注释掉换成print
再查看sil文件
查看push其中除了
itme外还有一个参数self,这个self就是Stack,并且是let的类型的,let不允许修改,所以当我们修改itmes的时候修改的是stack本身,其实itmes是self.itmes的简写。
添加mutating关键字
再查看sil文件
原来的
let变成了var,var可变,并且现在获取的是Stack的地址,不是之前直接获取的值了,所以mutating这个关键字是默认给他加了一个@inout关键字,mutating只在值类型里有用,在引用类型里没用,因为引用类型里修改的就是他的地址
从官方的例子中看
在函数的声明中默认的参数是不可变的,但是添加inout关键字后
交换成功,再从sil文件查看
和上面一样添加了
inout关键字后取的是地址了,也可以改变了
函数的静态调用
struct Stack {
func teach() {
print("teach")
}
}
var t = Stack()
t.teach()
上面的这段代码是如何调用teach函数的?用汇编模式看下
从图中可以直接看到函数的地址和符号,把可执行文件拖入MachOView中查看
_TEXT,_text是代码段,代码段里包含了所有可执行的汇编指令,搜索teach在最上面一条出现0x100002b70这个函数地址和在汇编模式下看到的一样,在MachOView中可以直接看到函数的地址,因为在编译链接完成之后这个地址就已经确定了,所有的struct的函数调用都是静态调用
符号
在上面的汇编图中有一个SwiftTest.Stack.teach()这个符号是从符号表中来
符号表存储的是符号位于字符串表中的位置虽然他是符号表,但他并不直接存储符号,
字符串表
字符串表里面存储着所有的
变量名和函数名,都以字符串的形式存储在里面
通过终端查看所有符号
输入nm再把可执行文件拖入终端
还可以通过
xcrun swift-demangle 来还原符号
还可通过
对应的地址找到重整过后的符号
在release环境下编译,会发现多出一个DSYM文件(此文件是用于线上项目定位崩溃用的文件),此时再把可执行文件拖入MachOView中查看
在符号表中搜索teach发现什么也没有,因为在release环境下会
strip掉这些符号,静态链接的函数是不需要符号的,一旦编译完成之后,符号表就会strip掉这些符号,剩下的都是一些不能确定地址的
就像Lazy Symbol Pointers首次被调用时才会生成地址,不能strip掉
函数重载
新建一个c工程
把可执行文件拖到machoview中查看symbol table
命名重整后
只是加了一个下划线
oc工程
是通过
selector查找imp的,但两个函数名相同无法查找imp了
swift工程
ASLR
新建工程,打断点查看汇编
每次APP启动时,都会生成一个
随机的地址偏移值(ASLR),在编译链接时确定的符号地址,加上随机生成的ASLR偏移值,才是运行时正确的符号地址
在控制台输入
image list会出现运行基地址
再把可执行文件拖入MachOView中查看
load Commands下的LC_SEGMENT_64(_TEST),找到VM Address,这个是编译链接基地址,
运行基地址-编译链接基地址=随机偏移地址(ASLR)
验证下
0x0000000006cbd000为ASLR
0x0000000100003ad0为编译链接地址
用nm命令在终端中列出符号表搜索再用
xcrun命令还原符号
完美还原