天天看点

Swift内存模型的那点事儿

        跟OC类似, Swift提供了MemoryLayout类静态测量对象大小, 注意是在编译时确定的,不是运行时哦! 作为Java程序员想想如何测量Java对象大小? 参考 Java对象到底有多大?

       写这篇博客的目的在于说明2个黑科技:

1、 类/结构体的成员变量声明顺序会影响它的占用空间。 原理是内存对齐, 有经验的码农会把占用空间大的变量写在前面, 占用空间小的写在后面。 PS: 大道同源,C/C++/Object-C/Swift/Java都需要字节对齐; 如果面试时问你内存优化有什么经验? 你告诉他这个一定会另眼相看!!!

2、 可以篡改Swift结构体/类对象的私有成员(通过指针操作内存)。 Java要用反射实现。

       为什么要内存对齐呢?简单来说就是CPU寻址更快,详情参见 内存对齐原因

       在不同机型上数据类型占用的空间大小也不同, 例如iPhone5上Int占4个字节, iPhone7上Int占8个字节。 本文是在iPhone7模拟器上验证的。

内存分配:参考 结构体和类的区别

Stack(栈),存储值类型的临时变量,函数调用栈,引用类型的临时变量指针,结构体对象和类对象引用

Heap(堆),存储引用类型的实例,例如类对象

Swift3.0提供了内存操作类MemoryLayout(注意:Swift跟OC一样,内存排列时需要对齐,造成一定的内存浪费,我们称之为内存碎片), 它有3个主要参数:

1、实例方法alignment和静态方法 alignment(ofValue: T):

字节对齐属性,它要求当前数据类型相对于起始位置的偏移必须是alignment的整数倍。 例如在iPhone7上Int占8个字节,那么在类/结构体中Int型参数的起始位置必须是8的整数倍(可认为类/结构体第一个成员变量的内存起始位置为0), 后面会用实例说明。

2、 实例成员变量size和静态方法size(ofValue: T)

得到一个 T 数据类型实例占用连续内存字节的大小。

3、实例成员变量stride和静态方法 stride(ofValue: T)

在一个 T 类型的数组中,其中任意一个元素从开始地址到结束地址所占用的连续内存字节的大小就是 stride。 如图:

Swift内存模型的那点事儿

注释:数组中有四个 T 类型元素,虽然每个 T 元素的大小为 size 个字节,但是因为需要内存对齐的限制,每个 T 类型元素实际消耗的内存空间为 stride 个字节,而 stride - size 个字节则为每个元素因为内存对齐而浪费的内存空间。

       所以, 一个对象或变量占用的空间是由本身大小和偏移组成的!  我们改变不了数据类型本身的大小, 但我们可以尽量的缩小偏移, 后面会讲怎么做!

下面用几个实例说明:

class People1: NSObject{
        var name: String?
    }
    
    class People2: NSObject{
        var name: String?
        var age: Int?
    }
    let people1 = People1()
    let people2 = People2()
           

      people1和people2占用多大内存???

     别晕! 这是类对象的引用, 而引用占用的内存空间是固定的,即people1和people2占用内存大小相同, 区别是指向的内存空间占用大小不同!

     如果将类改成结构体会怎样?

struct People1 {
        var name: String?
    }
    
    struct People2 {
        var name: String?
        var age: Int?
    }
    let people1 = People1(name: "zhangsan")
    let people2 = People2(name: "zhangsan", age: 1)
           

     结构体是值类型, 在iPhone7上Int占8个字节,String占24个字节; 所以people1占用24个字节,people2占用32个字节。

        类/结构体的成员声明顺序会影响占用空间,原理是变量要以自身类型的aligment整数倍作为起始地址,不足的话要在前面补齐字节(即内存碎片)。 下面示例说明: 结构体People1和People2的参数相同,区别是先后顺序不一致。

//在iPhone7上占16个字节
    struct People2 {
        var enable = false  //占用1个字节
        var age = 1         //Int占8个字节, 因为是8字节对齐,必须以8的整数倍开始,所以前面要补齐7个字节偏移。
    }
    //在iPhone7上占9个字节
    struct People3 {
        var age = 1
        var enable = false  //aligmnet是1, 所以不要添加偏移
    }
    print("People2:  size=\(MemoryLayout<People2>.size) align=\(MemoryLayout<People2>.alignment)  stride=\(MemoryLayout<People2>.stride)")
            
    print("People3:  size=\(MemoryLayout<People3>.size) align=\(MemoryLayout<People3>.alignment)  stride=\(MemoryLayout<People3>.stride)")
           

输出:

People2:  size=16 align=8  stride=16

People3:  size=9 align=8  stride=16

Optional即可选数据类型会增加1个字节空间, 由于内存对齐的原因,Optional可能占用更多的内存空间。下面以Int为例:

print("Int:  size=\(MemoryLayout<Int>.size) align=\(MemoryLayout<Int>.alignment)  stride=\(MemoryLayout<Int>.stride)")
     print("Optional Int:  size=\(MemoryLayout<Optional<Int>>.size) align=\(MemoryLayout<Optional<Int>>.alignment)  stride=\(MemoryLayout<Optional<Int>>.stride)")
           

Int:  size=8 align=8  stride=8

Optional Int:  size=9 align=8  stride=16

       Optional Int的size是9, Int的size是8。

      空类和空结构体占多大空间呢?

class EmptyClass {
        //占用一个引用的大小
    }
    struct EmptyStruct {
        //占1个字节,因为需要唯一的地址
    }         
   print("EmptyClass:  size=\(MemoryLayout<EmptyClass>.size) align=\(MemoryLayout<EmptyClass>.alignment)  stride=\(MemoryLayout<EmptyClass>.stride)")
            
    print("EmptyStruct:  size=\(MemoryLayout<EmptyStruct>.size) align=\(MemoryLayout<EmptyStruct>.alignment)  stride=\(MemoryLayout<EmptyStruct>.stride)")
           

EmptyClass:  size=8 align=8  stride=8

EmptyStruct:  size=0 align=1  stride=1

         EmptyClass占用8个字节(跟引用占用空间大小相等), 即使在EmptyClass添加几个成员变量, 得到的size仍然是8, 其实这里实际上测量的是引用; EmptyStruct的size为0但stride为1, 说明占用1个字节空间,因为每个实例都需要唯一的地址。

       如果你想知道类到底占用多大内存, 那么你可以尝试改为结构体后测量一下! 因为你测量的是类的引用。

      相信你对Swift内存占用情况有了一定的理解, 现在说说如何篡改内存。Swift提供了UnSafePointer类操作指针( 还记得Java怎样操作指针吗?看我前面的博客), iOS不负责UnSafePointer指向内存的回收(如果调用了allocate方法,则要调用deinitialize和deallocate方法释放内存), 所有在使用它时要注意回收内存。下面是Swift的所有指针操作类:

Swift内存模型的那点事儿

        如果你想操作类/结构体的内存,可以继承于_PropertiesMetriczable或对应函数。 下面代码摘自HandyJSON:

extension _PropertiesMetrizable {

    // locate the head of a struct type object in memory
    mutating func headPointerOfStruct() -> UnsafeMutablePointer<Byte> {

        return withUnsafeMutablePointer(to: &self) {
            return UnsafeMutableRawPointer($0).bindMemory(to: Byte.self, capacity: MemoryLayout<Self>.stride)
        }
    }

    // locating the head of a class type object in memory
    mutating func headPointerOfClass() -> UnsafeMutablePointer<Byte> {

        let opaquePointer = Unmanaged.passUnretained(self as AnyObject).toOpaque()
        let mutableTypedPointer = opaquePointer.bindMemory(to: Byte.self, capacity: MemoryLayout<Self>.stride)
        return UnsafeMutablePointer<Byte>(mutableTypedPointer)
    }

    // memory size occupy by self object
    static func size() -> Int {
        return MemoryLayout<Self>.size
    }

    // align
    static func align() -> Int {
        return MemoryLayout<Self>.alignment
    }

    // Returns the offset to the next integer that is greater than
    // or equal to Value and is a multiple of Align. Align must be
    // non-zero.
    static func offsetToAlignment(value: Int, align: Int) -> Int {
        let m = value % align
        return m == 0 ? 0 : (align - m)
    }
}
           

使用结构体亲测一下篡改私有变量(原理: 拿到对象头指针、判断出成员变量的偏移和占用内存大小,然后写内存):

//在iPhone7上测试
    struct Pig {
        private var count = 4   //8字节
        var name = "Tom"        //24字节
        
        //返回指向 Pig 实例头部的指针
        mutating func headPointerOfStruct() -> UnsafeMutablePointer<Int8> {
            return withUnsafeMutablePointer(to: &self) {
                return UnsafeMutableRawPointer($0).bindMemory(to: Int8.self, capacity: MemoryLayout<Pig>.stride) }
        }
            
        func printA() {
                print("Animal a:\(count)")
        }
    }
    
            var pig = Pig()
            let pigPtr: UnsafeMutablePointer<Int8> = pig.headPointerOfStruct()   //头指针, 类型同const void *
            //有了头指针,还需要知道每个变量的偏移位置和大小才可以修改内存
            let rawPtr = UnsafeMutableRawPointer(pigPtr)    //转换指针 类型同void *
            
            let aPtr = rawPtr.advanced(by: 0).assumingMemoryBound(to: Int.self)   //advanced函数时字节偏移,assumingMemoryBound是内存大小
            print("修改前:\(aPtr.pointee)")   //4
            pig.printA()  //count等于4
            aPtr.initialize(to: 100)    //将count参数修改为100,即篡改了私有成员
            print("修改后:\(aPtr.pointee)")   //100
            pig.printA()
           

输出:

修改前:4

Animal a:4

修改后:100

Animal a:100

类是引用类型, 实例是在Heap堆区域里, 而Stack栈里只是存放了指向它的指针; 而Swift是ARC即自动回收的,类相比于结构体需要额外的内存空间用于存放类型信息和引用计数。 在32bit机型上类型信息占4个字节,在64bit机型上类型信息占8字节; 引用计数占8字节。 考虑到内存对齐原因, 类属性总是从16字节开始。

       要修改类成员变量, 跟上面介绍的结构体类似, 但成员起始位置是第16个字节, 因为类型信息和引用计数占用的内存空间在类成员属性前面。 将Pig修改为类,对比一下:

//在iPhone7上测试
    class Pig {
        private var count = 4   //8字节
        var name = "Tom"        //24字节
        
        // 得到Pig对象在堆内存的首位置
        func headPointerOfClass() -> UnsafeMutablePointer<Int8> {
            
            let opaquePointer = Unmanaged.passUnretained(self as AnyObject).toOpaque()
            let mutableTypedPointer = opaquePointer.bindMemory(to: Int8.self, capacity: MemoryLayout<Pig>.stride)
            return UnsafeMutablePointer<Int8>(mutableTypedPointer)
        }
            
        func printA() {
                print("Animal a:\(count)")
        }
    }
    var pig = Pig()
    let pigPtr: UnsafeMutablePointer<Int8> = pig.headPointerOfClass()   //头指针, 类型同const void *
    //有了头指针,还需要知道每个变量的偏移位置和大小才可以修改内存
    let rawPtr = UnsafeMutableRawPointer(pigPtr)    //转换指针 类型同void *
            
    //在iPhone7上类型信息和引用计数参数占用16个字节,类成员属性相对起始位置要偏移16个字节
    let aPtr = rawPtr.advanced(by: 16).assumingMemoryBound(to: Int.self)   //advanced函数时字节偏移,assumingMemoryBound是内存大小
    print("修改前:\(aPtr.pointee)")   //4
    pig.printA()  //count等于4
    aPtr.initialize(to: 100)    //将count参数修改为100,即篡改了私有成员
    print("修改后:\(aPtr.pointee)")   //100
    pig.printA()
           

输出:

修改前:4

Animal a:4

修改后:100

Animal a:100

       结构体和类对象在篡改内存数据时, 结构体成员参数起始偏移为0, 类成员参数起始偏移为类型信息和引用计数占用空间之和。 

       直接操作内存是高阶玩法, 要判断当前机型CPU的位数(即需要适配),然后要理解内存模型。  

出个小题考验一下你是否理解了Swift内存模型:

struct Point {
    var a: Double?
    var b = 0
}
           

      从内存角度考虑, Point结构体有什么问题?

如果你没懵逼, 那么恭喜你已经掌握了Swift内存模型原理。  老司机应该这样写:

struct Point {
    var b = 0
    var a: Double?
}
           

       理由:因为Optional会多占1个字节, 第一种写法后面的Int型参数b会先内存对齐,然后再分配内存,即多占了一个Int型空间(PS:要减1)。

参考:

Swift 对象内存模型探究(一)

Swift进阶之内存模型和方法调度

Swift操作指针详解