乐趣区

关于swift:Swift之struct二进制大小分析

作者:京东批发 邓立兵

随着 Swift 的日渐成熟和给开发过程带来的便利性及安全性,京喜 App 中的原生业务模块和根底模块应用 Swift 开发占比逐步增高。本次探讨的是 struct 比照 Class 的一些优劣势,重点剖析对包体积带来的影响及躲避措施。

一、基础知识

1、类型比照

援用类型:将一个对象赋值给另一个对象时,零碎不会对此对象进行拷贝,而会将指向这个对象的指针赋值给另一个对象,当批改其中一个对象的值时,另一个对象的值会随之扭转。【Class】

值类型:将一个对象赋值给另一个对象时,会对此对象进行拷贝,复制出一份正本给另一个对象,在批改其中一个对象的值时,不影响另外一个对象。【structs、Tuples、enums】。Swift 中的【Array, String, and Dictionary】

两者的区别能够查阅 Apple 官网文档

2、Swift 中 struct 和 Class 区别

1、class 是援用类型、struct 是值类型
2、类容许被继承,构造体不容许被继承
3、类中的每一个成员变量都必须被初始化,否则编译器会报错,而构造体不须要,编译器会主动帮咱们生成 init 函数,给变量赋一个默认值
4、当你须要继承 Objective- C 某些类的的时候应用 class
5、class 申明的办法批改属性不须要 `mutating` 关键字;struct 须要
6、如果须要保证数据的唯一性,或者保障在多线程数据安全,能够应用 struct;而心愿创立共享的、可变的状态应用 class

以上三点能够参考深刻了解 Swift 中的 Class 和 Struct 进行更多细节的浏览学习

二、struct 优选

孔子曰:择其善者而从之,其不善者而改之。

1、安全性

应用 struct 是值类型,在传递值的时候它会进行值的 copy,所以在多线程是平安的。无论你从哪个线程去拜访你的 Struct,都非常简单。

2、效率性

struct 存储在 stack 中(这比 malloc/free 调用的性能要高得多),class 存储在 heap 中,struct 更快。

3、内存泄露

没有援用计数器,所以不会因为循环援用导致内存透露

基于这些因素,在日常开发中,咱们能用 struct 的咱们尽量应用struct

三、struct 的不完满

孟子曰:鱼,我所欲也,熊掌亦我所欲也;二者不可得兼。

“熊掌”再好,吃多了也难以消化。特地在中大型项目中,如果没有节制的应用struct,可能会带来意想不到的问题。

1、内存问题

值类型 有哪些问题?比方在两个 struct 赋值操作时,可能会发现如下问题:

1、内存中可能存在两个微小的数组;2、两个数组数据是一样的;3、反复的复制。

解决方案:COW(copy-on-write) 机制

1、Copy-on-Write 是一种用来优化占用内存大的值类型的拷贝操作的机制。2、对于 Int,Double,String 等根本类型的值类型,它们在赋值的时候就会产生拷贝。(内存减少)3、对于 Array、Dictionary、Set 类型,当它们赋值的时候不会产生拷贝,只有在批改的之后才会产生拷贝。(内存按需延时减少)4、对于自定义的数据类型不会主动实现 COW,可按需实现。

那么自定义的数据如何实现 COW 呢,能够参考官网代码:

/*
咱们应用 class,这是一个援用类型,因为当咱们将援用类型调配给另一个时,两个变量将共享同一个实例,而不是像值类型一样复制它。*/
final class Ref {
  var val : T
  init(_ v : T) {val = v}
}

/*
创立一个 struct 包装 Ref:因为 struct 是一个值类型,当咱们将它调配给另一个变量时,它的值被复制,而属性 ref 的实例仍由两个正本共享,因为它是一个援用类型。而后,咱们第一次更改两个 Box 变量的值时,咱们创立了一个新的 ref 实例,这要归功于:isUniquelyReferencedNonObjC
这样,两个 Box 变量不再共享雷同的 ref 实例。*/
struct Box {
    var ref : Ref
    init(_ x : T) {ref = Ref(x) }

    var value: T {get { return ref.val}
        set {
          //  isKnownUniquelyReferenced 函数来查看某个引 用只有一个持有者
          // 如果你将一个 Swift 类的实例传递给这个函数,并且没有其余变量强援用 这个对象的话,函数将返回 true。如果还有其余的强援用,则返回 false。不过,对于 Objective-C 的类,它会间接返回 false。if (!isUniquelyReferencedNonObjC(&ref)) {ref = Ref(newValue)
            return
          }
          ref.val = newValue
        }
    }
}
// This code was an example taken from the swift repo doc file OptimizationTips 
// Link: https://github.com/apple/swift/blob/master/docs/OptimizationTips.rst#advice-use-copy-on-write-semantics-for-large-values

实例阐明:咱们想在一个应用 struct 类型的 User 中应用 copy-on-write 的:

struct User {var identifier = 1}

let user = User()
let box = Box(value: user)
var box2 = box                  // box2 shares instance of box.ref.value

box2.value.identifier = 2             // 在扭转的时候拷贝 box2.value=2    box.value=1


// 打印内存地址
func address(of object: UnsafeRawPointer) {let addr = Int(bitPattern: object)
    print(NSString(format: "%p", addr))
}

留神这个机制 缩小的是内存的减少 ,以上能够参考写更好的 Swift 代码:COW(Copy-On-Write) 进行更多细节的浏览学习。

2、二进制体积问题

这是一个动向不到的点。发现这个问题的契机是 何骁 同学在对京喜我的项目进行瘦身的时候发现,在梳理我的项目中各个模块的大小发现商详模块的包体积会比其余模块要大很多。排除该模块业务代码多之外,通过对 linkmap 文件计算发现,有两个 struct 模型体积大的异样显著:

struct 类型库名 二进制大小
PGDomainModel.o 507 KB

通过简略的将两个对象,改成 class 类型后的二进制大小为:

class 类型库名 二进制大小
PGDomainModel.o 256 KB

这两个对象会存在在不同类中进行传递,依据 值类型 的个性,减少也只是内存的大小,而不是二进制的大小。那么问题就来了:

2.1、大小比照

答复该问题之前,先通过查阅材料发现,在 C 语言static stuct占用的二进制体积确实会大些,次要是因为 static stuctzero-initialized or uninitialized, 也就是说它在初始化不是空的。它们会进入数据段,也就是说,即便在初始化 struct 的一个字段,二进制文件也蕴含了整个构造的残缺 imageSwift 可能也相似。具体能够查问:Why does usage of structs increase application’s binary size?

通过代码实际:

class HDClassDemo {var locShopName: String?}
struct HDStructDemo {var locShopName: String?}

编译后计算 linkmap 的体积别离为:

1.54K HDClassDemo.o
1.48K HDStructDemo.o

并没有得出 struct 会比 class 大的体现,通过 Hopper Disassembler 查看 .o 文件比照:

发现有到处值得注意的点:

1、class 特有的 KVO 个性,想比照 struct 会有体积的减少;2、同样的 getter/setter/modify 办法,class 减少的体积也多一些,猜想有可能是 class 类型会有更多的逻辑判断;3、init 办法中,struct 减少体积较多,应该是 struct 初始化的时候,给变量赋一个默认值的起因;4、struct 中的 "getEnumTagSinglePayload value" 和 "storeEnumTagSinglePayload value" 占用较大的,然而通过 linkmap 计算,这两局部应该没有被最终在包体积中。通过浏览 https://juejin.cn/post/7094944164852269069 这两个字段是为 Any 类型服务,下面的例子不波及
struct ValueWitnessTable {
    var initializeBufferWithCopyOfBuffer: UnsafeRawPointer
    var destroy: UnsafeRawPointer
    var initializeWithCopy: UnsafeRawPointer
    var assignWithCopy: UnsafeRawPointer
    var initializeWithTake: UnsafeRawPointer
    var assignWithTake: UnsafeRawPointer
    var getEnumTagSinglePayload: UnsafeRawPointer
    var storeEnumTagSinglePayload: UnsafeRawPointer
    var size: Int
    var stride: Int
    var flags: UInt32
    var extraInhabitantCount: UInt32
}

所以论断是下面的写法,struct并没有体现比 class 体积大。可能是 Apple 在前面曾经优化解决掉了。

然而 ,测试验证过程中发现另外一个奇异的中央,当应用let 润饰变量时

class HDClassDemo {let locShopName: String? = nil}
struct HDStructDemo {let locShopName: String?}

编译后计算 linkmap 的体积别离为:

1.25K    HDStructDemo.o
0.94K    HDClassDemo.o

通过 Hopper Disassembler 查看 .o 文件比照:

在这种状况下,有两个论断

1、letvar 的二进制大小会小,缩小局部次要是在 setter/modifykvo字段中。所以开发过程中养成好习惯,非必要不应用 var 润饰

2、在一个或者多个 let 润饰的状况下,struct二进制大小确实是大于class

最初,如果 struct 对象通过赋值操作传递给其余类(OtherObject),比方这样(我的项目中常常存在)

let sd = HDStructDemo()
OtherObject().sdAction(sd: sd)

class OtherObject: NSObject {
    private var sd: HDStructDemo?
    func sdAction(sd: HDStructDemo) {
        self.sd = sd
        print(sd)
    }
}

在其余类 (OtherObject) 中的二进制中有多个内存地址的存储和读取端,一个变量会有两次 ldurstr 操作,猜想别离对 变量名称和类型的两次操作(下图是 7 个变量时的读写操作):

00000000000003c0         ldur       x4, [x29, var_F0]
00000000000003c4         str        x4, [sp, #0x230 + var_228]
00000000000003c8         ldur       x3, [x29, var_E8]
00000000000003cc         str        x3, [sp, #0x230 + var_220]
00000000000003d0         ldur       x2, [x29, var_E0]
00000000000003d4         str        x2, [sp, #0x230 + var_218]
00000000000003d8         ldur       x1, [x29, var_D8]
00000000000003dc         str        x1, [sp, #0x230 + var_210]
00000000000003e0         ldur       x17, [x29, var_D0]
00000000000003e4         str        x17, [sp, #0x230 + var_208]
00000000000003e8         ldur       x16, [x29, var_C8]
00000000000003ec         str        x16, [sp, #0x230 + var_200]
00000000000003f0         ldur       x15, [x29, var_C0]
00000000000003f4         str        x15, [sp, #0x230 + var_1F8]
00000000000003f8         ldur       x14, [x29, var_B8]
00000000000003fc         str        x14, [sp, #0x230 + var_1F0]
0000000000000400         ldur       x13, [x29, var_B0]
0000000000000404         str        x13, [sp, #0x230 + var_1E8]
0000000000000408         ldur       x12, [x29, var_A8]
000000000000040c         str        x12, [sp, #0x230 + var_1E0]
0000000000000410         ldur       x11, [x29, var_A0]
0000000000000414         str        x11, [sp, #0x230 + var_1D8]
0000000000000418         ldur       x10, [x29, var_98]
000000000000041c         str        x10, [sp, #0x230 + var_1D0]
0000000000000420         ldur       x9, [x29, var_90]
0000000000000424         str        x9, [sp, #0x230 + var_1C8]
0000000000000428         ldur       x8, [x29, var_88]
000000000000042c         str        x8, [sp, #0x230 + var_1C0]

这将势必对整个 App 的包体积带来微小的增量。肯定肯定肯定要联合我的项目进行正当的抉择。

2.2、如何取舍

在平安、效率、内存、二进制大小多个方面,如何获得均衡是要害。

单从二进制大小作为考量,这里有一些经验总结能够提供参考:

1、如果变量都是 let 润饰,class 远胜于 struct,变量越多,劣势越大;7 个变量的状况下大小别离为:

3.12K    HDStructDemo.o
1.92K    HDClassDemo.o

2、如果变量都是 var 润饰,struct 远胜于 class,变量越多,劣势越大:

 1 个变量:1.54K    HDClassDemo.o
1.48K    HDStructDemo.o

60 个变量:44.21K    HDClassDemo.o
24.22K    HDStructDemo.o

100 个变量:71.74K    HDClassDemo.o
38.98K    HDStructDemo.o

3、如果变量都是 var 润饰,然而都遵循 Decodable 协定,这里又有乾坤:

这种状况有可能在我的项目中存在,并且法则不是简略的谁大谁小,而是依据变量的不同,出现不同的规定:

应用脚本疾速创立别离蕴含 1 -200 个变量的 200 个文件

fileCount=200
for ((i = 0; i < $fileCount; i++)); do
    className="HDClassObj_${i}"
    classFile="${className}.swift"
    structName="HDStructObj_${i}"
    structFile="${structName}.swift"
    classDecodableName="HDClassDecodableObj_${i}"
    classDecodableFile="${classDecodableName}.swift"
    structDecodableName="HDStructDecodableObj_${i}"
    structDecodableFile="${structDecodableName}.swift"
    echo "class ${className} {" > $classFile
    echo "struct ${structName} {" > $structFile
    echo "class ${classDecodableName}: Decodable {" > $classDecodableFile
    echo "struct ${structDecodableName}: Decodable {" > $structDecodableFile
    for ((j = 0; j < $i; j++)); do
        line="\tvar name_${j}: String?"
        echo $line >> $classFile
        echo $line >> $structFile
        echo $line >> $classDecodableFile
        echo $line >> $structDecodableFile
    done
    echo "}" >> $classFile
    echo "}" >> $structFile
    echo "}" >> $classDecodableFile
    echo "}" >> $structDecodableFile
done

失去 200 个文件后,抉择 arm64 架构编译后,剖析 linkmap 文件,失去的文件大小为:

index    Class    Struct    ClassDecodable    StructDecodable
1    0.7    0.15    3.03    2.32
2    1.53    1.48    6.54    6.37
3    2.23    1.88    8.12    7.66
4    2.94    2.31    9.37    8.65
5    3.64    2.69    10.73    9.69
6    4.34    3.08    12.05    10.66
7    5.04    3.46    13.36    11.63
8    5.74    3.84    14.62    12.62
9    6.45    4.22    14.97    13.61
10    7.15    4.62    16.11    14.9
11    7.85    5.02    17.25    15.96
12    8.55    5.42    18.39    17.06
13    9.26    5.82    19.53    18.2
14    9.96    6.22    20.67    19.36
...
...
...
76    53.61    31.09    92.19    91.91
77    54.31    31.49    93.34    93.35
...
...
...
198    139.69    79.99    234.45    329.59
199    140.4    80.39    235.58    332
200    141.11    80.79    236.72    334.43

对于的减少曲线图为:

HDStructDecodableObj 在 77 个变量下体积将返超 HDClassDecodableObj

依据曲线规定,能够得出 Class、Struct、ClassDecodable 增长是线性函数,对应的别离函数近似为:

Y = 0.825 + X * 0.705
Y = 1.0794 + X * 0.4006
Y = 5.3775 + X * 1.1625

HDClassDecodableObj的函数规定散布猜想可能是 一元二次函数(抛物线)对数函数 。在实在比照测试数据均不合乎,也可能是 分段函数 吧。有通晓的同学请告知。

四、预防策略

圣人云:不治已病治未病,不治已乱而治未乱。

京喜 从 2020 年开始陆续应用 Swift 作为业务开发的次要开发语言,特地是在 商详、直播、购物车、结算、设置 等业务曾经全量化。单单将 商详 中的 PGDomainModelPGDomainDatastruct改成 class 类型,该模块的二进制大小从 12.1M 左右缩小到5.5M,这次要是因为这两个对象自身的变量较多,并且被大量其余楼层类赋值应用导致,收益堪称是具大。其余模块收益绝对会少一些。

模块名 v5.33.6 二进制大小 v5.36.0 二进制大小 二进制增量
pgProductDetailModule 12.1 MB 5.5 MB – 6.6 MB

能够通过 SwiftLint 的自定义规定,当在 HDClassDecodableObj 状况下,超过肯定数量变量时,编译谬误来躲避相似的问题。

自定义规定如下:

custom_rules:
  disable_more_struct_variable:
    included: ".*.swift"
    name: "struct 不应蕴含超过 10 个的变量"
    regex: "^(struct).*(Decodable).*(((\n)*\\s(var).*){10,})"
    message: "struct 不应蕴含超过 10 个的变量"
    severity: error

编译报错的成果如下:

规定也临时发现的两个问题:

1、regex 次数问题

实践上的数量应该是 77 个才告警,然而配置数量超过 15 在编译过程就会十分慢,在正则在正则可视化页面运行稳固,然而应用 SwiftLint 却简直卡死,问题暂未找到解决方案。可能须要浏览 SwiftLint 源码求助。

2、识别率问题

因为是依据 var 的次数进行匹配,一旦呈现正文(//)统计也会误差。正则过于简单,临时也没有找到解决方案。

本文波及到的代码、脚本、工具、数据都开源寄存在 HDSwiftStructSizeDemo,文件构造阐明如下:

.
├── Asserts # 图片资源
├── README.md
└── Struct 比照
    ├── HDSwiftCOWDemo # 测试 struct 和 class 大小的工程(代码)│   ├── HDSwiftCOWDemo    
    │   └── HDSwiftCOWDemo.xcodeproj
    ├── LinkMap # 革新后的 LinkMap 源码,反对二进制升 / 降排序序(工具)│   ├── LinkMap
    │   ├── LinkMap.xcodeproj
    │   ├── README.md
    │   ├── ScreenShot1.png
    │   └── ScreenShot2.png
    ├── StructSize.playground # playground 工程,次要验证二进制增长的函数(代码)│   ├── Contents.swift
    │   ├── contents.xcplayground
    │   └── playground.xcworkspace
    ├── Swift-Struct/Class 大小.xlsx # struct 和 class 大小数据及图表生成(数据:最终产物)└── linkmap 比照 # 记录 struct 和 class 的 linkmap 数据(数据)├── HDClassDecodableObj.txt
        ├── HDClassObj.txt
        ├── HDStructDecodableObj.txt
        ├── HDStructObj.txt
        └── LinkMap.app

欢送大家 🌟Star 🌟

五、参考资料

深刻了解 Swift 中的 Class 和 Struct

写更好的 Swift 代码:COW(Copy-On-Write)

Swift 官网 COW 文档

Understanding Swift Copy-on-Write mechanisms

swift 构造体 copy-on-write 技术

什么是 COW?

数据来测试是否实现 COW

COW 自定义实现

arm 汇编贮存指令 str stur 和读取指令 ldr ldur 的应用, 对应 xcode c++ 中的代码反汇编教程

正则可视化页面

正则表达式选集

SwiftLint

SwiftLint_Rule

SwiftLint-Advanced

退出移动版