swift进阶四:懒加载 & 单例 & Struct

swift进阶 学习大纲

上一节,我们分析了属性(存储性、计算型)和属性观察者(willSet、didSet)

  1. Lazy 懒加载
  2. static 单例
  3. struct 结构体
  4. mutating & inout
  5. 静态函数调用
  6. 函数重载
  7. 静态寻址

准备工作:

  1. MachoView软件: 下载地址
    MachoView是查看机器执行文件工具。苹果的应用经过LLVM编译处理后,会输出Mach-O格式(全称Mach Object)的可执行文件。在这个文件中,我们可以查看APP运行需要的代码资源执行指令

1. Lazy 懒加载

1.1 创建

swift懒加载是使用Lazy进行修饰

  • 必须是var(可变存储属性),不可以是let(不可变属性),也不能是option(可选值)。
class HTPerson {
    // 懒加载属性
    lazy var name: String = "ht"
}
  • 初始时,没有值

    image.png

  • 首次访问后,有值

    image.png

  • 所以Lazy修饰的属性,具备延时加载功能。(首次访问时才加载

1.2 大小

  • 懒加载属性大小,与本身属性大小不同
    swift中int(64位系统)原本8字节,但lazy修饰后,就变成16字节
    image.png

1.3 SIL分析

  • main.swift输出SIL文件,使用VSCode打开SIL文件:
swiftc -emit-sil main.swift >> ./main.sil
image.png
  • 可以清晰看到:懒加载属性创建时,是可选值。但是在首次访问(getter)时,进行初始赋值,返回非可选类型的值。

注意
懒加载线程不安全的。 读写未加锁多线程同时访问(getter)时,可能多次赋值

Q: 为何lazy修饰的Int属性是16字节:

  • 因为lazy修饰的属性,会变成可选类型
    option: 可选类型。本质是枚举值类型
    包含somenone两个枚举类型。其中none0x0。打印
    image.png
  • 其中:none1字节some8字节。所以实际大小(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:
    image.png
  • swift源码查看swift_once,内部调用gcddispatch_once_f,创建单例线程安全(内部有锁,读写安全)。
    image.png

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 结构体

对比structclass:

  1. init初始化方法
    struct: 没有init时,默认生成。创建init后,使用自己创建的
    class: 必须手动创建
    image.png
  1. 类型:
    struct: 值类型不可更改,分配在栈区。copy是值拷贝(深拷贝),不共享状态
    class: 引用类型可更改,分配在堆区。copy是指针拷贝(浅拷贝),共享状态
    image.png
  • 值类型是直接存储值,所以读取拷贝都是
  • 引用类型是存储指针地址,所以读取拷贝都是指针地址
    (需要通过指针地址取值

检验:(withUnsafeMutablePointer函数读取对象指针地址

image.png

struct实例化时,是栈区alloc_stack使用let创建并返回self。(没有看到malloc相关函数没有堆区开辟空间)。读取值是通过栈区地址偏移,直接读取。

  1. 写时复制(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)
    }
}
image.png
  • 生成SIL文件,可以查看到mutating修饰的函数,内部使用@inout声明了入参,读取的是入参地址,而不是。所以可以更改
    image.png

5. 静态函数调用

struct值类型属性直接读取,那它怎么调用函数呢?

  • 创建测试项目,编译生成Demo(.o可执行文件):

    image.png

  • 打开终端,输入nm 空格,拖入.o可执行文件读取完整路径,回车。
    可以看到eat函数编译后的符号名(_$s4Demo8HTPersonV3eatyyF)。

    image.png

  • 使用machoView软件,打开编译好.o文件,点击Assembly,搜索eat,比对函数名,定位eat函数符号表中的位置:

    image.png

  • 选择Symbol Table下的Symbol符号表,搜索eat比对Value,确实可通过Assembly记录的地址找到eat函数在符号表中的位置。而String Table Index记录了该符号字符串表中的位置。(第2位开始)

    image.png

  • 选择String Table字符串表,核对可发现,从第2个字符开始,就是eat函数命名重整后的符号名_$s4Demo8HTPersonV3eatyyF

    image.png

  • 总结:
  • 项目编译后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掉
    再比如:外部库函数调用:print运行时断点,在汇编中可以看到,调用了dyld_stub_binder。 因为print函数不在当前函数库中,编译文件记录了print函数所在的库,并进行了动态绑定。 在调用这个函数,需要沿着这个路径找库到它真正的地址进行调用

  • 静态链接函数名称地址,在编译期就已经确定,可优化直接使用地址
    (可多次编译查看,会发现代码没改变时,名称和地址都是不会变的)
    动态库链接,是dyld运行时动态查找的,无法直接确定地址。所以需要符号表记录它存放哪个库哪个地址,再顺应摸瓜找到调用它。

6. 函数重载

函数重载: 使用相同函数名,但入参不一样函数

struct HTPerson {
    // 函数名都是eat,但参数类型不一样。
    func eat(num: Int){
        print("吃\(num)个")
    }
    
    func eat(name: String) {
        print("吃\(name)")
    }
}

Q: 为什么COC语言不支持函数重载,而C++swift支持函数重载?

区别: 是否有命名重整规则。COC没有,C++swift有。

  • 通过打印MachO可执行文件符号表。就清楚了:

【方法】 打开终端,输入nm,拖入MachO可执行文件读取完整路径,回车:

  • 我们以各类语言test函数为例:
func test() { }

【C语言】函数名:_test

image.png

【OC语言】函数名:-[HTPerson test]

image.png

【C++语言】函数名:__Z4testv

image.png

【Swift语言】函数名:_$s4Demo4testyyF

image.png

这就是C++swift支持函数重载,而COC语言不支持的原因。

  • 因为他们函数符号(名称)没处理相同命名函数,无法区分

拓展
可以通过xcrun swift-demangle s4Demo4testyyF ,将swifttest函数符号名还原
(其中s4Demo4testyyF命名重整后test函数)

image.png

7. 静态寻址

  • 新建一个iOS项目,以test函数为例,在调用test函数处加断点

    image.png

  • 运行代码,打开汇编模式,可以看到test函数运行时调用地址0x10cbd51e0

    image.png

  • 编译后,用MachoView中打开.o文件,在__TEXT中搜索tes,找到编译后test函数地址: 0x1000041D0

    image.png

发现test函数调用地址编译期运行时偏差(ASLR随机地址偏移)。

ASLR:随机地址偏移

  • 保证APP的数据安全,每次APP启动时,都会随机生成一个地址偏移值
    运行时查找所有符号,必须在编译期确定的符号地址上,加上随机生成ASLR偏移值,才是运行时正确的符号地址

验证

  • 公式:

    1. ASLR随机偏移值 = 运行时基地址 - 编译期基地址
    2. 运行时函数地址 = 编译期函数地址 + ASLR随机偏移值
  • 获取信息:(每次APP启动,运行时的数据都会变)
    程序运行断点,输入image list打印镜像文件的地址。第一个镜像文件地址就是运行时基地址
    【运行时基地址】:0x000000010897f000
    【运行时函数地址】0x1089831d0

    image.png

【编译期函数地址】0x1000041D0

image.png

【编译期基地址】0x100000000Load Comand_TEXT中找到VM Address

image.png

【计算】:

  1. ASLR偏移值 = 【运行时基地址】:0x000000010897f000 - 【编译期基地址】0x100000000 = 0x0x000000000897f000

    image.png

  2. 运行时函数地址 = 【编译期函数地址】0x1000041D0 + ASLR偏移值0x0x000000000897f000 = 0x00000001089831d0

    image.png

与我们打印的【运行时函数地址】0x1089831d0 一抹抹一样样。完美!!

  • 至此。我相信你对ASLR随机偏移值静态寻址都十分熟悉了。

你可能感兴趣的:(swift进阶四:懒加载 & 单例 & Struct)