前不久看了一篇文章,喵神的值类型和引用类型,在阅读的时候有一个结论 值类型被复制的时机是值类型的内容发生改变时...
这个时候原本是想记下来的,后来转念一想,实践出真知,因此我就基于这个问题: 值类型究竟是何时被赋值的? 作了一些调查和实践,从而有了这系列文章...git
我在iOS Playground中写了以下示例,初始化了Int String Struct Array
而且马上进行了赋值操做:github
struct Me { let age: Int = 22 // 8 let height: Double = 180.0 // 8 let name: String = "XiangHui"// 24 var hasGirlFriend: Bool? // 1 } var a = 134 var cpa = a var b = "JoJo" var cpb = b var me = Me() var secondMe = me var likes = ["comdy", "animation", "movies"] var cpLikes = likes 复制代码
而且随后使用一个swift指针方法来输出值类型在内存中的地址:swift
withUnsafeBytes(of: &T, { bytes in print("T: \(bytes)") }) 复制代码
那么其实咱们能够猜想一下,若是是在值类型发生改变的时候才去赋值的话(写时复制),那么以上复制的变量的地址应该和原变量是同样的,结果以下:数组
a: UnsafeRawBufferPointer(start: 0x00007ffee3500ef8, count: 8) cpa: UnsafeRawBufferPointer(start: 0x00007ffee3500f00, count: 8) b: UnsafeRawBufferPointer(start: 0x00007ffee3500f18, count: 24) cpb: UnsafeRawBufferPointer(start: 0x00007ffee3500ee0, count: 24) me: UnsafeRawBufferPointer(start: 0x00007ffee3500fa8, count: 41) secondMe: UnsafeRawBufferPointer(start: 0x00007ffee3500f40, count: 41) likes: UnsafeRawBufferPointer(start: 0x00007ffee3500f30, count: 8) cpliles: UnsafeRawBufferPointer(start: 0x00007ffee3500f08, count: 8) 复制代码
显然,值类型的值并不是是在改变的时候才去复制,而是在赋值的时候就会进行复制! 这个结论显然是有问题的! 若是把上面的每一种类型拆开的话能够获得的结论大概是Int,Double, String, Struct等)是在赋值的时候复制的,为何?由于对于基本类型来说写时复制带来的开销其实有时比直接复制带来的开销更大!而对于集合类型来说,固然上面个人实例是数组,它直接复制的只是一个引用而已,集合类型(Array,Dictionary,Set)并不是是在赋值时复制的,而是在写时复制的!缓存
根据喵神的指导,我使用了如下方式来输出数组的地址:bash
func address<T: AnyObject>(of object: T) -> String { let addr = unsafeBitCast(object, to: Int.self) return String(format: "%p", addr) } func address(of object: UnsafeRawPointer) -> String { let addr = Int(bitPattern: object) return String(format: "%p", addr) } var likes = ["animation", "movies", "comdy"] var cpLikes = likes print("Array") print(address(of: &likes)) print(address(of: &cpLikes)) cpLikes.removeLast() print(address(of: &cpLikes)) 复制代码
最后输出的是:markdown
Array
0x6080000d4370
0x6080000d4370
0x6080000d5480
复制代码
分析:前两次输出的起始地址是同样的,因此在赋值的时候值并无发生变化,可是在移除cplikes最后一个元素时,数组的地址就发生了变化,因此能够得出的结论是数组是写时复制的!ide
如下是喵神的原话:oop
当这个问题解决以后又不由有了新的疑问:post
针对个人这三个简单可是宽泛的问题,我作了大量的阅读和实践,而后有了下面的一些思考和总结:
在进行更抽象的内存理论以前,得了解几个基本的概念,首先是可操做内存区域,在程序中咱们使用的内存区域就是图中的绿色区域:
在这块区域中咱们能够简要的分为三个区域堆,栈,全局区。在现代的CPU每次读取数据的时候,都会读取一个word,在64位上,也就是8个字节。
这样一看其实有一点豁然开朗的感受,其实基本只有方法或者特定类型如结构体中出现的变量才是局部变量,也就是说在方法中声明的变量都是分配在栈上的,然而在类中声明一个基本类型做为对象属性,实际上是在堆上分配的
class Test { let a = 4 // 分配在堆上 func printMyName() { let myName = "JoJo" // 分配在栈上 print("\(myName)") } } 复制代码
//值类型 MemoryLayout<Int>.size //8 MemoryLayout<Int>.alignment //8 MemoryLayout<Int>.stride //8 MemoryLayout<String>.size //24 MemoryLayout<String>.alignment //8 MemoryLayout<String>.stride //24 //引用类型 T MemoryLayout<T>.size //8 MemoryLayout<T>.alignment //8 MemoryLayout<T>.stride //8 //指针类型 MemoryLayout<unsafeMutablePointer<T>>.size //8 MemoryLayout<unsafeMutablePointer<T>>.alignment //8 MemoryLayout<unsafeMutablePointer<T>>.stride //8 MemoryLayout<unsafeMutableBufferPointer<T>>.size //16 MemoryLayout<unsafeMutableBufferPointer<T>>.alignment //16 MemoryLayout<unsafeMutableBufferPointer<T>>.stride //16 复制代码
MemoryLayout<Type>
是一个泛型,经过它的三个属性能够获取具体类型在内存中的分配:size
代表该类型实际使用了多少个字节;alignment
代表该类型必须对齐多少字节(如为8,意味着地址的起点地址能够被8整除);stride
代表从开始到结束一共须要占据多少字节。 Swift中基本类型的size和stride在内存中是同样的 (可选型如Double?
实际使用了9个字节,可是却须要占据16个字节) 内存对齐的好处这里针对内存对齐的好处有了比较详尽的描述,主要是速度快。
从一个栈的实例来看栈中内存的分配状况:
struct Me { let age: Int = 22 let height: Double? = 180.0 let name: String = "XiangHui" var hasGirlFriend: Bool = false } //MemoryLayout<Double?>.size 9 //MemoryLayout<Double?>.alignment 8 //MemoryLayout<Double?>.stride 16 class MyClass { func test() { var me = Me() print(me) } } let myClass = MyClass() myclass.test() 复制代码
在方法里打个断点使用调试器输出栈中的内存,在这以前能够猜测一下,Int
类型占8个字节,Double?
虽然size是9个字节,可是它的stride是16字节,因此占据了16字节,String
类型占据了24个字节,最后Bool
类型占据8个字节,一共8 + 16 + 24 + 8 = 56
字节,也就是说这个结构体在栈上占据56字节的内存,打印以下:
(lldb) po MemoryLayout.size(ofValue: me) 49 (lldb) po MemoryLayout.stride(ofValue: me) 56 复制代码
奇怪,为何size是49呢?由于size是从开始到实际结束所占据的内存,即Bool的size和stride都是为1个字节,这样的话,当前word还有7个字节是没有使用的内存,因此实际大小为49字节。再看详细地址打印:
(lldb) frame variable -L me 0x00007ffeea2cda50: (MemorySwiftProject.Me) me = { 0x00007ffeea2cda50: age = 22 0x00007ffeea2cda58: height = 180 0x00007ffeea2cda68: name = "XiangHui" 0x00007ffeea2cda80: hasGirlFriend = false } 复制代码
地址是从栈底一直向上增长的,我画出示意图以下:(Bool
size为1)
原来在结构体中栈的存储如此简单, 那么若是结构体中有声明引用类型呢?结果是引用类型占一个word(指针所占空间为8个字节);那么若是在结构体中有方法体呢? 结论是结构体中即便有方法实现依然不占据内存,这个问题留待下篇文章来解决!可是能够有一个初步的猜想,我以为应该是和方法的静态调用有关,也便是和编译器的编译相关。
// 方法体在结构体中并不占据内存 struct Test { let a = 1 func test01() {} } let test = Test() MemoryLayout.size(ofValue: test) // 8 struct Test2 { func test01() {} } let test2 = Test2() MemoryLayout.size(ofValue: test2) // 0 复制代码
原本应该是要了解了解堆的,结果在方法调用断点输出的时候,发现了一些值得一提的点,因此就决定聊一聊关于方法栈中的内存!关于方法的调度,其实就是一个一个方法的入栈,栈顶方法执行完以后出栈,而后新的栈顶方法执行完以后出栈。若是是在一个递归方法的执行过程当中,这个就感受看起来颇有意思。
可是呢,如今不聊方法的调度,而是聊一聊当执行一个方法的时候,方法的内部是如何进行内存分配的,首先一点,方法在执行过程当中内存是分配在栈上的!
struct Me { let age: Int = 22 // 8 let height: Double? = 180.0 // size: 9 stride: 16 let name: String = "XiangHui" // 24 let a = MemoryClass() // 8 let hasGirlFriend = false // 1 } // MemoryLayout<Me>.stride 64(8 + 16 + 24 + 8 + 8 = 64) func test() { var number = 134 // stride: 8 var name = "JoJo" // stride: 8 var me = Me() // stride: 64 var likes = ["comdy", "animation", "movies"] // stride: 8 withUnsafeBytes(of: &number, { bytes in print("number: \(bytes)") }) withUnsafeBytes(of: &name, { bytes in print("name: \(bytes)") }) withUnsafeBytes(of: &me, { bytes in print("me: \(bytes)") }) withUnsafeBytes(of: &likes, { bytes in print("likes: \(bytes)") }) } 复制代码
在这里首先解释一下为何结构体的stride是64个字节吗?经过上述讲了这里应该很明了了吧,在这个结构体中有Int Double? String Class Bool
类型,一共8 + 16 + 24 + 8 + 8 = 64字节。还有一个小细节为何数组likes的stride是8个字节呢?由于在栈上分配的依然是一个数组指针而已,它指向内存中的另外一块存储空间,至于实际数组所存储的内存空间是如何分配呢?留待下篇文章解决~ 代码输出结果以下:
0x00007ffee46f2ac0: (Int) number = 134 0x00007ffee46f2aa8: (String) name = "JoJo" 0x00007ffee46f2a68: (MemorySwiftProject.Me) me = { 0x00007ffee46f2a68: age = 22 0x00007ffee46f2a70: height = 180 0x00007ffee46f2a80: name = "XiangHui" scalar: a = 0x000060c00001de10 {} //引用类型在堆中的具体地址 0x00007ffee46f2aa0: hasGirlFriend = false } 0x00007ffee46f2a20: ([String]) likes = 3 values { 0x00007ffc9d780500: [0] = "comdy" 0x00007ffc9d721710: [1] = "animation" 0x00007ffc9d6443d0: [2] = "movies" } 复制代码
经过withUnsafeBytes(of:&T) {}
方法,count输出的是Size。那么接下来开始分析了:首先有一点值得注意,输出的内存竟然是依次递减的,也就是说栈底的元素反而内存地址较高,然后入栈的元素,地址是依次变小的,因此结构体以下:
而后接下来我改变结构体的大小结果发现,在方法栈中多出的这块内存依旧和结构体实例的size同样大,为何呢?为何在方法栈中给结构体分配内存的时候会多出一块内存呢,并且size还和它的size同样大?一样留着这个问题吧!
在咱们看完栈上的内存以后,堆上的内存其实也是同样的,代码实例以下:
class MemoryClass { static let name = "Naruto" let ninjutsu = "rasengan" // 24 let test = TestClass() // 8 let age = 22 // 8 func beatSomeone() { let a = ninjutsu + ninjutsu print(a) } } func heapTest() { let myClass = MemoryClass() print(myClass) } heapTest() 复制代码
在heapTest( )方法中打个断点能够获得如下输出:
(lldb) frame variable -L myClass scalar: (MemorySwiftProject.MemoryClass) myClass = 0x000060400027ca80 { 0x000060400027ca90: ninjutsu = "rasengan" scalar: test = 0x00006040004456d0 { 0x00006040004456e0: name = "Hui" } 0x000060400027cab0: age = 22 } (lldb) po malloc_size(Unmanaged.passUnretained(myClass as AnyObject).toOpaque()) 64 复制代码
那么根据输出的结果能够得出如下结论:
为何从最后一个word开始分析呢?由于每次新建一个object,object的属性都是从第16个字节开始分配的,因此在每一个对象的前两个word都必然存储一些其余的信息,由于以前的OC基础,因此能够猜想应该是存储的一个isa指针之类的信息。可是最后8个字节就不必定出现了,接下来个人测试方式是在MyClass中增长不停的增长Bool类型的成员变量,一开始预测,每一次添加都会增长一个word的字节数,结果经过malloc_size(UnsafeRawPointer)
方法我获得的每一次内存大小为64 80 96 ...
都是以16个字节递增的,因此我能够初步肯定这是堆分配内存的特性,每次都会分配16个字节的倍数的内存,回到上图,那么若是增长一个Int
成员变量,它的内存大小为应该为64字节,而实验结果大小正好也是64字节,符合!若是再增长一个Bool
型的成员变量,它的内存大小为80字节,也正如推测。因此结论是:至少在iOS 64 系统上,堆上对对象分配内存时,每次都是分配的16个字节的倍数
class MemoryClass { static let name = "Naruto" let ninjutsu = "rasengan" // 24 let test = TestClass() // 8 let age = 22 // 8 let age2 = 22 // 8 } // malloc_size(Unmanaged.passUnretained(myClass as AnyObject).toOpaque()) 64 class MemoryClass { static let name = "Naruto" let ninjutsu = "rasengan" // 24 let test = TestClass() // 8 let age = 22 // 8 let age2 = 22 // 8 let a = false // 1 (只多了一个Bool类型) } // malloc_size(Unmanaged.passUnretained(myClass as AnyObject).toOpaque()) 80 复制代码
使用static修饰的name属性,在初始化类实例的时候并无出现堆上的内存中,这在开篇第二幅图中就解释了这个问题,在整个内存区域能够分为栈区;堆区;全局变量,静态变量区;常量区;代码区。下面是我画的图:
类型变量并不会分配在堆上,而是会在编译的时候就分配在Global Data区域中,因此这也是在堆上为何类型变量没有分配内存的缘由.
其实这个问题呢我也思考了好久,感受上应该就是OC中的isa指针指向它的类,结果也是如此,这篇文章有很明确的解释:C++中对象的isa指针指向的是VTable,它只是单纯的方法列表,而在swift中更复杂一些,实际上全部的Swift类都是Objective-C类,若是添加了@obj或者继承NSObject的类会更直观,可是即便是纯粹的Swift类依然在本质上就是Objective-C类。针对这个问题我专门在twitter上询问了大神@mikeash,他回复的原话:
Yes, they subclass a hidden SwiftObject class.
因此第一个word其实就是一个isa指针,指向的就是Class; 可是更准确的说,不必定是isa指针,有时候是isa指针和其余的东西,好比说和当前对象相关联的其余对象(当前对象释放时它也须要清理)... 可是一般意义上咱们能够理解为就是isa指针。
咱们能够作一个实验,改变当前对象的isa指针,指向其余的类型,那么会发生什么呢?
class Cat { var name = "cat" func bark() { print("maow") } //可变原始指针(当前实例的指针) func headerPointerOfClass() -> UnsafeMutableRawPointer { return Unmanaged.passUnretained(self as AnyObject).toOpaque() } } class Dog { var name = "dog" func bark() { print("wangwang") } //可变原始指针(当前实例的指针) func headerPointerOfClass() -> UnsafeMutableRawPointer{ return Unmanaged.passUnretained(self as AnyObject).toOpaque() } } func heapTest() { let cat = Cat() let dog = Dog() let catPointer = cat.headerPointerOfClass() let dogPointer = dog.headerPointerOfClass() catPointer.advanced(by: 0) .bindMemory(to: Dog.self, capacity: 1) .initialize(to: dogPointer.assumingMemoryBound(to: Dog.self).pointee, count: 1) cat.bark() // wangwang } 复制代码
由于cat实例的isa指针指向了Dog类型,swift中的方法都是静态派发的,只有加上加上dynamic关键字才是动态派发的,在这里其实就是cat的第一个word指向了dog,它会直接调用方法列表中的第一个方法,问题来了:若是在bark() 前面再加上另外一个方法如fuck()会如何? 答案是执行fuck()!由于并不是是动态的寻找执行的方法,只是利用偏移量去找到对应的方法执行的!swift类默认都是静态派发的,根据偏移量找到对应方法。
既然提到了isa指针,那么接下来有会有疑惑了isa指向的Class的结构究竟是怎样的呢?由于以前已经提到了Swift类本质上是OC类,因此咱们看OC类的定义就能够了,由于Objective-C类定义是开源的,因此就看下图呗:
Class isa Class super_class const char *name long version long info long instance_size struct objc_ivar_list *ivars struct objc_method_list **methodLists struct objc_cache *cache struct objc_protocol_list *protocols 复制代码
内存中的Class存储了类名;它的实例大小;属性列表;方法列表;协议列表;缓存(加快了方法调度)等等...可是,这毕竟是一个Objective-C Class中的结构,事实上Swift Class拥有Objective-C Class里的全部内容并且还添加了一些东西,可是本质上,Swift Class只是拥有更多东西的Objective-C Class
uint32_t flags;
uint32_t instanceAddressOffset;
uint32_t instanceSize;
uint16_t instanceAlignMask;
uint16_t reserved;
uint32_t classSize;
uint32_t classAddressOffset;
void *description;
复制代码
好吧,第一个Word存储的能够简单地说就是指向Class的指针,那么第二个Word呢?其实第二个Word存放的是引用计数,在Swift是使用的引用计数来管理对象的生命周期的,Swift中有两种引用计数,一种是强引用,一种是弱引用,而在二者都在这个Word中,每一种引用计数的大小31个字节! 那么接下来那张图就能够完善了:
其实这一篇下来仍是学了挺多东西的,接下来我来捋一捋脉络:
可是呢,也给本身留下了一些问题,这些问题就留待在下篇文章解答吧:
参考文章:
Unsafe Swift: Using Pointers And Interacting With C
Exploring Swift Memory Layout
Swift 对象内存模型探究(一)
Swift进阶以内存模型和方法调度
Printing a variable memory address in swift
最后附上个人Blog地址,若是以为写得不错欢迎关注个人掘金,或者常来逛个人Blog~~