作者:京东批发 邓立兵
随着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某些类的的时候应用class5、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.valuebox2.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 stuct
是zero-initialized or uninitialized
, 也就是说它在初始化不是空的。它们会进入数据段,也就是说,即便在初始化struct
的一个字段,二进制文件也蕴含了整个构造的残缺image
。Swift
可能也相似。具体能够查问:Why does usage of structs increase application's binary size?
通过代码实际:
class HDClassDemo { var locShopName: String?}struct HDStructDemo { var locShopName: String?}
编译后计算linkmap
的体积别离为:
1.54K HDClassDemo.o1.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.o0.94K HDClassDemo.o
通过Hopper Disassembler
查看.o
文件比照:
在这种状况下,有两个论断
1、let
比var
的二进制大小会小,缩小局部次要是在setter/modify
和kvo
字段中。所以开发过程中养成好习惯,非必要不应用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
)中的二进制中有多个内存地址的存储和读取端,一个变量会有两次ldur
、str
操作,猜想别离对 变量名称和类型的两次操作(下图是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.o1.92K HDClassDemo.o
2、如果变量都是var润饰,struct 远胜于 class,变量越多,劣势越大:
1个变量:1.54K HDClassDemo.o1.48K HDStructDemo.o60个变量:44.21K HDClassDemo.o24.22K HDStructDemo.o100个变量:71.74K HDClassDemo.o38.98K HDStructDemo.o
3、如果变量都是var润饰,然而都遵循 Decodable 协定,这里又有乾坤:
这种状况有可能在我的项目中存在,并且法则不是简略的谁大谁小,而是依据变量的不同,出现不同的规定:
应用脚本疾速创立别离蕴含1-200个变量的200个文件
fileCount=200for (( 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 "}" >> $structDecodableFiledone
失去200个文件后,抉择arm64
架构编译后,剖析linkmap
文件,失去的文件大小为:
index Class Struct ClassDecodable StructDecodable1 0.7 0.15 3.03 2.322 1.53 1.48 6.54 6.373 2.23 1.88 8.12 7.664 2.94 2.31 9.37 8.655 3.64 2.69 10.73 9.696 4.34 3.08 12.05 10.667 5.04 3.46 13.36 11.638 5.74 3.84 14.62 12.629 6.45 4.22 14.97 13.6110 7.15 4.62 16.11 14.911 7.85 5.02 17.25 15.9612 8.55 5.42 18.39 17.0613 9.26 5.82 19.53 18.214 9.96 6.22 20.67 19.36.........76 53.61 31.09 92.19 91.9177 54.31 31.49 93.34 93.35.........198 139.69 79.99 234.45 329.59199 140.4 80.39 235.58 332200 141.11 80.79 236.72 334.43
对于的减少曲线图为:
HDStructDecodableObj在77个变量下体积将返超HDClassDecodableObj
依据曲线规定,能够得出Class、Struct、ClassDecodable增长是线性函数,对应的别离函数近似为:
Y = 0.825 + X * 0.705Y = 1.0794 + X * 0.4006Y = 5.3775 + X * 1.1625
HDClassDecodableObj的函数规定散布猜想可能是一元二次函数(抛物线)
、对数函数
。在实在比照测试数据均不合乎,也可能是分段函数
吧。有通晓的同学请告知。
四、预防策略
圣人云:不治已病治未病,不治已乱而治未乱。
京喜
从2020年开始陆续应用Swift
作为业务开发的次要开发语言,特地是在商详、直播、购物车、结算、设置
等业务曾经全量化。单单将商详
中的PGDomainModel
、PGDomainData
从struct
改成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