这是我参加11月更文应战的第2天,活动详情检查:2021最终一次更文应战

前言

Hi Coder,我是 CoderStar!

在 Swift 开发过程中,你很或许至少问过自己一次structclass之间的差异,即便你自己没问过,你的面试官应该也问过。对这个问题的答案中,或许最大的差异便是一个是值类型,而另一个是引证类型,今天咱们就来详细聊聊这个差异。

那在介绍值类型与引证类型之前,咱们仍是先来回顾一下structclass之间的差异这个问题。

class & struct

在 Swift 中,其实classstruct之间的中心差异不是很多,有很多差异是值类型与引证类型这个差异隐形带来的天然的差异。

  • class 能够承继,struct 不能承继(当然struct能够运用protocol来完成类似承继的效果。);受此影响的差异有:

    • struct中办法的派发方法全都是直接派发,而class中依据实践状况有多种派发方法,详情可看Swift 派发机制;
  • class 需求自己定义结构函数,struct 默许生成;

    struct 默许生成的结构函数有必要包括一切成员参数,只要当一切参数都为可选型时,可直接不用传入参数直接简略结构,class 中的特点有必要都有默许值,不然编译错误, 能够通过声明时赋值或者结构函数赋值两种方法给特点设置默许值。

  • class 是引证类型,struct 是值类型;受此影响的差异有:

    • struct 改动其特点受修饰符 let 影响,不可改动,class 不受影响;
    • struct 办法中需求修正自身特点时 (非 init 办法),办法需求前缀修饰符 mutating
    • struct 由于是值类型的原因,所以自动线程安全,并且也不存在循环引证导致内存走漏的风险;
    • 更多看下一章节

在 Swift 中,很多基础类型,如StringInt等等,都是运用Struct来定义。关于怎么挑选两者这个问题上,Apple 在一些官方文档中也给出了它们之间的差异以及官方主张。

  • choosing_between_structures_and_classes
  • Value and Reference Types
  • ClassesAndStructures

来自《choosing_between_structures_and_classes》

在向 app 中增加新数据类型时,您无妨考虑以下主张来协助自己做出合理的挑选。

  • 默许运用结构。
  • 在需求 Objective-C 互操作性时运用类。
  • 在需求控制建模数据的恒等性时运用类。
  • 将结构与协议调配,通过同享完成来采用行为。

值类型 & 引证类型

那在 Swift 中,值类型与引证类型之间的差异有哪些呢?

  • 存储方法及方位:大部分值类型存储在栈上,大部分引证类型存储在堆上;
  • 内存:值类型没有引证计数,也不会存在循环引证以及内存走漏等问题;
  • 线程安全:值类型天然线程安全,而引证类型需求开发者通过加锁等方法来保证;
  • 复制方法:值类型复制的是内容,而引证类型复制的是指针,从一定意义上讲便是所谓的深复制及浅复制;

在 Swift 中,值类型除了struct之外还有enumtuple,引证类型除了class之外还有closure/func

存储方法及方位

上文说的’堆’和’栈’是程序运行中的不同内存空间。

关于堆、栈存储原理,美团的这篇【基本功】深化剖析 Swift 功能优化给出了细节阐明,这儿就不再赘述了,大概说下定论。

值类型默许存储在栈区,栈区内存是接连的,通过出栈入栈进行分配和销毁,速度很快,并且每个线程都有自己的栈空间,所以不需求考虑线程安全问题;拜访存储内容时一次就能够拿到值。

引证类型,只在栈区存储了目标的指针,指针指向的目标的内存是分配在堆区的。堆在分配和开释时都要调用函数(MALLOC,FREE) 动态请求 / 开释内存,这些都会花费一些时间,并且由于堆空间被一切线程同享,所以在运用时要考虑线程安全。拜访存储内容时,需求两次拜访内存,第一次得获得指针,第2次才是真实的数据。

其中在 64 位体系上,iOS 加入了Tagged Pointer优化方法,即直接在指针中存储值,比如NSNumber以及NSString结构。

从描绘来看,咱们得到的最重要的定论是运用值类型比运用引证类型更快,详细技术指标可检查why-choose-struct-over-class,还有一个测试项目StructVsClassPerformance。

通过上面的描绘,咱们能够有一个问题,便是一切的class都存储在堆上,一切的struct都存储在栈上吗?这也是本篇文章的重点。其实关于绝大多数状况而言,这种说法都是没问题的,可是总会有些特殊状况。

在阅读下文之前,咱们先看一下,怎么判别目标是在栈分配仍是在堆分配。关于这个问题咱们能够在SIL.rst中找到答案。Swift 编译生成的 SIL 文件中,会包括派发指令,与内存分配相关的指令中,有alloc-stack和alloc-box指令能够来协助咱们解决这个问题,简略来说前者便是来栈上分类内存的指令,而后者便是在堆上分配任务的指令。

栈上的引证类型

仓库上的分配和开释本钱远低于堆上的分配和开释,因此有时编译器或许会提升引证类型也存储在仓库上,这个过程实践产生在 SIL 优化阶段,官方术语叫做Memory promotion。关于这一说法,咱们能够在Guaranteed Optimization and Diagnostic Passes找到支撑。

Memory promotion is implemented as two optimization phases, the first of which performs capture analysis to promote alloc_box instructions to alloc_stack, and the second of which promotes non-address-exposed alloc_stack instructions to SSA registers.

大致意思是便是 SIL 阶段会尽量进行内存提升,将本来堆内存提升为栈内存,栈内存提升为 SSA 寄存器内存。

详细优化部分代码咱们能够在AllocBoxToStack.cpp中看到。

堆上的值类型

在《Swift 进阶》书中有过这么一段话,(在 3.0 版别中呈现,5.0 版别删去掉了):

Swift 的结构体一般被存储在栈上,而非堆上。不过这其实是一种优化: 默许状况下结构体是存储在堆上的,可是在绝大多数时分,这个优化会收效,并将结构体存储到栈上。当结构体变量被一个函数闭合的时分,优化将不再收效,此时这个结构体将存储在堆上。

看到这句话有些同学会有点摸不着头脑,为什么默许状况结构领会存在堆上,然后通过优化时分才存储到栈上。下面咱们来看struct编译生成的相关 SIL 文件。

struct Test {}

这是一个十分简略的struct结构体,简略到连特点都没了,咱们运用swiftc指令生成 SIL 文件,指令如下:

swiftc Test.swift -emit-silgen | xcrun swift-demangle > TestSILGen.sil

其意义便是生成Raw SIL,也便是原生 SIL 文件,没有通过任何优化和处理。更多指令能够看之前输出的一篇文章iOS 编译简析。

生成的 SIL 文件内容如下:

sil_stage raw
import Builtin
import Swift
import SwiftShims
struct Test {
  init()
}
// main
sil [ossa] @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 {
bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>):
  %2 = integer_literal $Builtin.Int32, 0          // user: %3
  %3 = struct $Int32 (%2 : $Builtin.Int32)        // user: %4
  return %3 : $Int32                              // id: %4
} // end sil function 'main'
// Test.init()
sil hidden [ossa] @$s4main4TestVACycfC : $@convention(method) (@thin Test.Type) -> Test {
// %0 "$metatype"
bb0(%0 : $@thin Test.Type):
  %1 = alloc_box ${ var Test }, let, name "self"  // user: %2
  %2 = mark_uninitialized [rootself] %1 : ${ var Test } // users: %5, %3
  %3 = project_box %2 : ${ var Test }, 0          // user: %4
  %4 = load [trivial] %3 : $*Test                 // user: %6
  destroy_value %2 : ${ var Test }                // id: %5
  return %4 : $Test                               // id: %6
} // end sil function '$s4main4TestVACycfC'

咱们能够很明显的看到alloc_box字眼。

然后咱们再运用生成优化后 SIL 文件的指令,如下:

swiftc Test.swift -emit-sil | xcrun swift-demangle > TestSIL.sil

sil_stage canonical
import Builtin
import Swift
import SwiftShims
struct Test {
  init()
}
// main
sil @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 {
bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>):
  %2 = integer_literal $Builtin.Int32, 0          // user: %3
  %3 = struct $Int32 (%2 : $Builtin.Int32)        // user: %4
  return %3 : $Int32                              // id: %4
} // end sil function 'main'
// Test.init()
sil hidden @$s4main4TestVACycfC : $@convention(method) (@thin Test.Type) -> Test {
// %0 "$metatype"
bb0(%0 : $@thin Test.Type):
  %1 = alloc_stack $Test, let, name "self"        // user: %3
  %2 = struct $Test ()                            // user: %4
  dealloc_stack %1 : $*Test                       // id: %3
  return %2 : $Test                               // id: %4
} // end sil function '$s4main4TestVACycfC'

咱们很明显看到alloc_stack字眼。

相信我们已经明白产生了什么,struct 在生成原始的 SIL 文件中实践上会运用堆指令,然后在 SIL 优化阶段会依据代码上下文环境判别是否能够优化到栈上继而对指令进行修正。那大部分状况下是都能够优化到栈上的。这个过程就有上述AllocBoxToStack.cpp文件的参加。

当然,那必定还有别的的少部分状况。比如说:

func uniqueIntegerProvider() -> () -> Int {
    // i是Int类型,本质也是一个结构体
    var i = 0
    return {
        i+=1
        return i
    }
}

对此代码生成的两份 SIL 文件,中心部分如下:

优化前:

// uniqueIntegerProvider()
sil hidden [ossa] @main.uniqueIntegerProvider() -> () -> Swift.Int : $@convention(thin) () -> @owned @callee_guaranteed () -> Int {
bb0:
  %0 = alloc_box ${ var Int }, var, name "i"      // users: %11, %8, %1
  %1 = project_box %0 : ${ var Int }, 0           // users: %9, %6
  %2 = integer_literal $Builtin.IntLiteral, 0     // user: %5
  %3 = metatype $@thin Int.Type                   // user: %5
  // function_ref Int.init(_builtinIntegerLiteral:)
  %4 = function_ref @Swift.Int.init(_builtinIntegerLiteral: Builtin.IntLiteral) -> Swift.Int : $@convention(method) (Builtin.IntLiteral, @thin Int.Type) -> Int // user: %5
  %5 = apply %4(%2, %3) : $@convention(method) (Builtin.IntLiteral, @thin Int.Type) -> Int // user: %6
  store %5 to [trivial] %1 : $*Int                // id: %6
  // function_ref closure #1 in uniqueIntegerProvider()
  %7 = function_ref @closure #1 () -> Swift.Int in main.uniqueIntegerProvider() -> () -> Swift.Int : $@convention(thin) (@guaranteed { var Int }) -> Int // user: %10
  %8 = copy_value %0 : ${ var Int }               // user: %10
  mark_function_escape %1 : $*Int                 // id: %9
  %10 = partial_apply [callee_guaranteed] %7(%8) : $@convention(thin) (@guaranteed { var Int }) -> Int // user: %12
  destroy_value %0 : ${ var Int }                 // id: %11
  return %10 : $@callee_guaranteed () -> Int      // id: %12
} // end sil function 'main.uniqueIntegerProvider() -> () -> Swift.Int'

优化后:

// uniqueIntegerProvider()
sil hidden @main.uniqueIntegerProvider() -> () -> Swift.Int : $@convention(thin) () -> @owned @callee_guaranteed () -> Int {
bb0:
  %0 = alloc_box ${ var Int }, var, name "i"      // users: %8, %7, %6, %1
  %1 = project_box %0 : ${ var Int }, 0           // user: %4
  %2 = integer_literal $Builtin.Int64, 0          // user: %3
  %3 = struct $Int (%2 : $Builtin.Int64)          // user: %4
  store %3 to %1 : $*Int                          // id: %4
  // function_ref closure #1 in uniqueIntegerProvider()
  %5 = function_ref @closure #1 () -> Swift.Int in main.uniqueIntegerProvider() -> () -> Swift.Int : $@convention(thin) (@guaranteed { var Int }) -> Int // user: %7
  strong_retain %0 : ${ var Int }                 // id: %6
  %7 = partial_apply [callee_guaranteed] %5(%0) : $@convention(thin) (@guaranteed { var Int }) -> Int // user: %9
  strong_release %0 : ${ var Int }                // id: %8
  return %7 : $@callee_guaranteed () -> Int       // id: %9
} // end sil function 'main.uniqueIntegerProvider() -> () -> Swift.Int'

能够很明显的看出,无论是优化前仍是优化后,运用的都是alloc_box指令,也便是说此时的变量i是存储在堆上的。其实原因也很好了解,其实便是变量 i 被函数闭合了,即便在退出效果域的状况下,依然得坚持 i 的存在。当然这仅仅一种状况,还会有其他的状况。

总结:所以说在 Swift 中一切的class都存储在堆上,一切的struct都存储在栈上这种说法是有问题的,只能说大部分状况是如此的,总有些状况会跟你顽皮,详细存储方位还得结合结构所在上下文以及 SIL 优化手段等等要素归纳剖析。

复制方法

引证类型,在复制时,实践上复制的仅仅栈区存储的目标的指针;值类型复制的是实践的值。

关于值类型复制,Swift 有一套 写时复制 COW(Copy-On-Write) 优化机制,即只要赋值后值类型产生改动的时分才会进行真实的复制,当没有改动时,两者同享同一个内存地址。

Apple 在 OptimizationTips 中,给出了一个示例,代码很简略,相信我们一下就能明白。

该文档中还有一些 Apple 给出的别的的优化方法,比如减少动态派发的方法等等,主张 enjoy。

final class Ref<T> {
  var val: T
  init(_ v: T) {val = v}
}
struct Box<T> {
    var ref: Ref<T>
    init(_ x: T) { ref = Ref(x) }
    var value: T {
        get { return ref.val }
        set {
          /// 判别当时目标是否只要一个引证,假如不是才进行复制
          if !isKnownUniquelyReferenced(&ref) {
            ref = Ref(newValue)
            return
          }
          ref.val = newValue
        }
    }
}

Swift 规范库中,StringArrayDictionarySet 等默许完成了COW,关于自定义目标,咱们需求自己完成。

最终

在编写本地文章过程中,检查了 Swift 开源仓库 docs 目录下的一些文档,学到了很多,也主张各位读者同学 enjoy!

要更加尽力呀!

Let’s be CoderStar!

更多资料

  • iOS Swift5:浅析结构体(struct)与类(class)
  • Why Choose Struct Over Class?
  • Memory Management and Performance of Value Types
  • Value Types and Reference Types in Swift
  • why-choose-struct-over-class
  • reference-vs-value-types-in-swift

有一个技术的圈子与一群同道世人十分重要,来我的技术大众号,这儿只聊技术干货。

微信大众号:CoderStar