随着Swift的日渐老练和给开发进程带来的便利性及安全性,京喜App中的原生业务模块和基础模块运用Swift开发占比逐步增高。本次评论的是struct比照Class的一些优劣势,重点剖析对包体积带来的影响及躲避措施。

一、基础知识

1、类型比照

Swift之struct二进制大小分析
引证类型: 将一个目标赋值给另一个目标时,系统不会对此目标进行仿制,而会将指向这个目标的指针赋值给另一个目标,当修正其间一个目标的值时,另一个目标的值会随之改动。【Class】

值类型: 将一个目标赋值给另一个目标时,会对此目标进行仿制,仿制出一份副本给另一个目标,在修正其间一个目标的值时,不影响别的一个目标。【structs、Tuples、enums】。Swift中的【Array, String, and Dictionary】

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

2、Swift中struct和Class区别

1class是引证类型、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、重复的仿制。

Swift之struct二进制大小分析

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

1Copy-on-Write 是一种用来优化占用内存大的值类型的仿制操作的机制
2关于Int,Double,String 等根本类型的值类型,它们在赋值的时分就会产生仿制(内存添加)
3关于 ArrayDictionarySet 类型,当它们赋值的时分不会产生仿制,只要在修正的之后才会产生仿制(内存按需延时添加)
4关于自定义的数据类型不会主动完成COW,可按需完成

那么自定义的数据怎么完成COW呢,能够参阅官方代码:

/*
咱们运用class,这是一个引证类型,由于当咱们将引证类型分配给另一个时,两个变量将同享同一个实例,而不是像值类型相同仿制它。
*/
final class Ref<T> {
  var val : T
  init(_ v : T) {val = v}
}
/*
创建一个struct包装Ref:
由于struct是一个值类型,当咱们将它分配给另一个变量时,它的值被仿制,而属性ref的实例仍由两个副本同享,由于它是一个引证类型。
然后,咱们第一次更改两个Box变量的值时,咱们创建了一个新的ref实例,这要归功于:isUniquelyReferencedNonObjC
这样,两个Box变量不再同享相同的ref实例。
*/
struct Box<T> {
    var ref : Ref<T>
    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 文件比照:

Swift之struct二进制大小分析

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

1class特有的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 文件比照:

Swift之struct二进制大小分析

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

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 操作,猜想分别对 变量名称和类型的两次操作:

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

关于的添加曲线图为:

Swift之struct二进制大小分析

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

编译报错的效果如下:

Swift之struct二进制大小分析

规矩也暂时发现的两个问题:

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