Swift进阶之内存模型和方法调度
前言Apple今年推出了Swift3.0,较2.3来说,3.0是一次重大的升级。关于这次更新,在这里都可以找到,最主要的还是提高了Swift的性能,优化了Swift API的设计(命名)规范。 前段时间对之前写的一个项目ImageMaskTransition做了简单迁移,先保证能在3.0下正常运行,只用了不到30分钟。总的来说,这次迁移还是非常轻松的。但是,有一点要注意:3.0的API设计规范较2.3有了质变,建议做迁移的开发者先看下WWDC的Swift API Design Guidelines。后面有时间了,我有可能也会总结下。 内存分配通过查看Github上Swift的源代码语言分布 可以看到
对于C++来说,内存区间如下
Swift的内存区间和C++类似。也有存储代码和全局变量的区间,这两种区间比较简单,本文更多专注于以下两个内存区间。
栈在栈上分配和释放内存的代价是很小的,因为栈是一个简单的数据结构。通过移动栈顶的指针,就可以进行内存的创建和释放。但是,栈上创建的内存是有限的,并且往往在编译期就可以确定的。 举个很简单的例子:当一个递归函数,陷入死循环,那么最后函数调用栈会溢出。 例如,一个没有引用类型Struct的临时变量都是在栈上存储的 struct Point{
那么,这个内存结构如图 Tips: 图中的每一格都是一个Word大小,在64位处理器上,是8个字节 堆在堆上可以动态的按需分配内存,每次在堆上分配内存的时候,需要查找堆上能提供相应大小的位置,然后返回对应位置,标记指定位置大小内存被占用。 在堆上能够动态的分配所需大小的内存,但是由于每次要查找,并且要考虑到多线程之间的线程安全问题,所以性能较栈来说低很多。 比如,我们把上文的struct改成class, class PointClass{
这时候的内存结构如图 Memory Alignment(内存对齐)
和C/C++/OC类似,Swift也有Memory Alignment的概念。举个直观的例子 我们定义这样两个Struct 然后,用MemoryLayout来获取两个结构体的大小 可以看到,只不过调整了结构体中的声明顺序,其占用的内存大小就改变了,这就是内存对齐。 我们来看看,内存对齐后的内存空间分布: 内存对齐的原因是,
内存对齐的优点很多
自动引用计数(ARC)提到ARC,不得不先讲讲Swift的两种基本类型:
比如,如下代码 struct Point{ //Swift中,struct是值类型
? ?var x,y:Double
}
我们先看看对应内存的使用 值类型有很多优点,其中主要的优点有两个 - 线程安全,每次都是获得一个copy,不存在同时修改一块内存 - 不可变状态,使用值类型,不需要考虑别处的代码可能会对当前代码有影响。也就没有side effect。 ARC是相对于引用类型的。 > ARC是一个内存管理机制。当一个引用类型的对象的reference count(引用计数)为0的时候,那么这个对象会被释放掉。 我们利用XCode 8和iOS开发,来直观的查看下一个值类型变量的引用计数变化。 新建一个iOS单页面工程,语言选择Swift,然后编写如下代码 然后,当断点停在24行处的时候,Person的引用计数如下 这里,底部的 当栈上代码执行完毕,栈会断掉对Person的引用,引用计数也就减一,系统会断掉自动创建的引用。这时候,person的引用计数位0,内存释放。 方法调度(method dispatch)Swift的方法调度分为两种
Struct 对于Struct来说,方法调度是静态的。 // 8 bytes
? ?func draw(){
? ? ? ?print("Draw point at(x,y)")
? ?}
}
可以看到,由于是Static Dispatch,在编译期就能够知道方法的执行体。所以,在Runtime也就不需要额外的空间来存储方法信息。编译后,方法的调用,直接就是变量地址的传入,存在了代码区中。 如果开启了编译器优化,那么上述代码被优化成Inline后, 5.0)
print("Draw point at(point1.x,point1.y)")
print(MemoryLayout<Point>.size) //16
Class Class是Dynamic Dispatch的,所以在添加方法之后,Class本身在栈上分配的仍然是一个word。堆上,需要额外的一个word来存储Class的Type信息,在Class的Type信息中,存储着virtual table(V-Table)。根据V-Table就可以找到对应的方法执行体。 class Point{
继承 因为Class的实体会存储额外的Type信息,所以继承理解起来十分容易。子类只需要存储子类的Type信息即可。 例如 class Point3D:Point{
协议 我们首先看一段代码 struct Point:Drawable{
可以看到,输出 16 //point as Point
16和32不难理解,Point含有两个Double属性,Line含有四个Double属性。对应的字节数也是对的。那么,两个40是怎么回事呢?而且,对于Point来说,40-16=24,多出了24个字节。而对于Line来说,只多出了40-32=8个字节。 这是因为Swift对于协议类型的采用如下的内存模型 - Existential Container。 Existential Container包括以下三个部分:
那么,内存结构图,如下 范型 范型让代码支持静态多态。比如: func drawACopy<T : Drawable>(local : T) { ?local.draw() } drawACopy(Point(...)) drawACopy(Line(...)) 那么,范型在使用的时候,如何调用方法和存储值呢? 范型并不采用Existential Container,但是原理类似。
范型的编译器优化
比如 func drawACopy<T : Drawable>(local : T) { ?local.draw() } 在编译过后,实际会有两个方法 func drawACopyOfALine(local : Line) { ?local.draw() } func drawACopyOfAPoint(local : Point) { ?local.draw() } 然后 drawACopy(local: Point(x: 1.0))
会被编译成为 func drawACopyOfAPoint(local : Point(x: 1.0))
Swift的编译器优化还会做更多的事情,上述优化虽然代码变多,但是编译器还会对代码进行压缩。所以,实际上,并不会对二进制包大小有什么影响。 参考资料
了解最新移动开发相关信息和技术,请关注 mobilehub 公众微信号(ID: mobilehub)。 (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |