当前位置: 首页 > 科技观察

“弱,强,无人知晓,上帝!”-SWIFT中引用关系的解释

时间:2023-03-12 00:46:08 科技观察

我发现自己在写代码的时候经常会担心强引用循环(retaincycles)的出现。我认为这与任何其他问题一样普遍。我不了解你,但我总是听到“我什么时候使用关键字weak?‘unowned’到底是什么东西?”种声音。我们发现的问题是,我们知道在swift代码中使用strong、weak和unowned说明符来避免强引用循环,但我们不太清楚该使用哪一个。幸好我知道它们是什么以及何时使用它们!希望本文已教会您何时何地使用这3个说明符。让我们开始吧。ARCARC是Apple版自动内存管理的编译时功能。全称是AutomaticReferenceCounting。意思是对于一个对象来说,只有在没有强引用的情况下,该对象占用的内存才会被回收。STRONG-强引用从什么是强引用开始。它本质上是一个普通的引用(指针或者同义的东西),但它的特殊之处在于它可以通过将被引用对象(object)的保留计数(retaincount)增加1来保护对象不被ARC回收.从本质上讲,即使对任何东西的强引用指向了这个对象,这个对象也不会被回收。请记住这一点,稍后我们讨论强引用循环和相关内容时会用到它。强引用在Swift中几乎无处不在。其实在声明一个属性(property)的时候,默认是强引用。当关系层次结构是线性的时,通常使用强引用不是问题。当它从父层次结构流向子层次结构时,使用这个强引用总是没问题的。这是一个强引用的例子。classKraken{lettentacle=Tentacle()//子关卡的强引用。}classTentacle{letsucker=Sucker()//子关卡的强引用。}classSucker{}*/Kraken是海妖的意思,Tentacle是触手的意思,sucker是吸盘的意思……译者注/*例子是线性关系层次结构。Kraken对Tentacle实例有一个强引用,而Tentacle实例又对Sucker实例有一个强引用。引用关系流从父级别(Kraken)一直向下流到子级别(Sucker)。动画块中的引用层次类似:UIView.animateWithDuration(0.3){self.view.alpha=0.0}因为animateWithDuration是UIView的静态方法,所以这里的闭包是父层次,self是子层次。如果子层次结构想要引用父层次结构怎么办?这是我们将使用弱引用和无主引用的地方。WEAKANDUNOWNEDREFERENCES-弱引用和UNOWNED引用WEAK-弱引用弱引用是一个指针,它不能保护它所引用的对象不被ARC回收。强引用可以将其对象的保留计数增加1,而弱引用则不能。在swift中,所有弱引用都是非常量Optionals(想想var和let之间的关系),因为这个引用可以并且将会被更改为nil。比如下面的代码是无法编译的:classKraken{weaklettentacle=Tentacle()//let是一个常量。所有弱变量都必须是可变的。}因为tentacle是一个let常量。由于规范限制,无法在运行时更改Let。因为弱引用变量(weakvariables)在没有强引用时会变为nil,所以swift编译器要求你声明弱引用变量为var。那些有潜在强引用循环的地方是使用弱引用变量的关键。当两个对象通过强引用相互指向对方时,就会发生强引用循环,并且ARC不会为任何一个实例发出正确的释放消息代码,因为这两个实例正在互相保护对方。这是一张来自Apple的简洁图片,非常清楚地展示了这一点:这是一个使用NSNotificationAPI(仍然相对较新)的强引用循环的一个很好的例子。看一下下面的代码:){notificationinself.eatHuman()}}deinit{ifnotificationObserver!=nil{NSNotificationCenter.defaultCenter.removeObserver(notificationObserver)}}}这里我们创建了一个强引用循环。你看,Swift中的闭包与Objective-C中的块非常相似。如果在闭包外声明了一个变量,在闭包内引用该变量会创建另一个强引用。这种情况的唯一例外是使用值类型的变量,例如swift中的Ints、Strings、Arrays和Dictionaries。这里NSNotificationCenter保留了一个闭包,当您调用eatHuman()方法时,该闭包将self捕获为强引用。问题是:我们直到deinit才清除闭包,但ARC永远不会调用deinit,因为闭包对Kraken实例有强引用!这也会发生在使用NSTimers和NSThread的地方。解决方案是在闭包的捕获列表中使用对self的弱引用。这打破了强引用循环。至此,我们的对象引用图就变成了这样:把self变成weak,不会让self的retaincount加1,这样可以让ARC在合适的时候合理销毁它。要在闭包中使用弱变量和无主变量,您需要在闭包主体中使用[]语法。例如:letclosure={[weakself]inself?.doSomething()//记住,所有弱变量都是可选的。}为什么方括号中是weakself?这看起来很奇怪!在Swift中,当我们看到方括号时,我们会想到数组。你猜怎么了?可以在闭包中指定要捕获的多个值!例如:letclosure={[weakself,unownedkrakenInstance]in//看这个捕获多个值的数组self?.doSomething()//weak变量是一个可选类型krakenInstance.eatMoreHumans()//unowned变量不是一个optionaltype}看起来数组太多了吧?现在你知道为什么捕获值要写在方括号里了吧。那么,利用我们现在所学的,在上面的通知代码的闭包捕获列表中加入[weakself]就可以解决强引用循环的问题:NSNotificationCenter.defaultCenter().addObserverForName("humanEnteredKrakensLair",object:nil,queue:NSOperationQueue.mainQueue()){[weakself]notificationin//使用捕获列表消除强引用循环!self?.eatHuman()//self现在是一个可选类型!}#p#还有一个用到weak和unowned变量的地方就是使用协议(protocol)来实现多个类之间的委托(delegation),因为swift中的类是引用类型。结构体(structs)和枚举(enumerations)也可以符合协议,但它们是值类型。如果父类带子类使用委托,像这样:vardelegate:LossOfLimbDelegate?funccutOffTentacle(){delegate?.limbHasBeenLost()}}然后我们需要使用弱变量。在此示例中,Tentacle以其拥有的委托属性的形式持有对Kraken的强引用,而Kraken在其触手属性中也具有对Tentacle的强引用。我们在proxy语句前加一个weakspecifier来解决:weakvardelegate:LossOfLimbDelegate?你说什么?编译失败?好吧,因为不能将非类类型协议标记为弱。此时,我们不得不使用一个class-only协议(classprotocol)来使代理属性被标记为weak。让我们的协议继承自:class。protocolLossOfLimbDelegate:class{//协议现在继承classfunclimbHasBeenLost()}什么时候不用:class?苹果的文档说:当一个协议要求(behavior)定义的行为可以保证或者要求符合这个协议的类型是一个引用类型和非值类型,使用class-only协议。基本上,如果你自己的代码引用级别和我上面写的一样,你就加上:class。对于使用结构体或者枚举的情况,不需要:class,因为结构体和枚举都是值类型,class是引用类型。UNOWNEDweakreferences本质上与unownedreferences相同。Unowned引用不会增加它引用的对象的保留计数。但是,Swift中无主引用的另一个优点是它们是非可选的。这使得使用起来更加方便,而无需引入可选绑定。这与隐式可选类型(ImplicityUnwarppedOptionals)没有区别。这里有点乱。弱引用和无主引用都不会增加保留计数。它们都是用来解决强引用循环的问题。那么我们什么时候使用它们呢?Apple的文档说:当一个引用在其生命周期内变为nil时,将这个引用定义为弱引用仍然是合理的。相反,如果你事先知道一个引用在设置后不会变成nil,就定义它为无主引用。答案你懂的:就像隐式可选类型一样,如果在使用的时候能保证引用肯定不是nil,就用unowned,如果不能保证,就得用弱引用。下面是一个典型的例子。类的闭包中捕获的self不会变成nil,从而产生强引用循环:classRetainCycle{varclosure:(()->Void)!varstring="Hello"init(){closure={self.string="Hello,World!"}}}//初始化类并激活强引用循环。letretainCycleInstance=RetainCycle()retainCycleInstance.closure()//此时我们可以保证闭包中捕获的self不再为nil。之后的任何代码(尤其是更改对self的引用的代码)都需要检查unowned是否仍然在这里工作。上面的例子中,闭包以强引用的形式捕获了self,self也通过自己的闭包属性保留了对闭包的强引用,形成了强引用循环。只需在闭包中添加一个[unownedself]就可以打破循环:closure={[unownedself]inself.string="Hello,World!"}由于我们在初始化RetainCycle类后立即调用闭包,我们可以Thinkselfwon'不再是零了。结论强引用循环是不好的。但是仔细写代码,考虑清楚你的引用级别,合理选择弱引用和无主引用,避免内存泄漏和内存遗弃。希望本文对您有所帮助。祝码友编程愉快!