Swift 一、类与结构体(上)

类和结构体.png

一、类与结构体

1.1 结构体

结构体:结构体和类十分相似,既可以定义属性,又可以定义方法,但其不像类一样具有继承的特性。
在swift中,使用struct关键字来定义结构体,结构体中可以声明变量或者常量作为结构体的属性,也可以创建函数作为结构体的方法,结构体使用点语法来调用其中的属性和方法。

示例代码如下:

struct ZZTeacher {

    var age: Int

    var name: String

    init(age: Int, name: String) {

        self.age = age

        self.name = name

    }

}

1.2 类

类的定义:类是编程世界中万物的抽象,使用类可以模拟万物的对象。
类使用关键字class来声明

class ZZTeacher {

    var age: Int

    var name: String

    init(age: Int, name: String) {

        self.age = age

        self.name = name

    }

    deinit {

    }

}

1.3 类与结构体的异同

相同点:
1)定义存储值的属性
2)定义方法
3)定义下标以使用下标语法提供对其值的访问
4)定义初始化器
5)使用 extension 来拓展功能
6)遵循协议来提供某种功能
不同点:
1)类有继承的特性,而结构体没有
2)类型转换使您能够在运行时检查和解释类实例的类型
3)类有析构函数用来释放其分配的资源
4)引用计数允许对一个类实例有多个引用

1.4 引用类型和值类型

swift语言中的数据类型分为值类型引用类型。结构体、枚举以及除类以外所有数据类型都属于值类型,只有类是引用类型的。值类型数据和引用类型数据最大的区别在于当进行数据传递时,值类型总是被复制,而引用类型不会被复制,引用类型是通过引用计数来管理其生命周期的。

类是引用类型,也就意味着一个类类型的变量并不直接存储具体的实例对象,而是对当前存储具体实例内存地址的引用。示例代码如下:

class ZGTeacher {

    var age: Int

    var name: String

    init(age: Int, name: String) {

        self.age = age

        self.name = name

    }

    deinit {

        

    }

}

var t = ZGTeacher(age: 32, name: "Zhang")

var t1 = t
声明一个类并复制给变量t.png
var t1 = t
t1.png

这里我们借助两个指令来查看当前变量的内存结构

po : p 和 po 的区别在于使用 po 只会输出对应的值,而 p 则会返回值的类型以及命令结果的引用名。

x/8g: 读取内存中的值(8g: 8字节格式输出)

t和t1地址打印.png

根据上图可以看到,t和t1存储的是同一实例对象的内存地址,但是它们自己的存储地址是不一样的。

Swift中有引用类型,就有值类型,最典型的就是 Struct ,结构体的定义也非常简单,相比较类类型的变量中存储的是地址,那么值类型存储的就是具体的实例(或者说具体的值)

struct ZGTeacher {

    var age: Int

    var name: String

    init(age: Int, name: String) {

        self.age = age

        self.name = name

    }

}


var t = ZGTeacher(age: 32, name: "Zhang")

var t1 = t
t1.age = 20
结构体t和t1.png

读者在这里需要注意,如果值类型有数据传递,原来的实例会被复制一份,修改新的实例并不能修改原始的实例

Struct t和Struct t1.png

其实引用类型就相当于 在线的Excel ,当我们把这个链接共享给别人的时候,别人的修改我们是能够看到的;值类型就相当于本地的 Excel ,当我们把本地的 Excel 传递给别人的时候,就相当于重新复制了一份给别人,至于他们对于内容的修改我们是无法感知的。

1.5 结构体和类在内存中的分布

引用类型和值类型还有一个最直观的区别就是存储的位置不同:一般情况,值类型存储的在栈上,引用类型存储在堆上
首先我们对内存区域来进行一个基本概念的认知,大家看下面这张图

内存分布示例图.png

栈区(stack): 局部变量和函数运行过程中的上下文

func test () {
    ///我们在函数内部声明的age变量是不是就是一个局部变量
    var age: Int = 32
    print("end")
}

test()
age被分配在栈区.png

Heap: 存储所有对象
Global: 存储全局变量;常量;代码区

Segment & Section: Mach-O 文件有多个段( Segment ),每个段有不同的功能。然后每个段又分为很多小的 Section

TEXT.text : 机器码
TEXT.cstring : 硬编码的字符串
TEXT.const: 初始化过的常量
DATA.data: 初始化过的可变的(静态/全局)数据
DATA.const: 没有初始化过的常量
DATA.bss: 没有初始化的(静态/全局)变量
DATA.common: 没有初始化过的符号声明

///初始化过的可变的(静态/全局)数据 
int a = 20;
///没有初始化过的符号声明
int age;
int main(int argc, const char * argv[]) {
    @autoreleasepool {
       ///硬编码的字符串
        char * p = "Zhang";
        NSLog(@"Hello, World!");
    }
    return 0;
}

通过lldb打印地址,和libLGCatAddress.dylib工具可以看到a被放在DATA.data

(lldb) po &a 0x0000000100008020
(lldb) cat address 0x0000000100008020
address:0x0000000100008020, 8a <+0> , External: NO ZGOCTest.__DATA.__data +8

可以看到age被放在DATA.__common

(lldb) po &age 0x0000000100008020
(lldb) cat address 0x0000000100008020
address:0x0000000100008020, 0age <+0> , External: NO ZGOCTest.__DATA.__common +0

可以看到p被放在TEXT.__cstring

po &p 0x00007ff7bfeff2e8
(lldb) x/8g 0x00007ff7bfeff2e8
0x7ff7bfeff2e8: 0x0000000100003f9a 0x00007ff7bfeff440
0x7ff7bfeff2f8: 0x0000000000000001 0x00007ff7bfeff410
0x7ff7bfeff308: 0x00000001000194fe 0x0000000000000000
0x7ff7bfeff318: 0x0000000000000000 0x0000000000000000
(lldb) cat address 0x0000000100003f9a
address:0x0000000100003f9a, 0ZGOCTest.__TEXT.__cstring +0

下面我们看一下结构体和类在内存中的分布。示例代码如下:

结构体

struct ZGTeacher {
    var age = 18
    var name = "Zhang"
    
}
func test () {
    var t = ZGTeacher()
    print("end")
}

test()

接下来使用命令 frame varibale -L xxx

frame variable -L t
0x00007ff7bfeff2b0: (ZGSwiftTest.ZGTeacher) t = {
0x00007ff7bfeff2b0: age = 18
0x00007ff7bfeff2b8: name = "Zhang"

结构体t在内存中的地址为0x00007ff7bfeff2b0,第一个成员变量age的地址正好和结构体的内存地址相同,接下来的8个字节存放的是另外一个成员变量name。根据这个打印不难看出结构体在内存中的分布情况,如下图所示:

结构体在栈上的内存分配.png

class ZGperson {
    var age = 18
    var name = "Zhang"
}

struct ZGTeacher {
    var age = 18
    var name = "Zhang"
    var p = ZGperson()
    
}
func test () {
    var t = ZGTeacher()
    print("end")
}

test()

frame variable -L t
0x00007ff7bfeff2b0: (ZGSwiftTest.ZGTeacher) t = {
0x00007ff7bfeff2b0: age = 18
0x00007ff7bfeff2b8: name = "Zhang"
scalar: p = 0x00000001031074c0 {
0x00000001031074d0: age = 18
0x00000001031074d8: name = "Zhang"
}
}

为结构体添加一个引用类型的变量,结构体分配的位置不会发生改变,还是被分配到栈上。

class ZGTeacher {
    var age = 18
    var name = "Zhang"
   
    
}
func test () {
///1、栈上分配8个字节,分配实例对象的引用类型
///2、堆空间上寻找合适的内存区域
///3、将value----拷贝到堆
///4、将栈上空间的地址指向堆区
    var t = ZGTeacher()
    print("end")
}

test()

(lldb) frame variable -L -t
scalar: (ZGSwiftTest.ZGTeacher) t = 0x0000000101009fd0 {
0x0000000101009fe0: age = 18
0x0000000101009fe8: name = "Zhang"
}
(lldb) cat address 0x0000000101009fd0
address:0x0000000101009fd0, (String) $R0 = "0x101009fd0 heap pointer, (0x30 bytes), zone: 0x7ff859c9e000"

可以看到类是被分配到堆(heap pointer)上的

类在内存上的分配.png

1.5 类与结构体的应用场景和时间分配

类和结构体有着本质的不同,它们在传递数据时的机制不同,分别适用于不同的应用场景。苹果官方推荐开发者在如下情况使用结构体来描述数据:

1)要描述的数据类型中只有少量的简单数据类型的属性。
2)要描述的数据类型在传递数据时需要以复制的方式进行。
3)要描述的数据类型中的所有属性在进行传递时需要以复制的方式进行。
4)不需要继承另一个数据模型

这里我们也可以通过github上StructVsClassPerformance这个案例来直观的测试当前结构体和类的时间分配。

我们来看两个官方案例

案例一:

 import UIKit
enum Color { case blue, green, gray }
enum Orientation { case left, right }
enum Tail { case none, tail, bubble }
var cache = [String : UIImage]()
func makeBallon(_ color : Color, _ orientation: Orientation, _ tail: Tail) -> UIImage {
    let key = "\(color):\(orientation):\(tail)"
    if let image = cache[key] {
        return image
    }
    return UIImage.init()
    }

通过上面的案例代码可以看到调用key值会不停的访问开辟和释放空间,效率不高,可以进行相关的优化。

 enum Color { case blue, green, gray }
enum Orientation { case left, right }
enum Tail { case none, tail, bubble }
var cache = [String : UIImage]()
struct Ballon: Hashable {
    var color: Color
    var orientation: Orientation
    var tail: Tail
}
func makeBallon(_ ballon: Ballon) -> UIImage {

    if let image = cache[ballon] {
        return image
    }
    return UIImage.init()
    
}

案例二:

struct Attachment {
    let fileURL: URL
    let uuid: String
    let mineType: String
    init?(fileURL: URL, uuid: String, mineType: String) {
        guard mineType.isMineType else {
            return nil
        }
        self.fileURL = fileURL
        self.uuid = uuid
        self.mineType = mimeType
    }
}

可优化为:

enum MineType: String {
    case jpeg = "image/jpeg"
    ...
}
struct Attachment {
    let fileURL: URL
    let uuid: UUID
    let mineType: MineType
    init?(fileURL: URL, uuid: UUID, mineType: MineType) {
        guard mineType.isMineType else {
            return nil
        }
        self.fileURL = fileURL
        self.uuid = uuid
        self.mineType = mimeType
    }
}

在实际开发应用时,应该尽可能的用值类型替代引用类型,用结构体替代类

二、类的初始化器

在结构体中开发者并不需要提供构造方法,结构体会根据属性自动生成一个构造方法,而类则要求开发者自己提供构造方法,在init()构造方法中,需要完成对类中所有属性的赋值操作。

struct ZGTeacher {

    var age: Int

    var name: String


//    ///如果自己不创建的话,默认会自动生成的

//    init() {

//

//    }

}

Swift 中创建类和结构体的实例时必须为所有的存储属性设置一个合适的初始值。所以类 ZGPerson 必须要提供对应的指定初始化器,同时我们也可以为当前的类提供便捷初始化器(关键字convenience)(注意:便捷初始化器必须从相同的类里调用另一个初始化器。)

class ZGPerson {

    var age: Int

    var name: String

    ///_表示匿名函数

    ///_: 外部变量

    /// age: 内部变量

    init(_ age: Int, _ name: String) {

        self.age = age

        self.name = name

    }

    ///便捷初始化器,必须调用当前类的其他构造方法
    convenience init() {

        self.init(32, "Zhang")

    }

}

当我们派生出一个子类 ZGTeacher ,并指定一个指定初始化器之后会出现什么问题

class ZGTeacher: ZGPerson {

    var subjectName: String

    init(subjectName: String) {

        self.subjectName = subjectName

    }

}

报错.png

Swift语言中有这样的原则:

1)指定初始化器必须保证在向上委托给父类初始化器之前,其所在类引入的所有属性都要初始化完成。
2)指定初始化器必须先向上委托父类初始化器,然后才能为继承的属性设置新值。如果不这样做,指定初始化器赋予的新值将被父类中的初始化器所覆盖。
3)便捷初始化器必须先委托同类中的其它初始化器,然后再为任意属性赋新值(包括同类里定义的属性)。如果没这么做,便捷构初始化器赋予的新值将被自己类中其它指定初始化器所覆盖。
4)初始化器在第一阶段初始化完成之前,不能调用任何实例方法、不能读取任何实例属性的值,也不能引用 self 作为值。

可失败初始化器

可失败构造方法的定义十分简单,只需要使用init?()即可

class ZGPerson {

    var age: Int

    var name: String

    //_表示匿名函数

    //_: 外部变量

    // age: 内部变量

    init?(_ age: Int, _ name: String) {

        ///比如这里我们定义了如果小于18就不是一个合法的成年人

        if age < 18 {

            return nil

        }

        self.age = age

        self.name = name

    }

    ///便捷初始化器,必须调用当前类的其他构造方法

    convenience init?() {

        self.init(32, "Zhang")

    }

}

必要初始化器
另外,开发者也可以设置某些构造方法为必要构造方法,如果一个类中的某些构造方法被指定为必要构造方法,则其子类必须实现这个构造方法(可以通过继承或者覆写的方式),必要构造方法需要使用required关键字进行修饰,示例代码如下:

class ZGPerson {

    var age: Int

    var name: String

    //_表示匿名函数

    //_: 外部变量

    // age: 内部变量

    required init(_ age: Int, _ name: String) {

        self.age = age

        self.name = name

    }

    ///便捷初始化器,必须调用当前类的其他构造方法

    convenience init() {

        self.init(32, "Zhang")

    }

}

class ZGTeacher: ZGPerson {

    var subjectName: String

    init(subjectName: String) {

        self.subjectName = subjectName

        super.init(18, "Zhang")

    }
///如果在子类没有提供就会报错
    

}

三、类的生命周期

iOS开发的语言不管是OC还是Swift后端都是通过LLVM进行编译的,如下图所示:

LLVM.png

OC 通过 clang 编译器,编译成 IR,然后再生成可执行文件 .o(这里也就是我们的机器码)
Swift 则是通过 Swift 编译器编译成 IR,然后再生成可执行文件。

IR.png

// 分析输出AST
swiftc main.swift -dump-parse
// 分析并且检查类型输出AST
swiftc main.swift -dump-ast
// 生成中间体语言(SIL),未优化
swiftc main.swift -emit-silgen
// 生成中间体语言(SIL),优化后的
swiftc main.swift -emit-sil
// 生成LLVM中间体语言 (.ll文件)
swiftc main.swift -emit-ir
// 生成LLVM中间体语言 (.bc文件)
swiftc main.swift -emit-bc
// 生成汇编
swiftc main.swift -emit-assembly
// 编译生成可执行.out文件
swiftc -o main.o main.swift

SIL文件读取分析

@main: 入口函数, @为标识
%0: 寄存器,虚拟的,真的寄存器
xcrun swift-demangle 反编译

Swift 对象内存分配:

__allocating_init -----> swift_allocObject -----> swift_allocObject
-----> swift_slowAlloc -----> Malloc

Swift 对象的内存结构 HeapObject (OC objc_object) ,有两个属性:
一个是Metadata ,一个是 RefCount,默认占用 16 字节大小。

objc_object {
isa
}

源码中 kind种类

kind种类.png

struct HeapObject {
    var metadata: UnsafeRawPointer
    var refcounted1: UInt32
    var refcounted2: UInt32
}

struct Metadata {

    var kind: Int

    var superClass: Any.Type

    var cacheData: (Int, Int)

    var data: Int

    var classFlags: Int32

    var instanceAddressPoint: UInt32

    var instanceSize: UInt32

    var instanceAlignmentMask: UInt16

    var reserved: UInt16

    var classSize: UInt32

    var classAddressPoint: UInt32

    var typeDescriptor: UnsafeMutableRawPointer

    var iVarDestroyer: UnsafeRawPointer

}

class ZGTeacher {
    var age: Int = 18
    var name: String = "Zhang"
}

var t = ZGTeacher()
let objcRawPtr = Unmanaged.passUnretained(t as AnyObject).toOpaque()
let objcPtr = objcRawPtr.bindMemory(to: HeapObject.self, capacity: 1)
print(objcPtr.pointee)
let metadata = objcPtr.pointee.metadata.bindMemory(to: Metadata.self, capacity: MemoryLayout.stride).pointee
print(metadata)

HeapObject(metadata: 0x00000001000081a8, refcounted1: 3, refcounted2: 0)
Metadata(kind: 4295000432, superClass: _TtCs12_SwiftObject, cacheData: (140703534573568, 140943646785536), data: 4302469682, classFlags: 2, instanceAddressPoint: 0, instanceSize: 40, instanceAlignmentMask: 7, reserved: 0, classSize: 168, classAddressPoint: 16, typeDescriptor: 0x0000000100003c6c, iVarDestroyer: 0x0000000000000000)

经过源码分析和数据类型的重绑定,我们不难得出 swift 类的数据结构

struct Metadata {

    var kind: Int

    var superClass: Any.Type

    var cacheData: (Int, Int)

    var data: Int

    var classFlags: Int32

    var instanceAddressPoint: UInt32

    var instanceSize: UInt32

    var instanceAlignmentMask: UInt16

    var reserved: UInt16

    var classSize: UInt32

    var classAddressPoint: UInt32

    var typeDescriptor: UnsafeMutableRawPointer

    var iVarDestroyer: UnsafeRawPointer

}

你可能感兴趣的:(Swift 一、类与结构体(上))