Swift 5.1 温故而知新笔记系列之第一天

1.编译流程

在这里插入图片描述

在这里插入图片描述

操作

swiftc -dump-ast main.swift // 生成语法树
swiftc -emit-sil main.swift  // 生成最简洁的SIL代码
swiftc -emit-ir main.swift -o main.ll  // 生成LLVM IR代码
swiftc -emit-assembly main.swift -o main.s  // 生成汇编代码
Contents/Developer/Toolchains/XcodeDefailt.xctoolchain/usr/bin

2.基础语法

2.1 基本运算

简单略

2.2 流程控制

简单略

2.3 函数

1、基本函数

/// 求和 【概述】
///
/// 求和 【详述】
///
/// - Parameters:
///   - v1: 参数1
///   - v2: 参数2
///
/// - Note: 传入两个Int类型的参数相加
///
func sum(v1: Int, v2: Int) -> Int {
    return v1 + v2
}

var result = sum(v1: 10, v2: 20)
print(result)

在这里插入图片描述

2、参数标签

  • 修改参数标签
func goToSleep(at time: String){
    print("It's time to Sleep -> \(time)")
}

goToSleep(at: "23:00")
  • 可以使用_省略参数标签
func sub(_ v1: Int, _ v2: Int) -> Int {
    return v1 - v2
}

var res = sub(10, 20)
print(res)

3、可变参数

func sum(_ numbers: Int...) -> Int {
    var res = 0
    for number in numbers {
        res += number
    }
    return res
}

print(sum(10,20,30,40,50))

// A parameter following a variadic parameter requires a label
func testFunc(_ numbers: Int..., name1:String, name2:String) {}

testFunc(10,20,30,41,name1: "MKJ", name2: "HY")

一个函数只能最多有一个可变参数
紧跟在可变参数后面的参数不能省略标签,报错 A parameter following a variadic parameter requires a label

4、输入输出参数
函数参数默认是let,默认是常量

var number = 10
func test(_ num: inout Int){
    num += 1
}

test(&number)
print(number)

可变参数不能标记为inout
inout参数不能有默认值
inout参数只能传入可以被多次赋值的 var
inout参数的本质是地址传递(引用传递)

5、函数重载 (Function Overload)

  • 函数名相同
  • 参数个数不同 或 参数类型不同 或 参数标签不同
  • 返回值的类型与函数重载无关
  • 默认参数和函数重载一起使用产生二义性时,编译器并不会报错(C++ 就会报错)
func sum(v1: Int, v2: Int) -> Int {
    v1 + v2
}

func sum(v1: Int, v2: Int, v3: Int = 10) -> Int {
    v1 + v2 + v3
}
// 有歧义,会调用第一个,但是不会报错
print(sum(v1: 10, v2: 20))

6、内联函数(函数调用直接展开成函数体)
如果开启了编译器优化,编译器会将某些函数变成内联函数(Release模式下默认开启了优化)
在这里插入图片描述
在这里插入图片描述
虽然断点了,但是控制台会直接打印输出,函数体直接内联在了main函数里面

除非加了@inline关键字

@inline(never) func test() {
    print("Mikejing")
}
test()

以上代码就永远不会被内联,即使开了编译优化,@inline(__always)该标识符就代表即使代码很长,也会被内联(动态派发或者递归调用除非),这里的动态派发可以理解为子类父类的多态

哪些不会被自动内联
1.函数体比较长
2.包含递归
3.包含动态派发

2.4 枚举

变量的取值就几种,就应该考虑用枚举

  • 原始值 不占用枚举变量的内存
enum Season : Int {
    case spring = 1, summber, autumn, winter
}
// s是枚举类型,不是Int 类型, 原始值,一一对应,存储关联
var s = Season.spring
s = .autumn
print(s)
print(s.rawValue)
print(Season.winter.rawValue)
  • 关联值 和枚举存储在一起,占用内存
enum Date {
    case digit(year: Int, month: Int, day: Int)
    case string(String)
}

var date = Date.digit(year: 2021, month: 11, day: 2)

date = .string("Ten Grade-12-IEG")

print(date)

switch date{
case let .digit(year, month, day):
    print("\(year)-\(month)-\(day)")
case let .string(name):
    print("\(name)")
}
  • MemoryLayout 测试枚举内存
enum Password {
    case number(Int, Int, Int, Int)
    case other1
    case other2
}

var password = Password.number(1, 2, 3, 4)
print(MemoryLayout.size(ofValue: password)) // 实际用到的大小 33
print(MemoryLayout.stride(ofValue: password)) // 分配占用的空间大小 40
print(MemoryLayout.alignment(ofValue: password)) //  内存对齐 8

password = .other1

print(MemoryLayout.size(ofValue: password)) // 33
print(MemoryLayout.stride(ofValue: password)) // 40
print(MemoryLayout.alignment(ofValue: password)) // 8

由于关联值是和枚举存储在一起的,那么.number有四个Int,占用了32个字节,其他枚举都可以用一个字节来区分,因此只要33个字节,由于内存对齐是8个字节,因此,最终分配的内存是40个字节。

  • 特殊案例测试
enum Grade: String {
    case perfect = "A"
    case great = "B"
    case good = "C"
    case bad = "D"
}

// print(MemoryLayout<Grade>.size)  // 1
// print(MemoryLayout<Grade>.stride) // 1
// print(MemoryLayout<Grade>.alignment) // 1


enum Direction {
    case north, south, east, west
}

//print(MemoryLayout<Direction>.size)  // 1
//print(MemoryLayout<Direction>.stride) // 1
//print(MemoryLayout<Direction>.alignment) // 1



enum Score {
    case points(Int)
    case grade(Character)
    case other
}

// print(MemoryLayout<Score>.size)  // 17
// print(MemoryLayout<Score>.stride) // 24
// print(MemoryLayout<Score>.alignment) // 8




enum Date {
    case digit(year: Int, month: Int, day: Int)
    case string(String)
}

//print(MemoryLayout<Date>.size)  // 25
//print(MemoryLayout<Date>.stride) // 32
//print(MemoryLayout<Date>.alignment) // 8

enum TestEnum {
    case test0
    case test1
    case test2
    case test3(Int)
    case test4(Int, Int)
    case test5(Int, Int, Int, Bool)
}
//print(MemoryLayout<TestEnum>.size)  // 25
//print(MemoryLayout<TestEnum>.stride) // 32
//print(MemoryLayout<TestEnum>.alignment) // 8

根据上面的例子,其中原始值类型,占用都是一个字节。枚举Score类型,Charater占用了16个字节,再多一个字节,就能区分其他三个类型,因此只要17个字节,Date类型也一样。再来看最后一种类型TestEnum,个人理解是,当.test5类型下,最少需要25个字节,那么最后一个字节,就可以用来区分作为用其他枚举类型的字节位,如果用来表示.test5,只要有特定值即可,然后其他枚举有对应的特定值即可。

上述的内存分布可以往后查看

2.5 可选类型(optional)

在这里插入图片描述

  • 可选项是对其他类型的一层包装,可以将它理解为一个盒子
  • 如果为nil,那么它是个空盒子
  • 如果不为nil,那么盒子里装的是:被包装类型的数据
  • 如果要从可选项中取出被包装的数据(将盒子里装的东西取出来),需要使用感叹号! 进行强制解包
  • 如果对值为nil的可选项(空盒子)进行强制解包,将会产生运行时错误
    Fatal error: Unexpectedly found nil while unwrapping an Optional value
可选项绑定

可以使用可选项绑定来判断可选项是否包含值
如果包含就自动解包,把值赋给一个临时的常量(let)或者变量(var),并返回true,否则返回false

示例一:

if let number = Int("123") { 
	print("字符串转换整数成功:\(number)") // number是强制解包之后的Int值
	// number作用域仅限于这个大括号
} else { 
	print("字符串转换整数失败")
}
// 字符串转换整数成功:123

示例二:条件语句中用到可选绑定,就需要,隔开,不能用&隔开

// 遍历数组,将遇到的正数都加起来,如果遇到负数或者非数字,停止遍历 
var strs = ["10", "20", "abc", "-20", "30"]
var index = 0
var sum = 0
while let num = Int(strs[index]), num > 0 {
	sum += num
	index += 1 
}
print(sum)
空合并运算符
public func ?? <T>(optional: T?, defaultValue: @autoclosure () throws -> T?) rethrows -> T?
public func ?? <T>(optional: T?, defaultValue: @autoclosure () throws -> T) rethrows -> T
  • a ?? b
  • a 必须是可选项
  • b 是可选项 或者 不是可选项
  • b 跟 a 的存储类型必须相同
  • 如果 a 不为nil,就返回 a
  • 如果 a 为nil,就返回 b
  • 如果 b 不是可选项,返回 a 时会自动解包

?? 返回的类型取决于最右边的类型

let a: Int? = 1
let b: Int = 2
let c = a ?? b // c Int, 1


let a: Int? = nil
let b: Int = 2
let c = a ?? b // c Int, 2

??if let配合使用

let a:Int? = nil
let b: Int? = 2
if let c = a ?? b{
	print(c)
}
// 等价于 if a != nil || b != nil

字符串插值消除警告

var name: String? = "Mk"
print("\(name)")
// String interpolation produces a debug description for an optional value; did you mean to make this explicit?

print("\(name!)")

print("\(String(describing: name))")

print("\(name ?? "")")

多重可选项
var num1: Int? = nil
var num2: Int?? = num1
var num3: Int?? = nil

(num2 ?? 1) ?? 2 // 2
(num3 ?? 1) ?? 2 // 1

在这里插入图片描述

也可以用lldb指令 frame variable -R 或者 fr v -R 查看区别

(lldb) fr v -R num1
(Swift.Optional<Swift.Int>) num1 = none {
  some = {
    _value = 0
  }
}

(lldb) fr v -R num2
(Swift.Optional<Swift.Optional<Swift.Int>>) num2 = some {
  some = none {
    some = {
      _value = 0
    }
  }
}

(lldb) fr v -R num3
(Swift.Optional<Swift.Optional<Swift.Int>>) num3 = none {
  some = some {
    some = {
      _value = 0
    }
  }
}

因此,上面的(num2 ?? 1) ?? 2第一个空合运算符,代表num2是有值的,只是一个有值的空盒子包装,第二个运算符就是nil,取2即可。

2.6 枚举内存布局

示例一:
可以看到该枚举实际占用了25个字节,内存对齐8个字节,因此实际分配了32个字节
可以看到前 24个字节用来存储,第25个字节用来区分具体哪个case,可以看出,这里其实有四种case,分别对应第25个字节的 01,02,03,00,如果是test0,test1,test2这三个case,都是03类型,然后通过第一个字节进行值区分

enum TestEnum {
    case test0
    case test1
    case test2
    case test3(Int)
    case test4(Int, Int)
    case test5(Int, Int, Int)
}

print(MemoryLayout<TestEnum>.size)  // 25
print(MemoryLayout<TestEnum>.stride) // 32
print(MemoryLayout<TestEnum>.alignment) // 8


/*
01 00 00 00 00 00 00 00
02 00 00 00 00 00 00 00
03 00 00 00 00 00 00 00
02
00 00 00 00 00 00 00
*/
var test1 = TestEnum.test5(1, 2, 3)
withUnsafePointer(to: &test1) { ptr in
    print("address-\(ptr)")
}
print("开始")


/*
04 00 00 00 00 00 00 00
05 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
01
00 00 00 00 00 00 00
*/
test1 = .test4(4, 5)
/*
 06 00 00 00 00 00 00 00
 00 00 00 00 00 00 00 00
 00 00 00 00 00 00 00 00
 00
 00 00 00 00 00 00 00
 */
test1 = .test3(6)
/*
 02 00 00 00 00 00 00 00
 00 00 00 00 00 00 00 00
 00 00 00 00 00 00 00 00
 03
 00 00 00 00 00 00 00
 */
test1 = .test2;
/*
 01 00 00 00 00 00 00 00
 00 00 00 00 00 00 00 00
 00 00 00 00 00 00 00 00
 03
 00 00 00 00 00 00 00
 */
test1 = .test1;
/*
 00 00 00 00 00 00 00 00
 00 00 00 00 00 00 00 00
 00 00 00 00 00 00 00 00
 03
 00 00 00 00 00 00 00
 */
test1 = .test0;

示例二:
该示例如果按上面的套路,应该是24 + 1 + 1 = 26个字节,但是实际只需要25个字节,可以看到这边的Bool类型这个字节可以用来做区分不同case,因此只需要25个字节即可,编译器不至于那么傻

enum TestEnum {
    case test0
    case test1
    case test2
    case test3(Int)
    case test4(Int, Int)
    case test5(Int, Int, Int,Bool)
}

print(MemoryLayout<TestEnum>.size)  // 25
print(MemoryLayout<TestEnum>.stride) // 32
print(MemoryLayout<TestEnum>.alignment) // 8


/*
 01 00 00 00 00 00 00 00
 02 00 00 00 00 00 00 00
 03 00 00 00 00 00 00 00
 81
 00 00 00 00 00 00 00
*/
var test1 = TestEnum.test5(1, 2, 3, true)
withUnsafePointer(to: &test1) { ptr in
    print("address-\(ptr)")
}
print("开始")


/*
 04 00 00 00 00 00 00 00
 05 00 00 00 00 00 00 00
 00 00 00 00 00 00 00 00
 40
 00 00 00 00 00 00 00
*/
test1 = .test4(4, 5)
/*
 06 00 00 00 00 00 00 00
 00 00 00 00 00 00 00 00
 00 00 00 00 00 00 00 00
 00
 00 00 00 00 00 00 00
 */
test1 = .test3(6)
/*
 02 00 00 00 00 00 00 00
 00 00 00 00 00 00 00 00
 00 00 00 00 00 00 00 00
 C0
 00 00 00 00 00 00 00
 */
test1 = .test2;
/*
 01 00 00 00 00 00 00 00
 00 00 00 00 00 00 00 00
 00 00 00 00 00 00 00 00
 C0
 00 00 00 00 00 00 00
 */
test1 = .test1;
/*
 00 00 00 00 00 00 00 00
 00 00 00 00 00 00 00 00
 00 00 00 00 00 00 00 00
 C0
 00 00 00 00 00 00 00
 */
test1 = .test0;

示例三:
该示例Bool类型不是最后一位,因此内存对齐的原因,实际都需要32位来支撑,但是不需要额外的一个字节来区分不同case,Bool类型占用的那8个字节,低地址最后一个就可以用来区分不同case,高地址01就可以用来表示Bool值了

enum TestEnum {
    case test0
    case test1
    case test2
    case test3(Int)
    case test4(Int, Int)
    case test5(Int, Int, Bool, Int)
}

print(MemoryLayout<TestEnum>.size)  // 32
print(MemoryLayout<TestEnum>.stride) // 32
print(MemoryLayout<TestEnum>.alignment) // 8


/*
 01 00 00 00 00 00 00 00
 02 00 00 00 00 00 00 00
 01 00 00 00 00 00 00 80
 03 00 00 00 00 00 00 00
*/
var test1 = TestEnum.test5(1, 2, true, 3)
withUnsafePointer(to: &test1) { ptr in
    print("address-\(ptr)")
}
print("开始")


/*
 04 00 00 00 00 00 00 00
 05 00 00 00 00 00 00 00
 00 00 00 00 00 00 00 40
 00 00 00 00 00 00 00 00
*/
test1 = .test4(4, 5)
/*
 06 00 00 00 00 00 00 00
 00 00 00 00 00 00 00 00
 00 00 00 00 00 00 00 00
 00 00 00 00 00 00 00 00
 */
test1 = .test3(6)
/*
 02 00 00 00 00 00 00 00
 00 00 00 00 00 00 00 00
 00 00 00 00 00 00 00 C0
 00 00 00 00 00 00 00 00
 */
test1 = .test2;
/*
 01 00 00 00 00 00 00 00
 00 00 00 00 00 00 00 00
 00 00 00 00 00 00 00 C0
 00 00 00 00 00 00 00 00
 */
test1 = .test1;
/*
 00 00 00 00 00 00 00 00
 00 00 00 00 00 00 00 00
 00 00 00 00 00 00 00 C0
 00 00 00 00 00 00 00 00
 */
test1 = .test0;

总结:
1.一个字节存储成员值(多个case的情况,如果有Bool的情况下,可以共用这8个字节或者这1个字节)
2.N个字节存储关联值(N取占用内存最大的关联值),任何一个case的关联值/原始值都共用这N个字节

3.AT&T汇编和lldb指令

3.1寄存器和内存

通常,CPU会先将内存中的数据存储到寄存器中,然后再对寄存器中的数据进行运算
假设内存中有块红色内存空间的值是3,现在想把它的值加1,并将结果存储到蓝色内存空间

  • 1.CPU首先会将红色内存空间的值放到rax寄存器中: movq 红色内存空间, %rax
  • 2.然后让rax寄存器与1相加:addq $0x1, %rax
  • 3.最后/z将值赋值给内存空间:movq %rax, 蓝色内存空间
    在这里插入图片描述
    x86和x64汇编根据编译器的不同,有两种书写格式
  • Intel:Window派系
  • AT&T:Unix派系
    咱们iOS开发,最主要的汇编语言是:
  • AT&T汇编 — iOS模拟器
  • ARM汇编 — iOS真机

以下是常见汇编指令

项目AT&TInter说明
寄存器命名%raxrax
操作数顺序movq %rax, %rdxmov rdx, rax将rax的值赋值给rdx
常数、立即数movq $3, %rax
movq $0x10 %rax
mov rax, 3
mov rax, 0x10
将3赋值给rax,将0x10赋值给rax
内存赋值movq $0xa, 0x1ff7(%rip)mov qword ptr [rip+0x1ff7], 0xa将0xa赋值给地址为rip+0x1ff7的内存空间
取内存地址leaq -0x18(%rbp), %raxlea rax, [rbp-0x18]将rbp-0x18这个地址赋值给rax
jmp指令jmp *%rdx
jmp 0x4001002
jmp *(%rax)
jmp rdx
jmp 0x4001002
jmp [rax]
call 和 jmp写法类似
操作数长度movl %eax, %edx
movb $0x10 %al
leaw 0x10(%dx), %ax
mov edx eax
mov al 0x10
lea ax, [dx + 0x10]
b=byte (8-bit)
s = short(16-bit integer or 32-bit floating point)
w = word (16-bit)
l = long (32-bit integer or 64-bit floating point)
q = quad (64 bit)
t = ten bytes (80 -bit floating point)

16个常用寄存器

  • rax,rbx,rcx,rdx,rsi,rdi,rbp,rsp
  • r8,r9,r10,r11,r12,r13,r14,r15

寄存器具体用途

  • rax、rdx常作为函数返回值使用
  • rdi、rsi、rdx、rcx、r8、r9等常用于存放函数参数
  • rsp和rbp用于栈操作
  • rip作为指令指针(存储着CPU下一条要执行的指令的地址,一旦CPU读取一个指令,rip会自动指向下一个指令)

3.2 lldb常用指令

  • 读取寄存器的值
    register read/格式 、 register read/x

  • 修改寄存器的值
    register write 寄存器名称 数值 、 register write rax 0

  • 读取内存中的值
    x/数量-格式-字节大小 内存地址
    x/3xw 0x00000100

  • 格式
    x是16进制 f是浮点 d是十进制

  • 字节大小
    b - byte 1字节
    h -half word 2字节
    w - word 4字节
    g - giant word 8字节

  • 修改内存中的值
    memory write 内存地址 数值
    memory write 0x000001010

  • thread step-over、next、n
    单步运⾏行行,把子函数当做整体⼀一步执⾏行行(源码级别)

  • thread step-in、step、s
    单步运⾏行行,遇到子函数会进⼊入子函数(源码级别)

  • thread step-inst-over、nexti、ni
    单步运⾏行行,把子函数当做整体⼀一步执⾏行行(汇编级别)

  • thread step-inst、stepi、si
    单步运⾏行行,遇到子函数会进⼊入子函数(汇编级别)

  • thread step-out、finish
    直接执⾏行行完当前函数的所有代码,返回到上一个函数(遇到断点会卡住)

看完上述基本的汇编指令和lldb指令,我们从汇编的角度看下枚举的汇编代码

enum TestEnum {
    case test0
    case test1
    case test2
    case test3(Int)
    case test4(Int, Int)
    case test5(Int, Int, Int)
}

print(MemoryLayout<TestEnum>.size)  // 25
print(MemoryLayout<TestEnum>.stride) // 32
print(MemoryLayout<TestEnum>.alignment) // 8

var test1 = TestEnum.test5(10, 20, 30) // 断点到此处,开启汇编Debug模式

可以看到如下汇编

    0x100001b5d <+2845>: movq   $0xa, 0x3b48(%rip)        ; Swift01.date : Swift01.Date + 28
    0x100001b68 <+2856>: leaq   0x3b41(%rip), %rax        ; Swift01.test1 : Swift01.TestEnum
    0x100001b6f <+2863>: movq   $0x14, 0x3b3e(%rip)       ; Swift01.test1 : Swift01.TestEnum + 4
    0x100001b7a <+2874>: movq   $0x1e, 0x3b3b(%rip)       ; Swift01.test1 : Swift01.TestEnum + 12
    0x100001b85 <+2885>: movb   $0x2, 0x3b3c(%rip)        ; Swift01.test1 : Swift01.TestEnum + 23

记住如下两条原则:

  • rip存储的是指令的地址
  • CPU要执行的下一条指令地址就存储在rip中
  • si 汇编单步调试,进入函数

分析如下

0x100001b5d <+2845>: movq   $0xa, 0x3b48(%rip)  
可以看到 rip的地址就是下一条的地址  0x100001b68
0x3b48 + 0x100001b68 = 0x1000056B0  把0xa 存入  0x1000056B0 q占用八个字节 以下依次类推
0x100001b68 <+2856>: leaq   0x3b41(%rip), %rax  0x100001b6f + 0x3b41 = 0x1000056B0 地址放入 rax
0x100001b6f <+2863>: movq   $0x14, 0x3b3e(%rip)  0x1000056B8 存 20
0x100001b7a <+2874>: movq   $0x1e, 0x3b3b(%rip)  0x1000056C0 存 30
0x100001b85 <+2885>: movb   $0x2, 0x3b3c(%rip)   0x1000056C8 
存 2 类型关联值  b 一个字节

看到对应的地址下存储的值
在这里插入图片描述

规律知识点
1.内存地址格式为 0x4bdc(%rip),一般是全局变量,全局区(数据段)
2.内存地址格式为 -0x78(%rbp),一般是局部变量,栈空间
3.内存地址格式为 0x10(%rax),一般是堆空间
4. 小括号里面放的都是内存地址
5. r开头: 64bit 8个字节
e开头: 32bit 4个字节
ax,bx,cx:16bit, 2个字节
ah,al:1个字节
bh,bl
movq 0xa %rax
0xa 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0

第一天结束,明天继续!!!!!!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值