iOS里变量在内存中的存储方式

对于iOS里变量在内存中的存储方式这个问题, 可以分解成如下两个问题:

  • 内存是如何布局的
  • ObjC 与 Swift 是一样的么?两者的区别是什么?

内存是如何布局的

基本概念

操作系统会为运行的应用程序分配一片巨大的虚拟内存, 与物理内存不一样, 虚拟内存并不是在物理上真正存在的, 它是操作系统构建的逻辑概念。 通过 CPU 芯片中的内存管理单元(MMU)将虚拟内存映射到物理地址上。 一般虚拟内存会分成以下几个不同的区域:

内存区域

  • .stack 存储程序执行期间的本地变量和函数的参数, 从高地址向低地址生长。
  • .heap 动态内存分配区域。
  • .bss 存储未被初始化的全局变量和静态变量。
  • .data 存已经初始化的全局变量和静态变量,是静态内存分配。
  • .text 程序代码。

ObjC 与 Swift 是一样的么?两者的区别是什么?

简单来说, 不一样。

ObjC 对象内存分配

ObjC 在创建对象时, 使用两段申请, alloc 和 init。查看 NSObject.mm 源文件, 可以发现 alloc 方法是通过 calloc(或者 malloc_zone_calloc)给对象在 heap 上申请了一段内存。

static ALWAYS_INLINE id
_class_createInstanceFromZone(
    Class cls, 
    size_t extraBytes,  
    void *zone, 
    int construct_flags = OBJECT_CONSTRUCT_NONE, 
    bool cxxConstruct = true, 
    size_t *outAllocatedSize = nil)
{
    .........

    id obj;
    if (zone) {
        obj = (id)malloc_zone_calloc((malloc_zone_t *)zone,  1,  size);
    } else {
        obj = (id)calloc(1,  size);
    }

    ........
}
复制代码

通常在 ObjC 里对象创建在 heap 上, 如果你想在 stack 上创建对象, 会有些绕, 类似如下方法

struct {
    Class isa;
} fakeNSObject;
fakeNSObject.isa = [NSObject class];
    
NSObject *obj = (NSObject *)&fakeNSObject;
NSLog(@"%@",  [obj description]);
复制代码

在 ObjC 里 blocks 是存储在 stack 上的。其大小可以编译时计算出来, 事实上, 整个对象是由编译器生成的代码构建的。 因此, 无法通过写一个 initializer 来做点 tricky 的事情。 因为 blocks 是在 stack 上, 所以需要持有 blocks 代码时, 都需要 copy 一份到 heap 上, 而不是 retain 这也是为什么当 Block 作为属性时, 常常要用 copy 修饰。举例来说:

    [dictionary setObject: ^{ printf("hey hey\n"); } forKey: key];
复制代码

dictionary 会 retain 这个 block, 而不是 copy, 这就会导致悬挂指针问题

Swift 对象内存分配

在 Swift 里面变量的内存分配有了很大的变化, Swift 有两种类型, Value Type 和 Reference Type。一般认为, Value Type 在 stack 上, Reference Type 在 heap 上。 但这么表述只是部分正确,实际情况比这复杂的多, 准确的来说, Swift 并不保证 objects 和 values 存储在哪里。 如果 Reference Type 在内存中有一个稳定的位置, 则对同一对象的所有引用都会指向完全相同的位置。 而 Value Type 不能保证在内存中有一个稳定的位置, 并且可以在编译器认为合适的时候任意复制

在实践中,我们可以认为 reference type 是存储在 heap 上的。而 Value Type 的存储就比较复杂了, 需要视情况而定。

  1. 除非需要对一个值进行基于位置的引用(例如, 用&获取对结构的引用), 否则结构可能完全位于寄存器中(比如类似 Ints 和 Doubles 这样的小的、可能是短暂的值类型, 它们被保证适合放在寄存器中)。
  2. 大的值类型实际上是分配在堆上的, 这与值类型的值性并不冲突:它仍然可以按照编译器的意愿任意复制, 而且编译器在避免不必要的分配方面做得非常好

其实, 我们可以想象值类型是以 inline 方式存储的, 假设我们有下面的一个结构体:

struct Point {
    var x: Int
    var y: Int
}
复制代码

当你需要存储一个 Point 时, 编译器会确保你有足够的空间来存储 x 和 y, 通常一个接一个的存储, 如果一个 Point 被存储在 stack 中, 那么 x 和 y 被存储在 stack 中; 如果 Point 被存储在 heap 中, 那么 x 和 y 作为 Point 的一部分存在于 Heap 中。 无论 Swift 把一个 Point 存储在什么位置, 它总是确保有足够的空间, 当给 x 和 y 赋值时, 它们会被写到哪个空间。在哪里并不十分重要。

当 Point 是另外一个对象的属性时, 情况也是一样, 也可以当成 inline 来考虑


class Location {
    var name: String
    var point: Point
}
复制代码
┌──────────────────────┐
│       Location       │
├──────────┬───────────┤
│          │   Point   │
│   name   ├─────┬─────┤
│          │  x  │  y  │
└──────────┴─────┴─────┘
复制代码

当创建一个 Location 对象时, 编译器会确保有足够的空间来存储一个 String 和两个 Doubles, 并将它们一个接一个地排出来。这些空间在哪里并不重要, 但在这种情况下, 它们都在堆上(因为Location是一个引用类型, 它恰好包含值)。

但是当你把 Point 改成 class 时, 情况又会有所改变。这时候就不是 inline 这种方式了。而是我们熟悉的引用的方式了, 类似于这样:

┌──────────────────────┐      ┌───────────┐
│       Location       │ ┌───▶│   Point   │
├──────────┬───────────┤ │    ├─────┬─────┤
│   name   │   point ──┼─┘    │  x  │  y  │
└──────────┴───────────┘      └─────┴─────┘
复制代码

前面, Swift 为 Location 分配内存时, 是存储一个字符串和两个数, 现在它存储一个字符串和一个指向 Point 的指针。 但从使用者的角度是看不出什么的, 因为访问方式没有变。

总结

  1. ObjC 的 block 跟普通对象不一样, 一般是分配在栈上, 当你想要持有一个 block 时, 需要 copy 一份到 heap 上, 这也就是为什么 block 的 property 一般用 copy 代替 retain。
  2. Swift 的内存分配比 ObjC 要复杂很多, Value Type 并不是一定分配在栈中, 而是视情况而定。

欢迎关注微信公众号:morpheus的日志

博客:www.lvwei.blog

参考链接

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享