美文网首页Swiftswift进阶
swift进阶四:懒加载 & 单例 & Struct

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

作者: markhetao | 来源:发表于2020-12-10 01:54 被阅读0次

    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: 可选类型。本质是枚举值类型
      包含some<Int>none两个枚举类型。其中none0x0。打印
      image.png
    • 其中:none1字节some<Int>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:
      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

        本文链接:https://www.haomeiwen.com/subject/twmfgktx.html