您的位置:首页 > 移动开发 > Swift

swift语言的学习笔记六(ARC-自动引用计数,内存管理)

2016-12-16 18:10 423 查看
Swift使用自动引用计数(ARC)来管理应用程序的内存使用。这表示内存管理已经是Swift的一部分,在大多数情况下,你并不需要考虑内存的管理。当实例并不再被需要时,ARC会自动释放这些实例所使用的内存。

另外需要注意的:

引用计数仅仅作用于类实例上。结构和枚举是值类型,而非引用类型,所以不能被引用存储和传递。

swift的ARC工作过程

每当创建一个类的实例,ARC分配一个内存块来存储这个实例的信息,包含了类型信息和实例的属性值信息。

另外当实例不再被使用时,ARC会释放实例所占用的内存,这些内存可以再次被使用。

但是,如果ARC释放了正在被使用的实例,就不能再访问实例属性,或者调用实例的方法了。直接访问这个实例可能造成应用程序的崩溃。就像空实例或游离实例一样。

为了保证需要实例时实例是存在的,ARC对每个类实例,都追踪有多少属性、常量、变量指向这些实例。当有活动引用指向它时,ARC是不会释放这个实例的。

为实现这点,当你将类实例赋值给属性、常量或变量时,指向实例的一个强引用(strong
reference)将会被构造出来。被称为强引用是因为它稳定地持有这个实例,当这个强引用存在时,实例就不能够被自动释放,因此可以安全地使用。

例子:

[cpp] view
plaincopy





class Teacher

{

var tName : String

init(name:String)

{

tName = name

println("老师 \(tName) 实例初始化完成.")

}

func getName() -> String

{

return tName

}

func classing()

{

println("老师 \(tName) 正在给学生讲课.")

}

deinit

{

println("老师 \(tName) 实例析构完成.")

}

}

测试ARC:

[cpp] view
plaincopy





func testArc()

{

var teacher:Teacher? = Teacher(name:"张三") //实例化一个Teacher对象将指向一个变量,此时产生了一个强引用(就好像OC中的引用计数+1)

var refteacher:Teacher? = teacher //再次产生强引用即(引用计数再+1)

var refteacher2:Teacher? = teacher<span style="white-space:pre"> </span> <span style="font-family: Arial, Helvetica, sans-serif;">//再次产生强引用即(引用计数再+1)</span>

refteacher = nil //第一个引用对象为nil并没有使实例释放,(引用计数-1)

teacher?.classing() //正常

teacher = nil //第二个引用对象为nil并没有使实例释放,(引用计数-1)

refteacher2!.classing() //正常

refteacher2 = nil //第三个引用对象为nil此时已没有作何引用了,因此ARC回收,实例释放.(引用计数-1)最后引用计数为0,则自动调用析构

refteacher2?.classing() //不再有输出

}

输出结果:

[cpp] view
plaincopy





老师 张三 实例初始化完成.

老师 张三 正在给学生讲课.

老师 张三 正在给学生讲课.

老师 张三 实例析构完成.

从上面的例子来看,确实swift给我们自动管理了内存,很多时侯开发者都不需要考虑太多的内存管理。但真的是这样吗?真的安全吗?作为开发者要如何用好ARC?

尽管ARC减少了很多内存管理工作,但ARC并不是绝对安全的。下面来看一下循环强引用导至的内存泄漏。

例子:

[cpp] view
plaincopy





class Teacher

{

var tName : String

var student : Student? //添加学生对象,初始时为nil

init(name:String)

{

tName = name

println("老师 \(tName) 实例初始化完成.")

}

func getName() -> String

{

return tName

}

func classing()

{

println("老师 \(tName) 正在给学生 \(student?.getName()) 讲课.")

}

deinit

{

println("老师 \(tName) 实例析构完成.")

}

}

class Student

{

var tName : String

var teacher : Teacher? //添加老师对象,初始时为nil

init(name:String)

{

tName = name

println("学生 \(tName) 实例初始化完成.")

}

func getName() -> String

{

return tName

}

func listening()

{

println("学生 \(tName) 正在听 \(teacher?.getName()) 老师讲的课")

}

deinit

{

println("学生 \(tName) 实例析构化完成.")

}

}

测试泄漏:

[cpp] view
plaincopy





func testMemoryLeak()

{

var teacher :Teacher?

var student :Student?

teacher = Teacher(name:"陈峰") //(引用计数为1)

student = Student(name:"徐鸽") //<span style="font-family: Arial, Helvetica, sans-serif;">(引用计数为1)</span>

teacher!.student = student //赋值后将产生"学生"对象的强引用 (引用计数+1)

student!.teacher = teacher //赋值后将产生"老师"对象的强引用 (引用计数+1)

teacher!.classing() //因为我清楚地知道teacher对象不可能为空,所以我用!解包

student!.listening()

//下面的代码,写与不写都不能使对象释放

teacher = nil //引用计数-1 但还不能=0,所以不会析构

student = nil<span style="white-space:pre"> </span> //引用计数-1 但还不能=0,所以也不会析构

println("释放后输出")

teacher?.classing()<span style="white-space:pre"> </span>//因为我不能确定teacher对象是否为空,所以必须用?来访问。

student?.listening()

}

输出结果:

[cpp] view
plaincopy





老师 陈峰 实例初始化完成.

学生 徐鸽 实例初始化完成.

老师 陈峰 正在给学生 徐鸽 讲课.

学生 徐鸽 正在听 陈峰 老师讲的课

释放后输出

自始至终都没有调用deinit。因此就会泄漏,此时已经不能采取任何措拖来释放这两个对象了,只有等APP的生命周期结束

实例之间的相互引用,在日常开发中是很常见的一种,哪么如何避免这种循环强引用导致的内存泄漏呢?

可以通过在类之间定义为弱引用(weak)或无宿主引用的(unowned)变量可以解决强引用循环这个问题

弱引用方式:

弱引用并不保持对所指对象的强烈持有,因此并不阻止ARC对引用实例的回收。这个特性保证了引用不成为强引用循环的一部分。指明引用为弱引用是在生命属性或变量时在其前面加上关键字weak。

注意

弱引用必须声明为变量,指明它们的值在运行期可以改变。弱引用不能被声明为常量。

因为弱引用可以不含有值,所以必须声明弱引用为可选类型。因为可选类型使得Swift中的不含有值成为可能。

因此只需要将上述的例子任意一个实例变量前加上weak关键词即可,如:

[cpp] view
plaincopy





weak var student : Student?

weak var teacher : Teacher?

下面来测试一下weak var student : Student?设为弱引用后,测试释放的时间点(情况一)

[cpp] view
plaincopy





var teacher :Teacher?

var student :Student?

teacher = Teacher(name:"陈峰")

student = Student(name:"徐鸽")

teacher!.student = student //赋值后将产生"学生"对象的强引用

student!.teacher = teacher //赋值后将产生"老师"对象的强引用

teacher!.classing()

student!.listening()

teacher = nil //此时将没有马上调用析构,要等student释放后才会释放

//student = nil

println("释放后输出")

teacher?.classing()<span style="white-space:pre"> </span>//前面已设为nil,所以没有输出

student?.listening()

经测试输出:

[cpp] view
plaincopy





老师 陈峰 实例初始化完成. //执行teacher = Teacher(name:"陈峰")

学生 徐鸽 实例初始化完成. //执行student = Student(name:"徐鸽")

老师 陈峰 正在给学生 徐鸽 讲课. //执行teacher!.classing()

学生 徐鸽 正在听 陈峰 老师讲的课 //执行student!.listening()

释放后输出 //执行println("释放后输出")

学生 徐鸽 正在听 陈峰 老师讲的课 //执行student?.listening()

学生 徐鸽 实例析构化完成. //学生对象先释放

老师 陈峰 实例析构完成. //此时由于学生对象释放了,此时没有了引用,也可以进行析构了

如果 weak var teacher : Teacher?

再来进行测试:(情况二)

[cpp] view
plaincopy





var teacher :Teacher?

var student :Student?

teacher = Teacher(name:"陈峰")

student = Student(name:"徐鸽")

teacher!.student = student //赋值后将产生"学生"对象的强引用

student!.teacher = teacher //赋值后将产生"老师"对象的强引用

teacher!.classing()

student!.listening()

teacher = nil //此时将没有马上调用析构,要等student释放后才会释放

//student = nil

println("释放后输出")

teacher?.classing()

student?.listening() //此时并不因为

输出结果:

[cpp] view
plaincopy





老师 陈峰 实例初始化完成.

学生 徐鸽 实例初始化完成.

老师 陈峰 正在给学生 徐鸽 讲课.

学生 徐鸽 正在听 陈峰 老师讲的课

老师 陈峰 实例析构完成.

释放后输出

学生 徐鸽 正在听 nil 老师讲的课

学生 徐鸽 实例析构化完成.

经测试得出结论:

当A类中包函有B类的弱引用的实例,同时,B类中存在A的强引用实例时,如果A释放,也不会影响B的析放,但A的内存回收要等B的实例释放后才可以回收。(情况一的结果)

当A类中包函有B类的强引用的实例时,如果A释放,则不会影响B的析放。(情况二的结果)

无宿主引用方式:

和弱引用一样,无宿主引用也并不持有实例的强引用。但和弱引用不同的是,无宿主引用通常都有一个值。因此,无宿主引用并不定义成可选类型。指明为无宿主引用是在属性或变量声明的时候在之前加上关键字unowned。

因为无宿主引用为非可选类型,所以每当使用无宿主引用时不必使用?。无宿主引用通常可以直接访问。但是当无宿主引用所指实例被释放时,ARC并不能将引用值设置为nil,因为非可选类型不能设置为nil。

注意

在无宿主引用指向实例被释放后,如果你想访问这个无宿主引用,将会触发一个运行期错误(仅当能够确认一个引用一直指向一个实例时才使用无宿主引用)。在Swift中这种情况也会造成应用程序的崩溃,会有一些不可预知的行为发生。因此使用时需要特别小心。

将前面例子改为无宿主引用:

[cpp] view
plaincopy





class Teacher

{

var tName : String

var student : Student? //学生对象的强引用,实例可以为nil

init(name:String)

{

tName = name

println("老师 \(tName) 实例初始化完成.")

}

func getName() -> String

{

return tName

}

func classing()

{

println("老师 \(tName) 正在给学生 \(student?.getName()) 讲课.")

}

deinit

{

println("老师 \(tName) 实例析构完成.")

}

}

class Student

{

var tName : String

unowned var teacher : Teacher //无宿主引用,不可以设置为nil

init(name:String,tcher :Teacher)

{

tName = name

teacher = tcher //因为无宿主引用不能设为可选型,所在必须要初始化

println("学生 \(tName) 实例初始化完成.")

}

func getName() -> String

{

return tName

}

func listening()

{

println("学生 \(tName) 正在听 \(teacher.getName()) 老师讲的课")

}

deinit

{

println("学生 \(tName) 实例析构化完成.")

}

}

测试无宿主引用:

[cpp] view
plaincopy





func testNotOwner()

{

var teacher :Teacher? //声明可选型变量

teacher = Teacher(name:"陈峰")

var student = Student(name: "徐鸽",tcher: teacher!)

//进行相互引用

teacher!.student = student

student.teacher = teacher!

teacher!.classing()

student.listening()

teacher = nil

println("老师对象释放后")

teacher?.classing()

student.listening() //error 因为在前面的teacher设为nil时,隐式的将student对象给释放了,因此这里再访问就会crash

}

输出结果:

[cpp] view
plaincopy





老师 陈峰 实例初始化完成.

学生 徐鸽 实例初始化完成.

老师 陈峰 正在给学生 徐鸽 讲课.

学生 徐鸽 正在听 陈峰 老师讲的课

老师 陈峰 实例析构完成.

老师对象释放后

Program ended with exit code: 9(lldb) //会crash,thead1:Exc_BREAKPOINT(code=EXC_i386_BPT,subcode=0x0)

所以使用无宿主引用时,就需要特别小心,小心别人释放时,顺带释放了强引用对象,所以要想别人释放时不影响到原实例,可以使用弱引用这样就算nil,也不会影响。

上面介绍了,当某个类中的实例对象如果在整个生命周期中,有某个时间可能会被设为nil的实例,使用弱引用,如果整个生命周期中某一实例,一旦构造,过程中不可能再设为nil的实例变量,通常使用无宿主引用。但时有些时侯,在两个类中的相互引用属性都一直有值,并且都不可以被设置为nil。这种情况下,通常设置一个类的实例为无宿主属性,而另一个类中的实例变量设为的隐式装箱可选属性(即!号属性)

如下面的例子,每位父亲都有孩子(没孩子能叫父亲么?),每个孩子都有一个亲生父亲

[cpp] view
plaincopy





class Father

{

let children : Children! //声明为隐式可选类型

let fathername : String

init(name:String,childName:String)

{

self.fathername = name

self.children = Children(name: childName,fat:self) //初始化时产生相互引用

}

deinit

{

println("father deinited.")

}

}

class Children

{

unowned let father : Father //声明为无宿主类型

let name : String

init(name:String ,fat : Father)

{

self.name = name

self.father = fat

}

deinit

{

println("children deinited.")

}

}

测试代码:

[cpp] view
plaincopy





var fa = Father(name: "王五",childName: "王八")

println("\(fa.fathername) 有个小孩叫 \(fa.children.name)")

输出结果:

[cpp] view
plaincopy





王五 有个小孩叫 王八

father deinited.

children deinited.

同样可以看到,尽管是循环引用,但还是能正常回收。

另外,还有一种情况,当自身的闭包对自身(self) 的强引用,也会导致内存泄漏。

例子:

[cpp] view
plaincopy





class CpuFactory

{

let cpuName : String

let cpuRate : Double

init(cpuName:String,rate:Double)

{

self.cpuName = cpuName

self.cpuRate = rate

}

//声明一个闭包

@lazy var someClosure: (Int, String) -> String = {

//下面这句不可以注释编译器会报Tuple types '(Int,String)'and'()'hava a different number of elements (2 vs. 0)

[unowned self] (index: Int, stringToProcess: String) -> String in

// closure body goes here

return "A \(self.cpuName)" //闭包中引用self

}

//声明一个闭包,同样闭包中引用self

@lazy var machining: () -> String = {

[unowned self] in //这句可以注释(按照书上说,使用这句可以解释闭包的强引用,但个人实践,不管加不加这句,都不会释放,即这样写有内存泄漏)

// closure body goes here

if self.cpuRate > 10

{

return "\(self.cpuName) i7 2.5G"

}

else

{

return "\(self.cpuName) i3 2.0G"

}

}

//声明一个闭包,但闭包中将自身作为参数传进去(可以避去内存泄漏)

@lazy var machining2 : (CpuFactory) -> String = {

[unowned self] (cpu:CpuFactory) -> String in

if cpu.cpuRate > 10

{

return "\(cpu.cpuName) i7 2.5G"

}

else

{

return "\(cpu.cpuName) i3 2.0G"

}

}

deinit

{

println("Cpu Factroy is deinited.")

}

}

在这个例子中有三个闭包,分别是带参,和不带参,对于带参的 不能省略[unowned self] (paramers) in操作。否则会编译不过,另外,书中没有提到的,只有声明为@lazy的闭包中才可以使用[unowned self] 否则在普通闭包中使用也会报错。还有一点书中讲到当自身闭包中使用self.时会产生强引用,导至内存泄漏,因此加上[unowned
self ] in 这句可以破坏这种强引用,从而使内存得到释放,但经本人亲自验证,就算加上了也没有释放。

测试:

[cpp] view
plaincopy





func testClosure()

{

var cpu : CpuFactory? = CpuFactory(cpuName: "Core",rate: 5)

// println(cpu!.machining())

println(cpu!.machining2(cpu!))

// println(cpu!.someClosure(3,"hello"))

cpu = nil

}

分别单独验证各句输出结果:

[cpp] view
plaincopy





func testClosure()

{

var cpu : CpuFactory? = CpuFactory(cpuName: "Core",rate: 5)

println(cpu!.machining())

cpu = nil

}

输出:

[cpp] view
plaincopy





Core i3 2.0G

显然cpu = nil也不会释放内存。

再来看第二个。

[cpp] view
plaincopy





func testClosure()

{

var cpu : CpuFactory? = CpuFactory(cpuName: "Core",rate: 5)

println(cpu!.machining2(cpu!))

cpu = nil

}

输出

[cpp] view
plaincopy





Core i3 2.0G

Cpu Factroy is deinited.

可见使用自身作为参数传参时,可以释放内存。

同样再测试第三种:

[cpp] view
plaincopy





func testClosure()

{

var cpu : CpuFactory? = CpuFactory(cpuName: "Core",rate: 5)

println(cpu!.someClosure(3,"hello"))

cpu = nil

}

输出

[cpp] view
plaincopy





A Core

其实第三和第一种是一样的,都是引用了self.但第一种可以把[unowned self ]in 句注释和不注释的情况下进行测试,可以发现结果是一样的,并没有释放内存。

实在令人有点费解。。。。。。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐