该教程将向你展现怎么样去使用Xcode提供的工具"Instrument"中最重要的一些功能。帮助你检查本身代码中的性能问题、内存管理问题、循环引用问题以及其余种种。php
在本篇教程中,你将学到:html
怎样使用Time Profiler工具来定位你的代码中的"高消耗点(hot-spot)",从而让你的代码更加有效率。swift
怎样使用Allocations工具来检测和改正代码中的内存管理问题,例如循环强引用。api
注意:本教程假定你已经上手了iOS开发和swift语言。若是你是iOS开发的初学者,你可能更适合去看一下本网站上的其余教程。本篇教程还使用了storyboard,因此确保你熟悉相关概念。本网站上的这篇教程是一个很好的起点。
(编辑注:若是你想全面了解Instruments,请参看:Instruments 用户指南【中文完整翻译版】)缓存
一切就绪?准备好进入instrument的迷人的世界中吧。安全
起步网络
在本篇教程中,你无需从头开始建立一个完整的应用,咱们已经为你提供了一个示例程序,你的任务是浏览这个应用,而后使用instrument做为你的助手来改善这个应用--相似于你优化本身的应用的过程。数据结构
从这里下载starter project,解压后使用Xcode打开。闭包
该示例程序使用Flickr提供的API来搜索图片。你须要一个API key来使用这个API。对样例程序而言,你能够去Flickr的网站上建立一个样例key,而后就能够经过网站http://www.flickr.com/services/api/explore/?method=flickr.photos.search 来搜索图片,并使用时把API key拷贝到上述url的最后面,格式为"&api_key=",接下来的参数一样加到&后面。app
例如,若是URL是http://api.flickr.com/services/rest/?method=flickr.photos.search&api_key=6593783efea8e7f6dfc6b70bc03d2afb&format=rest&api_sig=f24f4e98063a9b8ecc8b522b238d5e2f ,那么API key就是6593783efea8e7f6dfc6b70bc03d2afb。
把这个key粘贴到FlickrSearcher.swift文件顶部,取代原有的key。
须要注意的是,该key每隔一天左右都会改变,因此你可能碰巧须要去从新生成一个key。若是key不可用了,你的应用将会提醒你。
编译并运行应用,执行一次查询,而后点击一个结果,你将会看到相似下面的界面。
浏览一下这个应用,弄清楚基本的功能,你可能会想,一旦UI看起来不错后,这个应用就准备好上传了。然而,接下来你将看到使用Instruments工具后将为你的app带来多少好处。
本教程剩下的内容将会向你展现怎么样找到并改正存在于你的应用中的问题。你将看到Instruments工具怎么样使debug程序的工做变得易如反掌。
时间分析仪
首先你将使用的工具是Time Profiler。在每一个测量时间间隔内,该工具将暂停程序执行,在每一个线程上进行一次栈追踪(stack trace),能够想象成点了Xcode调试工具中的暂停键。
这里有一张Time Profiler的预览图。
这个界面展现的是调用树(call tree)。调用树展现的是一个app中执行不一样的方法花费的时间,每一行都是程序执行路径中的一个不一样的方法,每一个方法花费的时间能够由分析工具在其中暂停的次数来决定。
例如,若是有100件事情要作,每件花费1毫秒,在栈顶的方法作了其中10件,那么你能够推断出,大约在总执行时间中的10%--10毫秒--花费在了这个方法中。这是至关粗糙的估计,但确实有效!
注意:一般来讲,你应该老是在真机上分析你的app,而不是在模拟器上。iOS模拟器有你的Mac提供的性能支撑,可是真机做为硬件移动设备,资源是有限的。因此你的app可能在模拟器上运行得很好,可是一旦它运行到真机上,你可能就会发现有性能问题。
那么马上开始分析吧。
从Xcode的菜单栏中,选择product/profile,或者按下commond+I,这时会编译程序,加载Instruments工具,而后会出现一个选择框,相似于下面的图片:
Instruments提供了不一样的模板。
选择Time Profiler工具,而后点击Choose,这时会出现一个新的工具文件。点击左上角的红色记录按钮,开始记录并加载你的app,你可能须要输入密码来为Instruments分析其余进程受权--不用担忧,这很安全。
在Instruments窗口中,能够看到一个计时器,还有一个小箭头在屏幕中央的图表上从左向右移动。这代表app正在运行。
如今开始使用这款app,搜索图片,而后点击几个查询结果进入详情界面,你可能会发现进入一个详情界面很是慢,另外滑动查询结果的列表也是慢得难以置信--这是一款笨重的app。
然而,你是幸运的,由于接下来你就会修正这一问题。不过在这以前你要先快速浏览一下当前展现的这个Instruments的界面。
首先,确保右手边工具栏上的视图选择器的每个选项都被选中,以下:
这样就确保全部的面板都被打开。如今看一下下面的截图和每一部分的说明。
一、这里控制记录过程,点击红色的"记录"按钮能够中止或开始当前正在分析的app(在记录和中止按钮之间切换),暂停键,如你所想,暂停当前正在运行的app。
二、这里是执行计时器(run timer),计时器记录着正在分析的app执行了多长时间、执行了多少次。若是你使用记录控制按钮来中止你的app,而后重启,这将建立一个新的运行记录,同时会显示"Run 2 of 2"。
三、这里被称做路径(track),就你选择的Time Profiler工具而言,由于只有一个工具,因此这里只有一条路径,关于这里显示的图标的详情,一会你就会在接下来的教程中了解更多。
四、这里是详情面板,展现的是你正在使用的工具的主要信息。就如今而言,这里展现的是最"笨重(hottest)"的方法--换句话说,占用CPU时间最长的方法。点击上方的bar会看到Call Tree(左手边的那个)并选中Sample List,而后你会看到数据的不一样视图。视图展现了每个示例。点击其中几个,你会在Extended Detail inspector中看到被捕获的堆栈跟踪。
五、这里是检查器(inspector)面板,一共有三个检查器:record setting(记录设置),display setting(展现设置),还有extends detail(扩展详情)。一会你将了解更多关于这里面的一些选项。
如今开始诊断这笨重的UI!:]
更进一步
搜索一次图片,而后点击结果进入详情界面,我我的喜欢搜索"狗",不过选一个你喜欢的就好--你多是想搜索猫的一员:]
如今连续上下滚动列表数次,这样你就在Time Profile工具中获得足够的数据了,能够发现屏幕中央的数字在改变,图表也开始被填充,这说明正在占用CPU循环。
你固然不但愿任何UI如此笨重,那么table view就绝对不会被忽略,除非它滚动起来很是流畅。
要定位这里的问题,你须要设置一些选项。
在右手边,选择display setting(或者按下commond+2),在该选择器中,在Call Tree栏下选中Separate by Thread, Invert Call Tree, Hide Missing Symbols 和 Hide System Libraries选项,你的界面应该看起来是这样的:
下面解释了每个选项对左侧列表中数据的显示起了什么做用:
Separate by Thread:每一个线程被单独考虑。这能让你知道哪个线程占用CPU最多。
Invert Call Tree:选中该选项后,调用栈会自上至下显示。这一般是你须要的,由于你想知道CPU花费时间的那个最深的方法。
Hide Missing Symbols:若是在你的app或者框架中找不到dSYM文件,那么你将只能在列表中看到二进制代码中的十六进制地址值,而不是方法的名称(符号)。选中该选项后,只有能被解析的符号能够被显示出来,未被解析的十六进制数值会被隐藏,这有助于清理显示的数据。
Hide System Libraries:选中该选项后,只有你本身app中出现的符号会被显示出来。一般选中该选项是有用的,由于你只关心CPU在你本身的代码中的哪一部分花费时间,你无法对系统库使用CPU作多少改变。
Flatten Recursion:该选项将每个调用栈中的递归函数(调用它们自身的函数)视做单一入口,而不是多入口。
Top Functions:选上这一选项让Instruments将花费在一个函数中的总时间视做在该函数中直接花费的时间加上调用的其余函数花费的时间。因此若是函数A调用了函数B,那么函数A花费的总时间被记为A花费的时间加上B花费的时间。这一选项很是有用,由于它能让你在每次进入调用栈时找到花费最长的时间,瞄准你最耗时的方法。
若是你正在使用Objective-C写的app,那么这里还有一个选项:Show Obj-C Only,选择该选项后,只展现Objective-C方法,不展现其余任何C或C++的函数。目前你的app中没有C或C++函数,可是举例来讲,若是你正在看的是一款OpenGL应用,那么可能会有一些C++的函数。
尽管一些值可能会有轻微的不一样,不过若是你选中了上面提到的几个选项后,列表中展现的入口的顺序应该是相似于下图的:
额,这看起来不怎么好,大量的时间被花在设置缩略图的"色调"滤镜('tonal'filter)的方法上了。这应该不会太让你惊讶,由于列表的加载与滚动是UI中最笨重的部分,而这里正式列表单元格被持续加载的地方。
为了解到更多关于这个方法作了什么的信息,双击列表中的这一行,这样将把你带到下面的视图中:
这颇有趣,不是吗?applyTonalFilter()是一个UIImage扩展中的一个方法,几乎100%的时间被花费在这个方法中的应用图片滤镜后建立CGImage输出这一地方了。
咱们没办法为这一过程加速,建立一张图片是个费时的过程。让咱们回退一步,看看applyTonalFilter()是从哪里调用的。点击代码界面的顶部栏中的Call Tree,回到上一界面。
而后点击列表顶部applyTonalFilter左侧的小箭头,这样就展开了Call Tree,展现出applyTonalFilter的调用者。你可能须要再展开到下一行。当你分析的是swift代码时,有时在Call Tree中会出现重复的一行,以@objc为前缀,此时你只须要关心第一行,以你的app的target名称为前缀(本例为InstrumentsTutorial)。
这里,该行代指collection view的cellForItemAtIndexPath方法的结果,双击该行能够看到工程中相关的代码。
如今你知道问题出在哪了。应用色调滤镜的方法占用了较长的时间,而该方法又直接从cellForItemAtIndexPath中调用,这样每当该方法要求一个被滤镜渲染的图片时都会会阻塞主线程(整个UI)。
卸下重任
要解决这一问题,能够分两步来:首先使用dispatch_async将建立滤镜的方法放到后台线程,接着在每一张图片被建立后都缓存起来。咱们的工程中有一个简单的图片缓存类(有一个易记的名字:ImageCache),简单地将图片保存到内存中,而后经过给定的键来获取它们)。
如今能够切换到Xcode上,手动找到当前你正在Instruments中看的源文件,不过如今在你的眼前,右侧就有一个快捷按钮Open in Xcode,在面板的代码部分的上面找到它并点击:
这样,Xcode就定位到正确的位置了。
接下来,在collectionView(_:cellForItemAtIndexPath:)方法中,把调用loadThumbnail()方法替换为下面的代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
flickrPhoto.loadThumbnail { image, error
in
if
cell.flickrPhoto == flickrPhoto {
if
flickrPhoto.isFavourite {
cell.imageView.image = image
}
else
{
if
let cachedImage = ImageCache.sharedCache.imageForKey(
"\(flickrPhoto.photoID)-filtered"
) {
cell.imageView.image = cachedImage
}
else
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), {
if
let filteredImage = image?.applyTonalFilter() {
ImageCache.sharedCache.setImage(filteredImage, forKey:
"\(flickrPhoto.photoID)-filtered"
)
dispatch_async(dispatch_get_main_queue(), {
cell.imageView.image = filteredImage
})
}
})
}
}
}
}
|
这段代码的第一部分和以前同样,从网络上加载Flickr的图片,若是该图片被渲染过,那么cell直接展现相应的缩略图,若是没有被渲染过,就将色调滤镜应用到图片上。
接下来就是改变的地方,首先代码检查图片的滤镜是否存在于图片缓存中,若是是,那么直接交由image view展现,若是没有,那么为图片添加色调滤镜的方法被分配到后台队列中执行,当该滤镜被渲染好之后,将渲染后的图片保存到缓存中,在主线程中让image view显示图片。
这样就解决了须要滤镜的图片的问题,不过还须要考虑从Flickr请求下来的本来的缩略图。打开FlickrSearcher.swift,找到loadThumbnail(_:),将其替换为:
1
2
3
4
5
6
7
8
9
10
11
12
|
func loadThumbnail(completion: ImageLoadCompletion) {
if
let image = ImageCache.sharedCache.imageForKey(photoID) {
completion(image: image, error: nil)
}
else
{
loadImageFromURL(URL: flickrImageURL(size:
"m"
)) { image, error
in
if
let image = image {
ImageCache.sharedCache.setImage(image, forKey: self.photoID)
}
completion(image: image, error: error)
}
}
}
|
这里与处理滤镜图片相似,若是一张图片已经存在于缓存中,那么直接用缓存的图片来调用completion回调,不然从Flickr上请求图片并保存到缓存中。
经过Product/Profile(或者commond+I,记住,这些快捷键能够节省你大量时间)打开Instruments,从新运行app。
能够发现这一次你不须要选择使用哪一个工具,由于你的app仍然在一个窗口中打开着,Instruments假定你想以一样的选项再次运行。
进行几回搜索,能够发现此次UI不是那么慢了,如今图片滤镜是异步渲染,图片也在后台被缓存,因此它们只须要被渲染一次,能够在Call Tree中看到几个dispatch_worker_threads,这里是处理繁重的加载图片滤镜的过程。
看起来不错,是时候作一次跨越了:]
分配、分配、分配
本教程要介绍的下一个工具是Allocations工具,它能够给你关于全部被建立的对象和它们背后使用的内存的详细信息。它也能显示出每一个对象的引用计数。
要打开一个新的分析工具,首先退出Instruments工具。此次,编译并运行app,在导航栏中点开Debug栏,而后点击Memory就能够在主窗口中显示内存的使用图表。
这些图表能够帮你大致上了解你的app的表现,不过你须要更强大的功能。点击Profile in Instruments按钮,而后能够把这部分转换到Instruments中。Allocations工具会自动打开。
此次你须要注意两个追踪,第一个叫作分配(Allocations),第二个是泄露(Leaks),分配追踪将在下文详细讨论,一般泄露追踪在Objective-C中更有用,因此本篇教程不会涉及。
那么接下来你将去查找哪一个bug呢?
有些事被隐藏在工程中,你可能不知道它的存在。你可能据说过内存泄露,但不知道其实有两种泄露:
一、"真正的内存泄露(True memory leaks)"是指一个对象再也不被引用但却没有被释放--这说明内存永远不能被复用,即便有swift和ARC帮助管理内存,最多见的内存泄露问题是保留环,或称为强引用环。当两个对象互相持有对方的强引用时,每一个对象保证另外一个不会被释放,这样它们的内存将永远不能被释放!
二、"无限内存增加(Unbounded memory growth)"是指内存持续被分配而没有机会被释放。若是这一现象永远持续下去,某一点上系统资源将被占满,这样你就亲手建立了一个大的内存问题。在iOS上意味着你的app将被系统杀死。
Allocations工具运行在app上时,进行五次不一样的搜索,但不要点进详细界面,确保每次搜索都有一些结果,如今让app静止等待几秒钟。
你应该能注意到Allocations追踪中的图表一直在增加,这说明内存正在被分配,这一特色将指导你找到无限内存增加问题。
接下来你要执行"分配分析(generation analysis)",要作到这一点,点击Mark Generation按钮,你能够在Display Setting检查器的顶部找到这一按钮。
按下它,你将会发现一个红旗出如今追踪中,以下:
分配分析的目的是屡次执行一个事件,查看内存是否以无限的形式增加,点击进入搜索的详情界面,等待几秒钟的图片加载,而后返回主页,再一次mark generation,对于不一样的搜索重复几回这样的操做。
在进入几回详情界面之后,Instruments将看起来以下图所示:
这时你应该会有所起疑,能够注意到每次搜索并进入详情界面后蓝色的图表都在增加,这样确定很差。不过等一下,内存警告呢?你应该知道的,内存警告是iOS告诉app内存紧缺的一种方式,并通知你你须要清理一些内存。
有可能这种增加不只仅是你的app形成的,它多是UIKit内部使用内存的结果。因此在指定具体哪个出现问题以前,给系统框架和你的app一个机会来清理本身的内存。
能够在Instruments的菜单栏中选择Instrument\Simulate Memory Warning来模拟一次内存警告,或者从模拟器的菜单栏中选择Hardware\Simulate Memory Warning。你会注意到内存使用图下陷了一点,也可能根本没有。很显然使用图没有回到应该的位置上,所以你的程序的某处依然有无限内存增加的问题。
每次点入详情界面后都作一次标记的缘由是,你能够看到在每一个标记段之间哪些内存被分配了。看一眼详情面板,你会发现有大量的内存分配。
漫谈分配
在每个generation段中,你能够看到全部自标记以来被分配了内存空间,而且一直存活的对象。随后的每一个generation段中只包含自上一个标记以后的符合上述描述的对象。
看一眼Growth栏,你就会发现确定在某处存在着增加问题,展开其中一个generation,你会看到以下图界面:
哇,有好多的对象,咱们从哪开始呢?
很不幸,在这一界面上swift比Objective-C杂乱得多,由于这里充满了你并不须要了解的内部数据结构。你能够经过切换Allocation Type至All Heap Allocations来方便地清除掉它们。固然也能够点击顶部的Growth头,让对象按照大小排序。
最顶部的对象是ImageIO_jpeg_Data,而且这确定是你的app创造的对象。点击ImageIO_jpeg_Data左侧的箭头展开详情列表,选中一行,而后打开Extended Detail检查器(或者按下commond+3)。
这里显示的是当指定对象被建立时的栈追踪,灰色部分的属于系统框架,黑色部分是你的app中的。要了解这一追踪的更多信息,双击黑色部分倒数第二行,这是惟一以InstrumentsTutorial开头的一行,表明它是来自swift代码的。双击它会把你带到相关方法的代码界面--你的老朋友collectionView(_:cellForItemAtIndexPath:)。
Instruments很是有用,可是这里它不能帮你更多了,如今你必须亲自浏览一遍代码来了解这里到底发生了什么。
看一遍代码,你会发现它调用了setImage(_:forKey:)方法,正如你在Time Profiler中看到的,这个方法缓存图像以便以后在app中复用。啊哈,听起来就像一个问题。
再次点击Open in Xcode跳入Xcode界面,打开ImageUtilities.swift,看一下setImage(_:forKey:)的实现:
1
2
3
|
func setImage(image: UIImage, forKey key: String) {
images[key] = image
}
|
这里以Flickr的图片ID做为键,将图片保存到字典中。可是若是你总体浏览一遍代码,你会发现图片永远不会从字典中被清除。
这就是你的无限内存增加的来源:全部事情都按照设定来工做,可是app永远不会清除缓存--它只是不断地往里增长。
要解决这一问题,你须要作的是让ImageCache监遵从UIApplication发来的内存警告的通知。当它收到通知后就清除掉它的缓存。
要让ImageCache监听通知,在该类中添加init和deinit方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
init() {
NSNotificationCenter.defaultCenter().addObserverForName(
UIApplicationDidReceiveMemoryWarningNotification,
object: nil, queue: NSOperationQueue.mainQueue()) { notification
in
self.images.removeAll(keepCapacity:
false
)
}
}
deinit {
NSNotificationCenter.defaultCenter().removeObserver(self,
name: UIApplicationDidReceiveMemoryWarningNotification,
object: nil)
}
|
这里注册了UIApplicationDidReceiveMemoryWarningNotification的观察者来执行上面的闭包,清除图片缓存。
代码须要作的就是移除缓存中的全部对象,这样就确保这些图像再也不占有什么资源,它们将被释放掉。
为了测试这一修改,再次启动Instruments(在Xcode中按下快捷键commond+I),重复以前的步骤,别忘了最后模拟一次内存警告。
注意:确保你是从Xcode中启动并通过编译,而不是仅仅按下Instruments中的红色按钮,这样才能确保你使用的是最新的代码。你也可能须要在进行分析以前先编译运行一次,由于有时若是你直接分析,那么Xcode彷佛没有将模拟器中的app编译更新到最新代码上。
这一次的分配分析应该看起来是这样的:
能够发如今内存警告以后内存的使用下跌了。整体上依然有不少内存增加,可是不像以前那样多了。
如今依然有不少内存增加是由系统库形成的,而且你也无法对其作一些改进。这些系统库并无释放它们的所有内存,这有多是刻意设计的,也有多是一个bug。你能对你的app作的就是尽量多地释放内存,而这一点你已经作到了! :]
很是好!又解决了一个问题!是时候进行新的跨越了。哦等等,还有第一种类型的泄露问题你没有涉及到。
强引用周期
最后,你将寻找在Flickr图片搜索app中的强引用环。正如以前提到的,当两个对象互相持有对方的强引用时会出现强引用环。你能够用另外一种方式使用Allocations工具来检测这一环。
注意:为保证你能跟上这篇教程的这一部分,你必须在一个真机上来分析你的app。不幸的是在写该教程时,当在模拟器上运行app并启用Allocations工具时会出现一个bug:大多数在工程中使用到的类没法出如今Instruments中。
关闭Instruments,返回Xcode,确保你的app的构建目标选中为真机设备。再一次选中Product\Profile,而后选择Allocations模板。
这一次,你再也不使用分配分析,取而代之的是,你要看存在于内存中的不一样类型对象的数量。你应该已经看过数量庞大的对象填充于详情面板--数量太多以致于看不过来。
为了筛选本身感兴趣的对象,在Allocations Summary列表上方的文本框中输入Instruments做为筛选词,这样就只会显示类型名中带有Instruments关键词的对象。由于咱们的示例工程名称为InstrumentsTutorial,Allocations列表将仅仅显示这个工程中定义的那部分类型的对象。这样就简化了些工做。
这里有两列值得一提:#Persistent和#Transient,Persistent这一列记录了存在于内存中的每一类型的对象的数量。Transient这一列记录了曾经存在可是如今已经被销毁了的对象的数量。Persistent对象(持久对象)正在使用内存,而Transient对象(临时对象)已经将它们占用的内存释放了。
你应该能看到有一个持久对象实例:ViewController,那就对了,由于这就是你当前看到的界面。除此以外,还有AppDelegate,还有一个Flickr API客户端的实例。
回到app中,执行一次搜索并点进详情界面,注意到有大量新的对象出如今Instruments中:解析搜索结果时建立的FlickrPhotos、还有SearchResultsViewController、还有ImageCache,ViewController实例依然是持久对象,由于它被它的导航控制器持有,这样很好。
如今按下返回按钮,SearchResultsViewController被从导航栈中弹出
,因此它应该被销毁。可是Allocations统计中#Presistent这一列依然显示着数量为1,为何依然存在呢?
试着进行另外两次搜索并每次都经过back按钮返回,如今一共有3个SearchResultsViewControllers?!这些视图控制器依然存在于内存中的事实说明有其余对象持有它们的强引用,看起来你有一个强引用周期。
此时你的主要线索是,不仅SearchResultsViewController存在,全部的SearchResultsCollectionViewCells也存在。看起来好像保留环是存在于这两个类之间的。
很不幸,在编写本教程时,Instruments对swift的输出在一些状况下并非怎么颇有用,这里Instruments只能给你一些关于问题出在哪里的提示,并展现对象从哪里分配的,接下来解决问题就是你的工做了。
让咱们去代码中一探究竟。把鼠标放到Category一栏的InstrumentsTutorial.SearchResultsCollectionViewCell上面,点击右边的小箭头,接下来的视图展现了运行app时SearchResultsCollectionViewCells的全部分配状况。有很是多的实例--每个查询结果对应一个。
经过点击面板顶部第三个按钮切换检查器到Extended Detail检查器,这一检查器显示的是当前选中分配的栈追踪。和以前的栈追踪同样,黑色部分是你的代码,双击最顶部的黑色的一行(以InstrumentsTutorial开头),看一下cell在哪被分配。
Cell是在collectionView(cellForRowAtIndexPath:)的一开始被分配的。若是你浏览接下来几行,你会看到这个(很不幸,Instruments没有给你提示显示):
1
2
3
|
cell.heartToggleHandler = { isStarred
in
self.collectionView.reloadItemsAtIndexPaths([ indexPath ])
}
|
这是处理点击一个集合视图单元格上的爱心按钮的闭包,这就是产生循环引用的问题的地方,但这很难发现,除非你以前遇到过这种状况。
Cell闭包经过self引用SearchResultsViewController,从而产生了一个强引用。实际上swift强制你在闭包中使用self(然而在指代当前对象的属性和方法时你一般能够省略它),这有助于加深你对正在捕获self这一事实的认识。经过集合视图,SearchResultsViewController也对这些cell持有强引用。
为了打破强引用环,你能够定义一个捕获列表(capture list)做为闭包定义的一部分,捕获列表能够用来声明实例,这些实例被闭包捕获时或者是weak,或者是unowned:
weak:当捕获的引用在之后可能会变成nil时使用,若是引用的对象被释放,引用变量自动变成nil。所以,这些变量都是可选类型。
Unowned:当被引用的对象和闭包拥有相同的生命周期而且会被同时释放时使用,一个unowned变量永远不多是nil。
要解决这个强引用环问题,再次点击Open in Xcode按钮,而后在SearchResultsViewController.swift的heartToggleHandler中添加捕获列表:
1
2
3
4
5
|
cell.heartToggleHandler = { [weak self] isStarred
in
if
let strongSelf = self {
strongSelf.collectionView.reloadItemsAtIndexPaths([ indexPath ])
}
}
|
把self声明为weak说明SearchResultsViewController可能被释放,即便集合视图的cell持有它的一个引用。由于如今它们之间的引用仅仅是弱引用。而且SearchResultsViewController的释放也会引发集合视图的释放,接着,cell释放。
在Xcode中,再次使用commond+I在Instrument中编译并运行app。
和以前作的同样,在Instruments中,再次使用Allocations工具观察app(记住要筛选结果,只显示属于咱们的示例工程部分的类)。执行一次搜索,导航到结果中,而后再次返回。能够看到此次当你导航返回时SearchResultsViewController和它的cell都被释放了。它们如今是临时对象,而不是持久对象。循环打破!再一次跨越!:]