Swift 循环引用

2021-02-07  本文已影响0人  just东东

Swift 循环引用

[TOC]

前言

本本主要研究的是Swift中闭包捕获外部变量时产生的循环引用。全部示例是在macOS 命令行工程中。

1. 循环引用

1.1 闭包捕获外部变量

举个简单的例子

var age = 10

let clourse = {
    age += 1
}

print(age)
clourse()
print(age)

打印结果:

image

此处即使闭包捕获外部变量的最简单的例子,闭包内部对变量的修改将会改变变量的原始的值,这个与OCblock是一致的,但是OCblock中修改外部变量的值需要使用__block修饰,Swift则不需要。

1.2 循环引用

1.2.1 deinit

定义一个类,代码如下:

class Person {
    var age = 18 
    //反初始化器
    deinit {
        print("Person deinit")
    }
}

类中的deinit方法是Swift中的反初始化器,当前实例对象即将回收的时候会调用该方法,所以说我们可以通过该方法是否被调用来判断当前实例对象是否被正常释放。如果没被释放就可能存在循环引用的情况。

声明一个方法,并调用它。

func test() {
    var p = Person()
}

test()

运行结果:

image

我们可以看到在以上的代码中,正常打印了Person deinit也就是说执行了deinit方法,所以说此时p这个实例对象正常释放了,不存在循环引用。

1.2.2 在闭包中修改属性值

还是上面的类,此时我们在闭包中修改实例对象的属性值,代码如下:

let clourse = {
    p.age += 1
}
    
clourse()

运行,打印结果:

image

此时依旧打印了Person deinit,说明并没有造成循环引用。

1.2.3 循环引用

在上面的两个例子中都没有造成循环引用,下面在看个例子,我们在类中增加一个无参无返回值的闭包属性。并且在这个闭包修修改age属性的值。代码如下:

class Person {
    var age = 18
    var myClourse: (()->())?
    //反初始化器
    deinit {
        print("Person deinit")
    }
}

func test() {
    var p = Person()
    p.myClourse = {
        p.age += 1
    }
}

test()

运行,打印结果:

image

此时我们并没有看到Person deinit的打印,说明此时就造成了循环引用,此时我们的myClourse持有着p对象,而myClourse作为p对象的属性,也被p对象持有着,p对象需要等待闭包释放后才能释放,而闭包也在等待内部的p对象释放,所以就造成了循环引用。

此处引入循环引用的过程虽长,但也一步一步的解释了出现循环引用的条件,在开发过程中我们最常见的循环引用就是self强持有闭包,而闭包中又要持有self,以此来实现对self中某些属性的修改,或者方法的调用。

2. 解决循环引用的方法

循环引用会使实例对象不能够正常释放,此时就会出现一些不可预估的问题,也会造成内存的浪费,所以我们需要解决循环引用,在Swift中有两种解决循环引用的方法。分别是weakunowned。关于这两个方法的底层实现及相关知识点可以参考我的另一篇文章Swift 内存管理,本篇注重应用,对原理的介绍偏弱。

2.1 weak

func test() {
    var p = Person()
    p.myClourse = { [weak p] in
        p?.age += 1
    }
}

weak也是我们OC中的一种解决循环引用的方式,在Swift中使用weak修饰的实例变量默认为可选类型,所以在使用weakp对象会成为可选类型,在使用的时候需要使用?或者!

2.2 unowned

func test() {
    var p = Person()
    p.myClourse = { [unowned p] in
        p.age += 1
    }
}

unowned也是Swift中独有的一种解决循环引用的方式,相较于weak,使用unowned修饰后的对象不是可选类型,也不可置为nilunowned的意思是假定当前对象有值,所以就可能存在野指针的情况,所以在使用的过程中要注意使用的位置。而weak修饰的对象一旦为nil的时候,由于swift特性,可选值为nil就不会执行后续的代码,相较于unowned比较安全。

3. 捕获列表

在上文中我们提到的[weak p]和[unowned p]在swift中被称为捕获列表

捕获列表:

下面我们来看个例子,以下代码的执行结果是什么?

func test() {
    var age = 0
    var height = 0.0

    let clourse = { [age] in
        print(age)
        print(height)
    }
    
    age = 10
    height = 1.85
    clourse()
}

test()

打印结果:

image

根据以上的打印结果我们可以知道,对于闭包中捕获列表中的常量,闭包会捕获外部相同名称的变量或常量来初始化捕获列表中定义的常量,其有如下特点:

  1. 捕获列表中的是常量,不可修改
  2. 该常量是值拷贝,相当于复制了一份外部变量或常量
  3. 既然是常量就是不可修改的

如果我们修改会报编译错误:

image

关于此处的原理将会在闭包相关文章中进行分析,也可以先看看OCBlock底层原理

上一篇 下一篇

猜你喜欢

热点阅读