Swift汇编看枚举、类、闭包

2,353 阅读2分钟

本篇文章代码均在Apple Swift version 5.0.1,学习Swift的笔记

枚举

简单枚举,通常这样子写

enum TestEnum{
    case test1 = 1,test2 = 2,test3 = 3
}

c中,TestEnum占用空间是1位[就是8字节],那么我们在Swift中是怎么样的呢?

通过测试代码来看枚举占用的空间

enum Password {
	case number(Int,Int,Int,Int)
	case other
}
enum TestEnum {
	case test1,test2,test3,test4
}
var testum = TestEnum.test1
var p1 = MemoryLayout.stride(ofValue: testum)
var p2 = MemoryLayout.size(ofValue: testum)
var p3 = MemoryLayout.alignment(ofValue: testum)
print("分配:\(p1)  占用:\(p2) 对齐:\(p3)")
//分配:1  占用:1 对齐:1


var pwd = Password.number(3, 5, 7, 8)//字节 8*4 =32
//pwd = .other//同样的变量还是32字节
 p1 = MemoryLayout.stride(ofValue: pwd)// 40
 p2 = MemoryLayout.size(ofValue: pwd)//33
 p3 = MemoryLayout.alignment(ofValue: pwd)//8
print("分配:\(p1)  占用:\(p2) 对齐:\(p3)")

由代码可以得出:

简单枚举,case当默认为数字,占用空间为1字节,关联枚举则占用的比较多,我们主要从内存上分析一下为什么占用这么多

首先在关键代码打断点查看内存布局

enum Password {
	case number(Int,Int,Int,Int)
	case other
}
//此处打断点
var pwd = Password.number(3, 5, 7, 8)//字节 8*4 =32

按照我们正常猜想,4个int类型的,应该是要占32字节,为什么系统是占用了33个字节,另外一个字节存储了什么呢?

这种方式在oc中可以正常使用,但是在Swift中不行。

我们寻求其他方式来查看变量内存地址,mj大哥的小工具 使用很简单或者设置DEBUG->DEBUG Workflow ->Always Show Disassembly

可以看到他们的内存地址是0x1000088a0,或使用lldb 命令查看数据

(lldb) x/9x 0x1000088a0
0x1000088a0: 0x00000003 0x00000000 0x00000005 0x00000000
0x1000088b0: 0x00000007 0x00000000 0x00000008 0x00000000
0x1000088c0: 0x00000000

看到了我们赋值的3/5/7/8后边是0x0,然后我们再测试下边的代码查看内存

let pwd2 = Password.other//同样的变量还是33字节

查看汇编

movq $0x1,0x4b3b(%rip) //含义是将字面量1,赋值给右边的寄存器 rip是CPU执行下句编码的地址,那么这句含义就是将1赋值给rip+0x4b3b的内存地址。

内存大小不变,变的是最后一位由0x0变成了0x1

将枚举稍微修改一下

enum Password {
	case number1(Int,Int,Int,Int)
	case number2(Int,Int,Int)
	case number3(Int,Int)
	case number4(Int)
	case other
}

再测试下代码

let pwd2 = Password.other//同样的变量还是33字节

let pwd4 = Password.number2(5, 6, 7)//同样的变量还是33字节

let pwd5 = Password.number4(8)//同样的变量还是33字节

其实枚举就是的思路和联合体比较相似,枚举占用的空间是其中最大元素的空间+1,就是这个枚举占用的空间。 利用最后一位来分辩是哪个类型,不存在switch .case是调用函数的思路的。

同样可以测试

enum Password {
	case number1(Int,Int,Int,Int)
	case number2(Int,Int,Int)
	case number3(Int,Int)
	case number4(Int)
	case other
}

同样占用了33位,前32位是存储值,第33位存储哪个类别。

报错解析

let 和var修饰的到底是哪些内存?

let s2 = Point(x: 10, y: 12)
s2 = Point(x: 10, y: 10)//报错原因是let修饰的结构体 内存区域(栈) 所以值和属性都不能改
s2.x = 11//报错
s2.y = 12//报错

let ret = PointClass()
ret.x = 0
ret.y = 0
ret = PointClass()//报错 报错原因是let修饰的类 指针(栈) 所以指针不能改,但是指针指向的属性能改

其实let和const类似,修饰的是直接接触的指针,只是指针不能变,不是指针指向的数据不能变更。

闭包

一个函数和它所捕获的常量、变量环境组合起来,成为闭包

  • 一般指定义在函数内部的函数
  • 一般它捕获的是外层函数的局部变量/常量
typealias Clo = (Int) -> Int
func getFunc () -> Clo {
	var num = 5 //局部变量 赋值到了堆空间来保证变量的值。每次调用都访问了堆空间了。
	func plus(_ v:Int) ->Int{
		num += v
		return num//断点B
	}
	return plus//断点A
}
var f = getFunc()
print(f(1)) //5+1 = 6
print(f(2)) //6 + 2 =8

var f2 = getFunc()//每次调用都会调用新的堆空间的 num,他们是分开没联系的。
print(f2(3))// 5 + 3 = 8

每次执行getFunc(),都会重新申请栈空间来存储numff2的栈空间的独立的。 通过汇编看下栈空间的值的变化,在断点A地方,输出num的占空间地址

得到了存储num的地址。首先在赋值之前查看内存

x/3xg 0x00000001005735f0
0x1005735f0: 0x0000000100002080 0x0000000000000002
0x100573600: 0x00007fff76760ab9

然后运行到断点B,再次打印num的值

(lldb) x/3xg 0x00000001005735f0
0x1005735f0: 0x0000000100002080 0x0000000000000002
0x100573600: 0x0000000000000005

可以看到寄存器的值和num值一致。

然后运行一次,第二次断点到B位置

(lldb) x/3xg 0x00000001005735f0
0x1005735f0: 0x0000000100002080 0x0000000200000002
0x100573600: 0x0000000000000006

由此证明了,闭包将局部变量复制到了栈上。

其实闭包就像一个class(类),局部变量像属性(类的成员变量),类定义的函数类似闭包的代码。

class CloClass{
    var num = 6
    func plus(_ v:Int) -> Int {
        num += v
        return num
    }
}

类的实例化在堆区,而闭包也是在堆区,类有成员变量,闭包有属性,类可以定义函数,闭包也可以定义函数。。。

闭包原理已经明白了,来两道菜压压惊。

最后来两道题,探究输出结果是什么?

题目一

typealias FnClo = (Int) -> (Int,Int)
func getFns() -> (FnClo,FnClo) {
    var num1 = 0
    var num2 = 0
    func plus(_ i:Int) -> (Int,Int){
        num1 += i
        num2 += i << 1
        return (num1,num2)
    }
    func mnus(_ i:Int) -> (Int,Int){
        num1 -= i
        num2 -= i << 1
        return (num1,num2)
    }
    return (plus,mnus)
}

let (p,m) = getFns()
print(p(5))
print(m(4)) 
print(p(3))
print(m(2))

题目二

结果输出什么?为什么?

class Person {
	var age = 9
}
typealias Clo = (Int) -> Int
func getFunc () -> Clo {
	var p = Person()
	func plus(_ v:Int) ->Int{
		p.age += v;
		return p.age
	}
	return plus
}
var f = getFunc() 
print(f(3))

答案在这里,有更多的问题可以在这里提问哦

inout

当给inout传递一个变量,直接地址传递进去即可,那么传递进去计算属性,又如何呢?

struct Photo {
	
	var age:Int {
		get{
			return height/2
		}
		set{
			height =  newValue * 2
		}
	}
	var height = 12
}
var p = Photo(height: 10)

func test(_ v: inout Int) {
	v = 9
}
test(&p.age)

查看汇编看到其实传递的还是p.age的地址,不过地址通过getter之后然后将地址传递到函数内部的。

当设置了是属性观察器则是采用Copy-In-Copy-Cout策略进行赋值和调用will和set函数。

struct Photo {
	
	var age:Int {
		willSet{
			print("will\(newValue)")
		}
		didSet{
			print("didset old:\(oldValue)")
		}
	}
}
var p = Photo(age: 5)
func test(_ v: inout Int) {
	v = 9
}
test(&p.age)//此处打断点

然后查看汇编

inout如果有物理内存地址,且没有属性观察器,直接将内存地址传入函数。 如果是计算属性,或者设置了属性观察器,采取Copy-In-Copy-out做法,调用该函数,先复制实参的值,产生副本,将副本的内存地址传入函数(副本进行引用传递),在函数内部可以修改副本的值,函数返回后,将副本的值覆盖实参的值。

属性

属性分为实例属性和类型属性,实例属性是通过实例可以访问的属性,类型属性是通过类来访问的属性。

我们定义个实例属性age和类属性level

struct Propty {
    var age = 10
    static var level = 11
}
var pro = Propty()
pro.age = 11;
var level = Propty.level;

static修饰的内存只会分配一次,类可以访问,当这个也可以实现单例,本质是执行的dispatch_once_f

class fileManger {
    public static let  manger:fileManger = fileManger()
    private init(){//设置私有,外部不可访问
        print("init")
    }
    open func close()  {
        print("close")
    }
    public  func open()  { //mode 可访问
        print("open")
    }
}
var file = fileManger.manger
file.open()
file.close()
file.init()//error:init' is inaccessible due to 'private' protection level

其实inout本质是引用传递(地址传递),分情况分为地址直接传递和副本地址传递。

其他

下标Subscript

struct Point {
	var x = 0,y = 0
	
}
class Pointcls {
	var p = Point()
	
	subscript (index index:Int) ->Point{
		set{
			print("set")
			p = newValue
		}
		get{print("get"); return p }
	}
	
}
var p = Pointcls()
p[index: 0] = Point(x: 1, y: 2)
// set
// get

其实从代码也可以看出来,下标就是执行的setget方法,这点就不用汇编分析了。

初始化器

每个类一定指定一个init,不写的话,系统默认生成init。另外一个是便捷生成初始化器convenience,必须指定调用初始化器。

class Size {
	var width = 0,height = 0
	convenience init(_ w:Int,_ h:Int) {
		self.init(width:w,height:h)
		//code
	}
	convenience init(w:Int) {
		self.init(width:w,height:0)
		//code
	}
	convenience init(h:Int) {
		self.init(width:0,height:h)
		//code
	}
//私有外部不能访问
	private init(width:Int,height:Int) {
		self.width = width
		self.height = height
	}

}

Size设计了一个init三个便捷初始化器,init可以做一些必要的配置,另外的便捷初始化可以单独处理,这样子,关键代码不会漏掉。

X.self type(of:X)

在OC中有obj.class,在Swift中则是obj.Self与之对应的

var t = 1.self
var t2 = type(of: [1])
print(t2,t)//[Int].type Int.type

可以使用is关键字判断是否是某个类

var age = 1
if age is Int{
    print("age is int")
}else if age is Double{
    print("age is int")
}
//age is int

self代表当前类,而不是特定的类。

protocol Run {
    func test() -> Self
}
class RunSub: Run {
    func test() -> Self {
        return RunSub.init()//报错 Cannot convert return expression of type 'RunSub' to return type 'Self'
    }
}

报错了,因为self指当前类,可以理解泛型的指针,RunSub可能有子类,不能直接返回RunSub.init(),这样子相当于类型写死了,通常我们这样子写

class RunSub: Run {
    required init(){
        //code
    }
    func test() -> Self {
        let ty = type(of: self)
        return ty.init()
    }
}

required这样子保证了子类必须实现init函数,类型也是type(of: self)的类型。

String

SwiftString也是采用了小数据优化,大数据不优化方案,在OC中叫tag pointer,其实就是小数据不用指针,大数据使用指针,Swift中是长度大于15使用指针存储,不大于15直接存储数据。 当不大于15String是存储在数据段,当进行append()操作,会将数据段数据复制到栈区,而且使用指针存储数据。 长度小余15在内存布局

//var str2 = "123456789012345"// 0x3837363534333231 0xef35343332313039
//var str2 = "12345678901234"   // 0x3837363534333231 0xee00343332313039
//var str2 = "1234567890123"      // 0x3837363534333231 0xed00003332313039

长度大于15在内存的布局

var str2 = "123456789012345678901234"
//0xd000000000000019 0x80000001000056c0 0x19是字符串长度

0x80000001000056c0需要加上0x20才是字符串的真正地址,类似OCmask

//str2真实地址 :0x80000001000056c0 - 0x7fffffffffffffe0 = 0x1000056E0
//0x1000056c0 + 0x20 = 0x1000056E0
/*0x1000056e0: 31 32 33 34 35 36 37 38 39 30 31 32 33 34 35 36  1234567890123456
  0x1000056f0: 00 0a 00 20 00 00 00 00 00 00 00 00 00 00 00 00
*/

append()之后

str2的地址
0xf000000000000019 0x000000010340a490
进行+0x20
0x10340a490+0x20=0x10340a4b0
0x10340a4b0的数据
x/4xg 0x10340a4b0
0x10340a4b0: 0x3837363534333231 0x3635343332313039
0x10340a4c0: 0x3433323130393837 0x0004000000000035

指针地址

array

数组占8字节,指向了存储数据的真实地址。默认数组大小是4,负载超过0.5则进行扩容,扩容系数是2。

var arr = [Int]()
for i in 1...3{
	arr.append(i)
}
0x00007fff90381cc0 0x0000000200000002 
0x0000000000000003 0x0000000000000008 
0x0000000000000001 0x0000000000000002 
0x0000000000000003 0x0004003c00000000

for i in 1...9{
	arr.append(i)
}
0x00007fff90381cc0 0x0000000200000002 0x0000000000000009
0x0000000000000020 0x0000000000000001 0x0000000000000002
0x0000000000000003 0x0000000000000004 0x0000000000000005
0x0000000000000006 0x0000000000000007 0x0000000000000008
0x0000000000000009 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x00007fff59610004 0x00007fff90381d58
0x0000000000000000 0x0000000000000000

可选项类型

可选类型本质上是Optional类型

var age:Int? = 10

相当于

var age1:Optional<Int> = .some(10)

?只是语法糖,更加简单。

溢出运算符 &+ &- &* &/

高级运算符溢出运算符,在大于最大值+1等于最下值,最小值-1是最大值。

var v:Int8 = Int8.max &+ 1
print(v) // -128
v = Int8.min &- 1
print(v)//127

溢出加法

运算符重载

运算符==、-、+、+=、-=、/、*可以自己实现重载,在OC中是不支持的。


struct Point:Equatable{
	var x = 0,y = 0
	//中缀加好
	static func +(p1:Point,p2:Point) -> Point{
		return Point(x: p1.x+p2.x,y: p1.y+p2.y)
	}
	//中缀减法
	static func -(p1:Point,p2:Point) -> Point{
		return Point(x: p1.x-p2.x,y: p1.y - p2.y)
	}
	//中缀乘法
	static func *(p1:Point,p2:Int) -> Point{
		return Point(x: p1.x*p2,y: p1.y*p2)
	}
	//h中缀除法
	static func /(p1:Point,p2:Int) -> Point{
		if p2 == 0{
			return p1
		}
		return Point(x: p1.x/p2,y: p1.y/p2)
	}
	//前缀 减号
	static prefix func -(p1:Point) -> Point{
		return Point(x: -p1.x, y: -p1.y)
	}
	static postfix func --(p1: inout Point) {
		p1.x -= 1
		p1.y -= 1
	}
	static postfix func ++(p1: inout Point) {
		p1.x += 1
		p1.y += 1
	}
	
	
	static func == (p1:Point,p2:Point)->Bool{
		if p1.x == p2.x  && p1.y == p2.y{
			return true
		}
		return false
	}
	static func === (p1:Point,p2:Point)->Bool{
		if p1.x == p2.x  && p1.y == p2.y {
			return true
		}
		return false
	}
}

var p1 = Point(x: 10, y: 10)
var p3 = p1 + p1
print(p1 == p1)
print(p1 === p3)

有效区域

权限控制分为5个层次

  • open最高权限,可以被任意模块访问和继承
  • public次高权限,可以被任意模块访问,不能被其他模块继承重写
  • internal默认权限,允许在当前模块访问,不允许在其他模块访问
  • fileprivate 允许当前文件中访问
  • private 允许当前定义有效范围访问

private不一定比fileprivate小,在类中定义属性,fileprivate访问有效区域大,是整个文件,private是当前类中,在相同文件全局变量中,privatefileprivate有效区域是整个文件。

class test {
//	private class testSub{}
//	fileprivate class testSub2:testSub{}//报错因为testSub有效区域是test函数,testSub2是真个文件。
}

private class testSub{}
// private 和 fileprivate等价
fileprivate class testSub2:testSub{}

当一个属性读权限高,写权限低的时候

class Person {
	private(set) var age = 0
	fileprivate(set) var weight = 100
}

人的年龄不是单独设置,是根据年限变化的,体重是根据吃东西变化的,不是单独可以设置的,所以可读不可写。

闭包逃逸和非逃逸

  • 非逃逸就是在函数体内执行
  • 逃逸闭包是不清楚是否在函数体内执行需要加关键字@escaping,一般用到的self也需要weak处理
//非逃逸闭包
func test (_ fn:()->()){
	fn()
}
//逃逸闭包
func test2 (_ fn:@escaping ()->()) -> ()->(){
	return fn
}
func test3 (fn:@escaping ()->()) -> Void{
	DispatchQueue.global().async {
		fn()
	}
}
//weak 修饰的逃逸闭包
public class Run {
	var age = 0
	func test4(_ fn:@escaping ()->()) -> Void {
		DispatchQueue.global().async {
			fn()
			print(self.age)
		}
	}
}

内存访问错误(Simultaneous accesses )

Simultaneous accesses 

var step = 1
func plus(_ n:inout Int)  {
	 n += step
}
plus(&step)

只需要稍微改动下即可,数值型,copy是在另外一块内存存储step的值。

var copy = step
plus(&copy)

同一块内存只能同时多度单写,不能读写同时操作

匹配模式

自定义运算符,并和switch混合使用,自定义了几个运算符,然后重载了Int的对比函数。

prefix operator ~=;
prefix operator ~>=;
prefix operator ~<=;
prefix operator ~<;

prefix func ~= (_ v:Int) ->  ((Int)->Bool){return{ $0>v}}
prefix func ~>= (_ v:Int) -> ((Int)->Bool){return{ $0 >= v}}
prefix func ~<= (_ v:Int) -> ((Int)->Bool){return{$0 <= v}}
prefix func ~< (_ v:Int) ->  ((Int)->Bool){return{$0 < v}}
extension Int{
    static func ~=(pattern:(Int)->Bool,value:Int) -> Bool{
        return pattern(value)
    }
}
var age = 10
switch age {
case ~>=0:
    print("~>=0")
case ~<=100:
    print("~<=100")
default:
    break
}

把age当做参数,~>=是前置运算符,返回一个(Int)->Bool {return{$0 < v}}闭包,返回值是Bool类型,根据这个Bool值进行判断是否进入这个case

资料参考