Swift笔记2:值类型和引用类型

978 阅读10分钟

延迟存储属性-lazy

class JClass {
    lazy var age:Int = 18
}

print(class_getInstanceSize(JClass.self))
var person = JClass()
print(person.age)
print("end")

metadata占8字节,refcount占8字节,Int类型占8字节应该是24字节,多出来的8字节怎么回事儿呢

可以观察到age属性占了8字节,另外开辟了1字节用来标记延迟存储属性age是否被访问过,1表示未被访问过,值为空,0表示被访问过,有值,通过SIL观察

Swift Intermediate Language (SIL)查看一下lazy_getter的作用

文档提到lazy属性存储的是一个可选类型Optional,但是我们使用的时候不需要考虑解包的问题,那么是不是可以理解成调用setter方法的时候系统帮我们处理成了可选类型Optional类型进行存储,调用getter方法时系统帮我们完成了解包的过程呢??

setter方法存储的就是一个可选类型

getter方法帮我们完成了解包的过程

lazyOptional的关系,lazy修饰的属性编译器帮我们处理成了可选类型进行存储,这一点两者是相同的,不同的是访问getter方法时 lazy修饰的属性编译器帮我们完成了解包的操作,而可选类型需要我们自己解包

总结

  • 延迟存储属性必须有一个默认的不为空的初始值
  • 延迟存储属性第一次访问的时候才会被赋值
  • 延迟存储属性不能保证线程安全
  • 延迟存储属性会另外开辟1字节的空间存储枚举值

延迟存储属性是在需要用到的时候再去装载,这是一种牺牲一点点访问效率来换取空间的做法,一定程度上可以加快可执行文件加载到内存的速度,如果延迟存储属性的值为空的话,那访问效率的牺牲就没有任何意义了。就好像我要竞标一个项目,需要用到你的东西,你拍着胸脯告诉我先不用把东西拿走,需要的时候随时来拿,于是乎我信了你的邪只把必要的东西准备好拿到竞标现场,当我开始走竞标流程的时候要用你的东西,我派人去取,这个时候你告诉我你没有这个东西,我是不是白跑了,时间是不是耽误了,然后还要再花时间从其它地方弄到这个东西,这个时候我是不是想弄死你,你没有早说啊,我也好有个准备。

多个线程同时访问一个延迟存储属性不能保证属性只被赋值一次

类型属性

class JClass {
    static var age = 18
}
var person = JClass()

通过SIL文件查看

类型属性是通过指针访问的,这里的once方法其实就是swift_once方法,也就是这块全局空间只会被开辟一次,从SIL看到执行getter方法时才开辟的空间并存储初始值,注意类型属性也必须有一个初始值,也可以起到延迟加载的效果

GCD的方法dispatch_once_t就保证了不同线程访问类型属性时线程安全

OC实现单例子

@interface JClass : NSObject

@end



@implementation JClass

+ (instancetype)shareInstance{
    static JClass *class = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        class = [[JClass alloc] init];
    });
    return class;
}

@end

swift实现单例

//通过JClass.instance访问单例
class JClass {
    static let instance:JClass = JClass()
    private init(){}
}

结构体的初始化

在使用class的时候如果我们声明了一个存储属性而没有赋初始值,那么会报一个Class 'JClass' has no initializers错误

解决办法有三种:

第一:赋初始值

class JClass {
    var age:Int = 18
}

第二:声明为可选

class JClass {
    var age:Int?
}

第三:创建初始化方法

class JClass {
    var age:Int
    init(_ a:Int) {
        self.age = a
    }
}

使用的时候必须使用初始化方法给属性赋初值,否则会报Missing argument for parameter 'age' in call错误

那如果是结构体struct呢,不赋初始值也可以,也不需要声明为可选,也不需要创建初始化方法,在使用的时候赋初始值就可以了,好像编译器帮我们创建了初始化方法

查看SIL

如果我自己创建了初始化方法呢

struct JClass {
    var age:Int
    init(_ a:Int) {
        self.age = a
    }
}
var a = JClass(10)

系统就不帮我创建了

结构体是值类型 class是引用类型

func test() {
    var age:Int = 18
    var age1 = age
    age = 20
    age1 = 30
    print("age=\(age),age1=\(age1)")
}

test()

值类型就是内存里边直接存储的就是值,而不是地址 var age1 = age这一行代码相当于直接将字面量18赋值给age1

内存模型图

我们在方法内分配的局部变量存储在栈区,由系统负责分配回收,栈区是连续的先进后出,从高地址向低地址延伸,那么怎么知道地址是在哪个区呢?

struct St1 {
    var age:Int = 18
    var name:String = "SC"
}

var s1 = St1()
var s2 = s1

s1.age = 30
print("s1.age=\(s1.age)")
print("s2.age=\(s2.age)")

s1修改之后s2的值并没有跟着修改

这时s1s2是两块独立的内存空间,所以修改s1不会影响到s2

如果修改structclass

class St1 {
    var age:Int = 18
    var name:String = "SC"
}

var s1 = St1()
var s2 = s1

s1.age = 30
print("s1.age=\(s1.age)")
print("s2.age=\(s2.age)")

var s2 = s1这行代码是值拷贝,但是拷贝的是一个地址,多个地址指向同一块内存空间,通过哪一个地址访问的都是同一块内存空间

对于这种值类型中包含引用类型也是一样,两个结构体中包含同一个地址

class JClass {
    var age:Int = 18
}


struct JStruct {
    var age:Int = 18
    var jClass:JClass = JClass()
}

var jClass = JClass()
var s2 = JStruct(age: 20, jClass: jClass)
var s3 = s2
s2.jClass.age = 30
print("s2.jClass.age=\(s2.jClass.age)")
print("s2.jClass.age=\(s3.jClass.age)")
print("end")

总结:

值类型就好像你我各自下载一份文档自己修改,谁都不影响谁,引用类型就好像你我同时访问一个在线文档,我们同时编辑,我能修改你的,你也能修改我的

mutating关键字

如果我想用结构体实现一个栈

struct JStruct {
    var items = [Int]()
    func push(_ item:Int) {
        items.append(item)
    }
}

编译不通过

通过SIL来查看

struct JStruct {
    var items = [Int]()
    func push(_ item: Int) {
        
    }
}

push方法中self是常量let修饰的,修改items相当于修改结构体本身

加上mutating关键字之后就可以编译通过了

struct JStruct {
    var items = [Int]()
    mutating func push(_ item: Int) {
        items.append(item)
    }
}

查看SIL,这时self成了var修饰的引用类型

再看一个例子,交换两个值

func swap(a:Int,b:Int) {
    var temp = a
    a = b
    b = temp
}

编译报错,参数ab都是let修饰的,不可变

使用inout修饰方法的参数之后

func swap(a: inout Int,b:inout Int) {
    var temp = a
    a = b
    b = temp
}

var a = 1
var b = 2

swap(&a, &b)

print("a=\(a),b=\(b)")

这时就可以通过正常交换a和b的值

结构体中的函数

struct JStruct {
    func test() {
        print("end")
    }
}

print(MemoryLayout<JStruct>.size)
print(MemoryLayout<JStruct>.stride)

JStruct().test()

方法并没有占用结构体的空间,那么方法存储在哪里了呢

我们打开always show disassembly,再JStruct().test()打断点

结构体中的函数都是静态调用的,程序编译完成之后函数地址就已经确定了,不需要去查找函数地址

静态调用的函数直接通过函数地址调用,符号表只是为了方便我们debug模式调试,再release模式会删除很多编译器可以确定的东西,留下动态绑定的一些符号

我们在realese模式下查看可执行文件多了一个DSYM格式的还原符号表,在调试线上Bug的时候可以帮助我们将命名重整之后的符号还原成我们能看懂的类名+函数名的格式

在release模式下符号表少了很多,而且编译器可以确定的很多符号删除掉了

除了可以使用MachOView查看符号表我们还可以使用终端命令nm 可执行文件地址来查看符号集

如果想根据地址查看对应符号可以使用命令nm 可执行文件地址 | grep 函数地址,注意这里的函数地址不带0x,获取符号之后使用命令xcrun swift-demangle 符号来获取函数信息

swift的命名重整规则很复杂,这也尽可能降低了符号冲突的可能,通过不同的函数来区分函数是被允许的

func test() {
    
}

func test(_:Int) {
    
}
c语言中的命名重整
#include <stdio.h>

void test(){
}

int main(int argc, const char * argv[]) {
    test();
    return 0;
}

所以通过不同的函数来区分同名函数在c语言是不被允许的

oc的命名重整

所以通过不同的函数来区分同名函数在c语言是不被允许的,看一个小例子,创建一个OC的类JClass

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface JClass : NSObject
-(void)test:(NSInteger)a;
@end
NS_ASSUME_NONNULL_END

#import "JClass.h"
@implementation JClass
-(void)test:(NSInteger)a{
    NSLog(@"JClass");
}
@end

创建JClass的分类JClass+JC

#import "JClass.h"

NS_ASSUME_NONNULL_BEGIN

@interface JClass (JC)
-(void)test:(NSString *)name;
@end
NS_ASSUME_NONNULL_END

#import "JClass+JC.h"
@implementation JClass (JC)
- (void)test:(NSString *)name{
    NSLog(@"JClass-JC");
}
@end

调用方法test

#import <Foundation/Foundation.h>
#import "JClass.h"
#import "JClass+JC.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        JClass *c = [[JClass alloc] init];
        [c test:18];
    }
    return 0;
}

我们知道在OC中category中定义的方法是插入到函数表的前边,类中定义的方法会移动到后边。

🤔思考一下执行[c test:18];之后是进入到类中定义的test方法还是进入到category定义的test方法,然后由于类型不匹配而报错呢???

报错了!!!验证了验证了

-(void)test:(NSInteger)a;

-(void)test:(NSString *)name;

在命名重整之后的符号是一样的

ASLR

我们创建一个正常的iOS工程,使用Swift语言

import UIKit

struct St {
    func test() {
        
    }
}

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        var t = St()
        t.test() //打断点
        // Do any additional setup after loading the view.
    }
}

看到函数地址为0x10143dc10,找到编译产物工程名.app显示包内容,找到可执行文件放到MachOView

这里的函数地址是0x0000000100003C10,和汇编看到的地址有偏差,首先找到基地址,可以看到基地址为0x0000000100000000

再找到程序装载基地址,执行指令image list,可以看到程序装载基地址为0x000000010143a000

那么本次运行随机编译地址等于程序装载基地址减去基地址 ASLR = 0x000000010143a000 - 0x0000000100000000 = 0x143A000

注意每次运行ASLR都会随机生成

那么用符号表里的地址加上ASLR等于汇编指令执行的函数地址 0x0000000100003C10 + 0x143A000 = 0x10143DC10

验证成功!!!

本篇文章介绍了很多很多的内容,写的略潦草,其中单单MachO文件就不是三两天就能理解的,在后续的使用中慢慢理解也就是了!!!