更多课程 选择中心

软件测试培训
达内IT学院

400-111-8989

如何通过单元测试来发现并避免 Swift 中的内存泄露?


内存管理和避免内存泄露对于任何程序而言都是至关重要的一部分。好在 Swift 在这部分做的在大多数情况下都相对的比较简单,多亏了自动引用计数(ARC)。然而,还是有几种情况会非常容易导致内存泄露的发生的。

在 Swift 中内存泄露通常就是循环引用 (retain cycle,居然还有翻译成「保留环」的……)的结果,也就是两个(或者多个)对象互相保持了对方的一个强引用。这种情况通常很难被定位到,造成的崩溃也不容易重现。

我们来看看如何通过单元测试既可以发现内存泄露,又能够轻而易举地在将来避免导致泄露错误的发生。

代理(Delegates)

代理模式在苹果的开发平台中非常常见,也是一个非常漂亮的模式——简单而且可以让两个对象拥有低耦合关系的同时还可以互相通信。

假定 app 中有一个视图控制器来显示用户所有朋友的列表,为了让视图控制器能够将事件发回给它的拥有者(owner),我们需要添加一个类似于这样的代理 API:

class

FriendListViewController

:

UIViewController

{

var

delegate

:

FriendListViewControllerDelegate

}

乍一看上去,代码好像没什么问题啊,但是只要你再仔细看一下的话你就会发现问题在哪里了,没错,代理是个强引用。这很有可能会导致循环引用,因为 FriendListViewController 实例的拥有者也有可能就是代理,这就造成了两个对象互相强引用。

为了保证不会再犯这种错误,我们可以搭建一个单元测试来确保 FriendListViewController 对其代理不是强引用:

class

FriendListViewControllerTests

:

XCTestCase

{

func testDelegateNotRetained

()

{

let

controller

=

FriendListViewController

()

// 创建一个代理的强引用,将其赋给视图控制器

var

delegate

=

FriendListViewControllerDelegateMock

()

controller

.

delegate

=

delegate

// 将这个强引用重新分配给一个新的对象,这样就应该会释放原对象,

// 因此视图控制器的代理也应该为 nil

delegate

=

FriendListViewControllerDelegateMock

()

XCTAssertNil

(

controller

.

delegate

)

}

}

测试第一次运行会失败,但并不是坏事,因为这个测试之后会保证我们确实修复了问题。针对的修复也很简单,只需要把代理声明为 weak就可以了:

class

FriendListViewController

:

UIViewController

{

weak

var

delegate

:

FriendListViewControllerDelegate

?

}

这时我们再次运行测试的话,测试就会通过了。我们不仅修复了 app 中的一处内存泄露,同时还保证了这个 bug 在将来也不会再发生了(这对于重构的工作来说超级有用)。

还有一个方法就是使用 SwiftLint,这个工具会在这种情况下警告你没有把代理的属性设为 weak 。(其实不止也一点用途了,lint 类工具能做的事情非常多)

观察者(Observers)

观察者模式也是一种常见的模式,通常用于让一个对象给其他对象发送各种通知事件。和代理一样,我们也不想拥有观察者的强引用,因为这些观察者对象通常会保持所观察对象的强引用。

假定有一个 UserManager,在数组里存着所有观察者的引用:

class

UserManager

{

private

var

observers

=

[

UserManagerObserver

]()

func addObserver

(

_ observer

:

UserManagerObserver

)

{

observers

.

append

(

observer

)

}

}

就像实现代理模式那样,代码特别容易造成观察者对象的强引用,也就有可能造成内存泄露。

好在这个问题在测试用例中可以简单地被重现:

class

UserManagerTests

:

XCTestCase

{

func testObserversNotRetained

()

{

let

manager

=

UserManager

()

// 分别创建观察者的一个强引用和弱引用

// 将强引用添加到 UserManager 中

var

observer

=

UserManagerObserverMock

()

weak

var

weakObserver

=

observer

manager

.

addObserver

(

observer

)

// 如果给强引用重新分配一个新的对象,期望的结果是弱引用变为 nil

// 因为观察者数组不应该保存观察者的强引用

observer

=

UserManagerObserverMock

()

XCTAssertNil

(

weakObserver

)

}

}

同样,这个测试第一次会失败,这也是件好事,因为我们可以重现这个问题。为了修复这个问题,我们需要一个小的 wrapper 来保存观察者的弱引用(因为数组总是保存元素的强引用):

private

extension

UserManager

{

struct

ObserverWrapper

{

weak

var

observer

:

UserManagerObserver

?

}

}

接着,对 UserManager 稍作修改,将观察者的数组变成 wrapper 的数组,这就可以通过测试了:

class

UserManager

{

private

var

observers

=

[

ObserverWrapper

]()

func addObserver

(

_ observer

:

UserManagerObserver

)

{

let

wrapper

=

ObserverWrapper

(

observer

:

observer

)

observers

.

append

(

wrapper

)

}

}

这种写法在许多需要在集合中存储弱引用对象的情况下特别有用。需要特别留心的是清理那些观察者被释放的 wrapper。一个比较好的做法是遍历后过滤出那些不再需要的 wrapper,比如在发送通知事件的时候:

private

func notifyObserversOfUserChange

()

{

observers

=

observers

.

filter

{

wrapper

in

guard

let

observer

=

wrapper

.

observer

else

{

return

false

}

observer

.

userManager

(

self

,

userDidChange

:

user

)

return

true

}

}

闭包(Closures)

最后,我们看一下如何在基于闭包的 API 中发现并阻止内存泄露。闭包是许多与内存相关的 bug 和泄露的常见源头,因为闭包对内部的所有对象都保持着强引用。

假定有一个 ImageLoader ,功能是通过网络加载远程的图片,然后在加载完毕后执行一个 completion handler:

class

ImageLoader

{

func loadImage

(

from

url

:

URL

,

completionHandler

:

@escaping

(

UIImage

)

->

Void

)

{

...

}

}

代码中的一个常见的错误就是在操作结束后仍然保持 completion handler 的引用。可能为了能够取消或者批处理的操作需要,我们希望在某种集合中存储这些 completion handler,然后就忘了移除,最终导致内存泄露。

那么应该如何通过单元测试来确保 completion handler 能够在执行完被立即移除呢?上面两种情况我们都是通过使用弱引用来达到目的的,但是弱引用不能用在闭包上啊??(多说一句,函数也不可以,Swift 中只有类的实例可以使用弱引用)。

我们采用的做法是通过对象捕获(capturing)来和一个带有闭包的对象关联,然后用这个对象来验证闭包是否被释放:

class

ImageLoaderTests

:

XCTestCase

{

func testCompletionHandlersRemoved

()

{

// 用一个模拟的 network manager 来搭建一个 image loader

let

networkManager

=

NetworkManagerMock

()

let

loader

=

ImageLoader

(

networkManager

:

networkManager

)

// 根据给定的 URL 来模拟一个响应

let

url

=

URL

(

fileURLWithPath

:

"image"

)

let

data

=

UIImagePNGRepresentation

(

UIImage

())

let

response

=

networkManager

.

mockResponse

(

for

:

url

,

with

:

data

)

// 创建一个对象(任何类型都可以),然后保持对其的强引用和弱引用

var

object

=

NSObject

()

weak

var

weakObject

=

object

loader

.

loadImage

(

from

:

url

)

{

[

object

]

image

in

// 在闭包中捕获这个对象

_

=

object

}

// 发送响应,这就会让上面的闭包执行,然后移除并释放

response

.

send

()

// 然后给对象的强引用重新分配一个新的对象,这样弱引用应该变为 nil,

// 因为这时候闭包应该已经执行完并移除了

object

=

NSObject

()

XCTAssertNil

(

weakObject

)

}

}

这下我们就可以保证 image loader 不会再保持久的闭包了,而且我们有干掉了另一个内存泄露的隐患。

结论

这么使用单元测试可能一开始看起来有些大材小用,但是这确实是一个利器,特别是在你想要重现内存泄露或者在将来可能会出现的问题上多加一层保护的时候。

尽管上面的测试不一定会完全避免所有的内存泄露,但是却能够在无形中节省大量查找问题根源的时间。

预约申请免费试听课

填写下面表单即可预约申请免费试听! 怕学不会?助教全程陪读,随时解惑!担心就业?一地学习,可全国推荐就业!

上一篇:3年测试从业人员Jmeter压测最近心得分享
下一篇:交易系统升级之性能测试思路

软件测试工程师有哪些岗位?

软件测试工程师要求?

软件测试项目去哪里找?

软件测试这个岗位今年如何?

  • 扫码领取资料

    回复关键字:视频资料

    免费领取 达内课程视频学习资料

Copyright © 2023 Tedu.cn All Rights Reserved 京ICP备08000853号-56 京公网安备 11010802029508号 达内时代科技集团有限公司 版权所有

选择城市和中心
黑龙江省

吉林省

河北省

陕西省

湖南省

贵州省

云南省

广西省

海南省