启航之Swift语言下的Instruments使用教程

图1

学习如何使用【Xcode Instruments】来进行错误排查和优化代码。

更新提示:这篇教程由James Frost进行更新以适配iOS 8 和 Swift语言,原始教程由教程组成员Matt Galloway提供。

不管你是开发过众多iOS App的老手,还是正在着手于你的第一个App的新手:但是毫无疑问的是,你要使用最新的语言特性,并且想知道怎么做才能让你的App变得更好。

要想改进完善你的App,除了尽可能使用最新的语言特性外,还有一件所有优秀的app开发者应该做的——instrument你的代码!

PS:(instrument:仪器、工具;用仪器,用工具。文中直接使用英文单词,各人根据自己喜好定含义。)

这篇教程将向你展示如何使用Xcode自带的调试工具Instruments的几个最重要的方面。该工具可以帮你检查代码中涉及到的性能问题、内存问题、循环引用问题以及其他许多难题。

在这篇教程中你将学到以下内容:

1、如何使用Instruments中的Time Profiler选项来找出代码中最耗时的“热点”,以改进,使代码的运行效率更高。

2、如何使用Instruments中的Allocations选项,检测并修复代码中的内存管理问题,比如强循环引用。

注意:在这篇课程中,我们假设你对Swift语言和iOS编程已经相当的熟悉。但是如果你是个iOS编程方面的新手,你或许还需要浏览一下该网站上的其他内容。本课程使用到了storyboard(故事板),所以请确保你已经熟悉storyboard这个东西。如果不熟悉,请浏览这个链接

准备好了吗?我们即将开始深入探索迷人的Instruments世界了。

正式开始了:

在此,你不需要为此课程从头开始创建一个全新的应用程序,因为,我们已经给你提供了一个样例工程。你所需要的做的就是通览整个程序,根据instruments给出的指引改进这个程序,就像你要优化自己的app一样。

点击下载样例工程,然后解压并使用Xcode打开。

这个样例程序使用了Flickr API来搜索图片。要想使用Flickr API你需要一个API key。在演示工程中,你可以通过Flickr网站来生成一个样例key。具体做法是,只要通过下面的网址,执行一个搜索即可:

http://www.flickr.com/services/api/explore/?method=flickr.photos.search

在返回的结果网址中,拷贝出URL中的API key即可,该key的内容是“&api_key=” 和下一个“&”中间的部分。

比如,如果我们得到的URL是下面这样的:

http://api.flickr.com/services/rest/?method=flickr.photos.search&api_key=6593783

efea8e7f6dfc6b70bc03d2afb&format=rest&api_sig=f24f4e98063a9b8ecc8b522b238d5e2f

那么我们所需要的API key就是:6593783efea8e7f6dfc6b70bc03d2afb。

然后将上面那个key粘贴到FlickrSearcher.swift 文件的顶部替换掉原来的API key即可。

注意这个样例API key可能每天都在变化,所以你可能偶尔需要重新生成一个新的key。不论什么时候,如果这个key不再有效,app都会给出警告的。

接着,编译并运行这个app,然后执行一个搜索,搜索结果出来后,点击搜索结果,你将会看到类似下面图片中的内容:

图2

通览整个应用程序并检查其中的基础函数。你可能忍不住会想一旦App的UI界面运行良好,就可以将代码提交到库中了。然而,请等一等,接下来你将看到使用instruments能给的App带来的好处。

余下的课程将向你展示,如何找到并修复App中仍然存在的问题。你将会看到使用Instruments调试程序中的问题是多么的简单。

时间性能检测

图3

你第一个接触到的Instruments选项是Time Profiler在每个测量的时间间隔内,Instruments将会暂停程序的执行,并记录每个正在运行着的线程的堆栈执行情况(堆栈轨迹)。想象一下,这就像你在Xcode的调试模式下运行程序时按下了暂停按钮。

这里是一张Time Profiler运行时的预览图:

图4

这张屏幕截图中展示了Call Tree(调用层次树)。Call Tree能够显示出app中不同方法执行时所花费的时间。图中的每一行代表着在程序执行过程中所追踪到的不同方法。每个方法所花费的时间可以通过Profile在其上停留的次数来断定。

比如,每1毫秒完成一次采样,现在完成了100次采样,此时我们在栈顶端发现某一方法被采样10次,那么你可以估测大约10%的运行时间——10毫秒——花在了该方法上。虽然这只是比较粗略的估计,但却是有效的。

注意:一般情况下,你总应该在真机上检测你的程序,而不是在模拟器上。iOS模拟器使用的是电脑的硬件资源(远超移动设备的硬件性能),而真机会受限于其移动设备的硬件性能。有时候你会发现你的app在模拟器上运行情况良好,但是在真机上运行时,会出现一些性能问题。

好了,还是不要杞人忧天了,是时候开始检测了。

从Xcode的菜单栏中,依次选中菜单项:Product\Profile,或者按组合键⌘ + I。接着将编译程序,并运行Instruments。你将会看到类似下面的一个选项窗口界面:

图5

这里面列出了Instruments自带的所有的工具模版选项。

选中Time Profiler项并点击右下角的Choose按钮,这将打开一个新的Instruments运行界面。在新出现的界面中,点击左上角的红色按钮启动程序并开始记录。此时,你可能会被要求输入密码以授权Instruments分析其他进程——不过不用担心,在此提供密码是安全的。

在Instruments窗口中,你可以看到时间在累加,同时在屏幕中央的图表上方,一个小箭头在从左到右的移动着。这意味着app正在运行。

现在,操作你的app。

搜索几张图片,并层层深入点击一个或多个搜索结果,你或许已经注意到了,要查看一个搜索结果非常的慢,并且要对所有的搜索结果进行滚动操作也非常的慢——这是一个体验非常差的app。

不过,你还是很幸运的,因为你即将着手修复上述问题。然而,你只是第一次浅显的接触Instruments。所以,首先,确保工具栏上右上角的视图选择按钮都被选中。像下图中的这样:

图6

这将确保所有的输出面板都被打开。现在,我们开始学习下面屏幕截图中的内容以及图片下方对每一项的解释:

图7

1、这里是录制控制部分,当你点击红色的“录制”按钮时,将开启或关闭当前正在监测的app(当该按钮被触发时,其状态在“录制”和“停止”之间切换)。“暂停”按钮暂停当前app的执行。

2、这是执行计时器,该计时器计算当前被检测的app运行了多长时间,以及被执行了几次。如果你使用录制控制器停止并重启当前的app,那么将会重新运行一次Time Profiler并且该计时器上将显示:Run 2 of 2

3、这里是显示的是某个工具的使用轨道。当你只选择Time Profiler模版这一项时,只显示一个轨道。在后面的课程中你将学习到更多在这里显示的图表的相关细节。

4、这里显示的是详情面板。它呈现的是你当前正在使用的Instruments配置选项的主要信息。在目前这种选项下,它能够显示出使用CPU时间最多的所谓“最火”的方法。

5、这是检查器面板。这里包含三种检查器:录制设置、显示设置、和扩展信息。很快你就可以学到有关这些选项的更多内容。

现在,我们开始修复那个体验极差的UI。

深入了解

执行一次图片搜索,并深入搜索结果。我个人喜欢搜索“狗”,当然你也可以搜索其他东西,比如:“猫”:

现在,对搜索结果列表做几次上下滚动的操作,这样就可以为Time Profiler获取到大量的分析数据。你应该注意到了屏幕中部的数字改变了,上述的轨道图表也被不同的颜色填满了;这是在向你传达该程序使用CPU的情况。

你肯定不喜欢像现在这样糟糕的UI——列表竟然没能及时加载数据!要精确地定位该问题,你需要对某些选项做一些配置。

在右手边,选择Display Setting检查器(或者快捷键⌘+2)。在检查器窗口中,在Call Tree选项下面, 选择Separate by ThreadInvert Call TreeHide Missing SymbolsHide System Libraries。操作完成后,界面看起来应该是这样的:

图8

这里是各个选项如何对左边列表中的数据的显示产生影响的。

1、Separate by Thread: 每个线程都应该单独对待。这样可以让你知道到底哪个线程占用了最多的CPU周期。

2、Invert Call Tree: 利用这个选项,堆栈使用情况按照从上到下的方式排列。通常情况下,这也是你想要的,因为你可能想看到最深一层的方法调用以及其所占CPU时间周期。

3、Hide Missing Symbols: 如果你的app或者system framework(系统框架文件)的dSYM文件没有被找到,那么你将看到的是方法在库中的十六进制地址,而不是方法名(symbols)。如果该选项被勾选,那么你将看到的是完整的方法名,而不是难以理解的十六进制数字。这可以帮你优化当前数据的显示。

4、Hide System Libraries: 当这个选项被勾选时,只有你自己app中的方法名会被显示。通常情况下,勾选这个选项还是很有用的,因为你只会关注自己代码所使用的CPU时间——当然你不会关注,也无法控制系统代码对CPU的使用。

5、Flatten Recursion: 这个选项在每个堆栈上把递归函数(自己调用自己的函数)作为一个条目来对待,而不是多条。

6、Top Functions: 启用这个选项可以让Instruments这样计算一个函数花费的总时间——自己本身花费的时间和内部调用其他函数花费的时间之和。举个例子,函数A调用函数B,那么我们看到的花费在A上的时间就是A本身花费的时间加上花费在B上的时间的总和。这样做非常有用,这可以让你每次从调用堆栈中按照降序的方式获取到最大的时间数字,让你集中精力关注那个花费最多时间的方法。

7、如果你正在运行的是Objective-C app,这里还会出现一个选项Show Obj-C Only:此时,如果这个选项被选中,那么只有Objective-C类型的函数会被显示,C、C++类型的函数则不会被显示。当你的程序中没有C、C++类型的函数时,该选项没有什么作用,但是如果我们正在运行的是一个OpenGL app,其中很可能会有一些C++函数,此时该选项就可以发挥做用了。

虽然在一些数值上可能会有细微的差别,但是一旦你启用上述的那些选项,相关条目的排列顺序应该与下表相似:

图9

嗯,那看起来确实很糟糕。大部分的时间都花在了方法applyTonalFilter上面了,不过这不应该让你感到很震惊,因为表格的加载和滚动才是UI体验最差的部分,尤其是在表格单元不断更新的时候。

要找出有关该方法更多的内部细节,双击表格中该方法所对应的这一行,接着将会跳转到下面的界面:

图10

是不是很有趣,applyTonalFilter()是UIImage的一个扩展方法,几乎100%的花费在它上面的时间都被用在了生成过滤后要输出的图片上。

要想提速这个过程,还真没有什么好的办法,毕竟创建图片是一个连续的过程,要一直持续到其创建完毕。现在让我们跳回一步看看applyTonalFilter()是在哪里被调用的。点击代码预览区顶部的浏览路径记录中的Call Tree返回上一个界面看看:

图10

现在点击表格顶部的函数applyTonalFilter左侧的小箭头,这将展开Call Tree以显示applyTonalFilter的上级调用者。你可能还需要展开下一行;当对Swift语言做性能分析时,有时会在Call Tree中出现以@objc为前缀的重复行。你所感兴趣的应该第一行中以你的app名字为前缀的那个调用者(此处的前缀应该是InstrumentsTutorial):

图11

(PS:自己插一句,从这里也可以看出Swift语言和Object-C语言的关系,更好的理解在开发中二者为什么可以混编)

这种情况下,你可以看到该行涉及到结果集合界面中的函数:cellForItemAtIndexPath。双击该行以查看工程中的相关代码。

现在你可以看出到底是什么问题了。直接由函数cellForItemAtIndexPath调用的色调过滤方法执行时占用了太长的时间,这样一来每次请求一张过滤后的图片时都会阻塞主线程(进而阻塞整个UI界面)。

进行分流的操作

要解决这个问题,你需要两步走:第一,通过dispatch_async(异步线程函数)函数将图片过滤方法分流到后台线程中;然后,缓存已经生成的图片。在样例工程中已经提供了一个轻量级的图片缓存类(有一个引人注目的名字ImageCache),该类将图片储存到内存中,然后在需要时通过一个键值再将图片取出来。

你现在可以手动切换到Xcode看到你在Instruments中所看到的代码,但是有一个更便捷的方法实现这一功能:点击下图红色圈圈中的按钮即可。

图12

你可以看到,Xcode在非常精确的位置显示出相应的代码。

现在, 在collectionView(_:cellForItemAtIndexPath:)中, 使用下面的代码来替换对loadThumbnail()的调用:

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显示当前的缓存的内容,否则色调过滤器就会被调用来生成相应的图片。

那么哪些内容发生改变了呢:首先,这里代码会首先检测缓存中是否已经有存在的图片,如果有,那好,图片会被直接显示出来。如果没有,会对图片调用色调过滤器代码,并将这些代码发送到后台队列处理。这样在对图片进行加工处理的同时,仍然能够让UI保持非常流畅的响应。当加工处理的操作进行完毕后,图片被缓存,然后在主线程中更新图片的显示。

上述那些是已经处理好的图片,但是仍然有一些原始的Flickr缩略图需要你去关注。打开FlickrSearcher.swift文件并找到loadThumbnail(_:)这个函数。用下面的函数替换掉该函数。

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加载然后加工处理后再储存在缓存中。

通过Xcode中的菜单项Product\Profile,在Instruments中重新运行这个app(或者使用快捷键⌘+I )。

像前面说的那样,执行几个搜索任务,这次你会看到UI的体验没有像先前那么迟钝了。图片过滤器现在在后台异步调用,图片缓存也被放到了后台,也就是说图片仅仅是加工过滤一次,同时也仅仅被缓存一次。在Call Tree中你将看到大量的后台工作线程,这些线程用来处理任务繁重的图片处理工作。

一切看起来那么的美好,是时候上传代码了吗?还没有!

分配、分配、再分配

本课程中的下一个Instruments选项是Allocations。该选项会给出程序中创建的所有对象的详细信息,以及它们使用内存的情况,同时该选项还会呈现出每个对象的引用计数。

退出先前运行的app,重新启动一个Instruments配置项。这次,编译并运行app,然后在导航区域打开调试导航选项。接着点击子选项Memory,在主窗口中显示内存使用情况的图表。其图表应该和下面这张图差不多:

图13

这些图表对于想快速了解你的app当前的运行状况是非常有用的。但是你或许需要更进一步。点击窗口右上方的Profile in Instruments按钮,然后该会话将自动被带入到Instruments中,并且Instruments将自动打开Allocations选项。

图14

这次你将注意到有两个条目轨道。一个叫Allocations, 另一个叫Leaks。轨道Allocations将在稍后讨论其细节;轨道Leaks通常情况下在Objective-C中更有用,所以本课程不涉及这部分。

那么接下来你将追踪什么Bug呢?

项目中可能有一些你不知道的东西隐藏在其中。你很可能听说过内存泄漏。但是或许你还不知道其实存在两种类型的内存泄漏:

1、True memory leaks(真正的内存泄漏),存在于一个对象不再被使用,但是仍然占用已经分配了的内存——这意味着其所占用的内存永远不可能被重复利用,即使使用Swift和ARC帮忙管理内存。最常见的内存泄漏是retain cycle(循环持有)或者strong reference cycle(强引用循环)。这种情况发生在两个对象相互之间强引用,因此在析构时发现两个对象相互持有,这也就意味着它们所占用的内存永远都不可能被释放,造成内存泄漏。

2、Unbounded memory growth(无界内存增长) 这种情况发生在内存持续的分配却从没有机会释放,如果任由这种情况持续,然后在某个时间点上系统内存将会被耗尽,此时你也必将面临着该怎么处理一个大的内存管理问题。在iOS系统环境中,这意味着你的app将会被系统干掉。

在Instruments的Allocations选项下运行app,在app中做5次不同的搜索,但是先不要对搜索结果进行点击操作。在确保有搜索结果的情况下,现在等几秒钟,让app独自“静静”。

你或许已经注意到了在轨道Allocations中的图表内容一直在增长,这是在告诉你内存正在被分配。正是这种特性将引导着你找到“无界内存增长”。

接下来你要执行的操作是“内存生成分析”(“generation analysis”)。要这么做,你只需点击Mark Generation按钮。如下图所示:

图15

点击Mark Generation按钮后,你将看到一面红色的小旗出现在轨道中,就像下面这样的:

图16

“内存生成分析”的目的是多次执行一个动作,看看内存是否会无限的增长。进入一个搜索结果,接着等几秒钟等待图片加载完毕,然后返回主界面。然后再次执行生成分析。对不同的搜索结果,重复执行上述过程几次。

反复点击几个搜索结果后,Instruments将会看起来是这样的:

图17

在这个时候,你应该产生怀疑。注意一下,看看你每次进入搜索结果的时候蓝色图表的增长。这看起来确实不好,但是等等,我们都知道的内存警告呢!内存警告是iOS系统下的一个告知app内存变得紧张,需要做内存清理的一种方式。

有可能上述现象的产生不仅仅是你的app造成的,也有可能是系统UIKit库中某个深层次的原因。在你指责它们之前,给系统框架或者你自己的app一个机会去清理内存。

点击Instruments菜单栏中的Instrument\Simulate Memory Warning项,或者模拟器菜单栏中的Hardware\Simulate Memory Warning项,来模拟一个内存警告。你将观察到内存使用降了一点点或者根本就没有降,图表根本没有变回到它该有的形式。因此我们可以断定在某个地方仍有“无界内存增长”产生了。

关于你每次浏览搜索结果后内存就增长的原因,看一下详情面板,你会看到一大堆分配内存的条目。

探讨每一次的标记

在每次做标记的过程中,你会看到所有的对象都被分配了内存,到下次做标记的时候,这些东西仍然存在。下次标记只包含在上次标记时出现的对象。

看看“Growth”这一列,你会看到在某处内存明显的增长了,打开某一次标记,你将看到下面图中的情况:

图18

那么多的对象数据,从哪里下手呢?

不幸的是,相比于Objective-C语言,Swift将该界面搞得非常的乱,其中填充了好多你不需要关注的内部数据。你可以将Allocation Type切换到All Heap Allocations模式,以及点击“Growth”列的头部使数据按照数字大小排列内容,这样界面看起来会清晰一些。

在接近顶部的地方,有这么一行:ImageIO_jpeg_Data,这确实是app中的内容,用来处理某些事物的东西。点击ImageIO_jpeg_Data行左边的箭头显示完整的列表。对下面展开的列表中选择一行,点击该行右侧的向右的扩张箭头(或者快捷键:⌘+3),可以查看其堆栈使用轨迹:

图19

这显示了特定对象被创建时堆栈的使用轨迹。堆栈轨迹中,灰色的部分是系统库的内容,黑色部分才是你的app代码相关的部分。要为当前的堆栈轨迹获取更多的上下文信息,双击倒数第二行的黑色部分,也就是唯一的一个有着“InstrumentsTutorial”前缀的行,这表示这行内容来自于Swift代码。双击该行将跳转到相应的代码:collectionView(_:cellForItemAtIndexPath:)。

Instruments确实很有用,但是其作用有限!现在你需要自己观看代码以找出问题的原因。

通览整个方法,你会发现它正在调用setImage(_:forKey:),就像我们先前讲述Time Profiler时提到的,这个函数是用来缓存图片等,听起来好像问题就出在这里!

点击前面提到的“Open in Xcode”按钮,跳回到Xcode中。打开ImageUtilities.swift文件看看方法setImage(_:forKey:)的实现:

func setImage(image: UIImage, forKey key: String) {

images[key] = image

}

该函数将一张图片加入到字典中进行缓存,但是仔细看代码,你就会发现没有任何操作会将该图片从字典中删除!

这就是“无限内存增长”的原因——一直向缓存中添加东西,却从来没有删除缓存中的东西。

要修复这个问题,你需要让ImageCache监听UIApplication发出的内存警告通知,当ImageCache收到这个通知时就会执行清理缓存的操作。

要让ImageCache监听UIApplication发出的内存警告通知,向类中添加initializer和de-initializer两个方法:

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。但是别忘了最后模拟几次内存警告看看效果如何。

注意:先关闭Instruments,在Xcode中,点击菜单栏中的Product/Clean,对代码进行清理,然后编译运行,最后再运行Instruments。这样确保使用的是最新的代码。

这次“Mark Generation”的结果应该是下面这样的:

图20

你可能已经注意到了内存的使用量在内存警告后下降了,然而仍然有些部分的内存是增长的,但是其增长量比先前少多了。

仍有小部分内存增长的原因确实是因为系统库造成的,当然你对此也无能为力。看起来系统库没有释放所有的内存,这可能是被设计成如此或者是个bug。你能做的就是在你的app中尽量多的释放内存,而且你已经做到了。

干的好,又一个问题被解决了,但是现在还不是上传代码的时候,仍然有一些我们前面提到的第一类内存泄漏问题没有被定位到。

强循环引用

最后,你要开始着手找出程序的中的强循环引用了。如先前所提到的,强循环引用发生在两个对象持有相互的强引用时,导致最后对象不能释放,进而消耗内存。可以利用Instruments中的Allocations以一种不同的方式侦测到这种循环。

注意:要想学习课程余下的部分,你必须让app在真机上运行,而不是在模拟器上。

关闭Instruments,返回Xcode,连上真机,并确保app的编译目标选项为真机。再次选择Product\Profile,接着选择Allocations选项。

图21

这轮调试中,你只需要关注在内存中悬垂着的指针即可。你可能已经注意到了,详情面板上填满了大量的对象——太多了导致很难全部浏览。

为了缩小范围,只看自己感兴趣的对象,在Allocations Summary右侧的输入框中输入“Instruments”作为过滤词,那么就会只显示名称中有此关键字的对象了。因为样例app的名称就叫 “InstrumentsTutorial”,所以现在显示的都是本项目所定义的。这样一来事情看起来就简单了一些。

图22

Instruments中的“# Persistent” 和 “# Transient”这两列没有什么作用。“# Persistent”这一列记录的是当前内存中每种对象数量的计数, “# Transient”这一列记录的是曾经存在但是现在已经释放的对象数量。Persistent对象消耗内存,Transient对象所占的内存已经释放。

你应该可以看到一个ViewController实例——显示当前你看到的界面。另外还有AppDelegate实例,Flickr API客户端实例。

返回到app中,执行一个搜索,然后点击搜索结果。你可以看到Instruments又显示出很多其他的对象:当解析搜索结果的时候,FlickrPhotos相册被创建了,SearchResultsViewController和ImageCache也被创建了。ViewController实例仍然存在——此时导航控制器仍然需要用到它。

此时,点击app中的后退按钮——SearchResultsViewController对象应该被从导航堆栈弹出——那么它应该被释放。但是在Allocations摘要中的“# Persistent”列仍然能看到其计数为1!问题来了,为什么会出现这种情况??

尝试执行两次下述过程:执行一次图片搜索,然后点击进入搜索结果,接着点击后退按钮。此时你可以看到有3个SearchResultsViewControllers对象存在?!事实情况是,这些视图控制器对象仍然存在于内存中意为着有其他对象对它们产生了强引用——看起来你的代码中存在强循环引用。

图23

在这种情况下,你要排查的问题的主要线索不仅来自现存的SearchResultsViewController对象,还有所有的SearchResultsCollectionViewCell对象。貌似强循环引用发生在这两个类的实例对象之间。

不幸的是,在写这篇文章时,某些情况下,Instruments为Swift输出的信息仍然不是特别的有用——不是特别详细或者明确。Instruments只能给出问题在哪里的一些提示,以及显示出对象在什么地方被分配——接下来,还需要你自己去排查到底出了什么问题。

让我们深入代码中看看。把你的鼠标移动到Category列中的InstrumentsTutorial.SearchResultsCollectionViewCell这一项上,然后点击其右边的小箭头——跳转到的下个界面,将向你展示所有在app运行中SearchResultsCollectionViewCell对象的分配情况——很多项内容,每个都是由一次搜索结果产生的。

图24

操作右边的Inspector(检查器),点击其面板顶部右边第三个按钮,切换到Extended Detail选项。此时该Inspector将向你展示当前选中的对象的内存分配过程中堆栈使用轨迹。和前面介绍的堆栈使用轨迹情况相同。堆栈轨迹中黑色的部分是和你的代码相关的。双击最顶部的黑色行(以“InstrumentsTutorial”开头的那一行)看看cell是在哪里分配的。

所有的cell都是在方法collectionView(cellForRowAtIndexPath:)开始时被分配的,如果你多向下浏览几行代码,你会看到下面的情况:

cell.heartToggleHandler = { isStarred in

self.collectionView.reloadItemsAtIndexPaths([ indexPath ])

}

这是处理集合视图中每个cell上的心形按钮的点击动作的闭包。强循环引用就是在这里发生的——但是很难发现,除非你以前遇到过这种情况。

The closure cell refers to theSearchResultsViewControllerusingself, which creates a strong reference. The closurecapturesself. Swift actually forces you to explicitly use the wordselfin closures (whereas you can usually drop it when referring to methods and properties of the current object). This helps you be more away of the fact you’re capturing it. TheSearchResultsViewControlleralso has a strong reference to the cells, via their collection view.

闭包单元通过使用self引用了SearchResultsViewController,造成强引用。闭包 “捕捉” 到了self。在实际使用中,Swift语言强迫你在闭包中明确的使用关键字self。SearchResultsViewController通过它们的collection view也对cell产生了强引用。(这是对上一段英文的翻译,抱歉有部分内容不知道该怎么翻译比较好,所以贴出英文原文。)

要破除强循环引用,你可以定义一个“捕获列表 ”作为闭包定义的一部分。捕获列表可以用来声明那些被闭包捕获的对象实例,这些实例可以被声明为weak或者unowned类型。

Weak:当对某个对象的引用在未来的某个时间点有可能变为nil的时候应该使用weak关键字。如果被引用的对象被释放,引用变为nil。就其本身而论,weak是可选的。

Unowned:当闭包和其引用的对象相互之间总是有着相同的生命周期时,并且也是同时被释放时,使用Unowned关键字。一个unowned类型的引用绝对不会变成nil。

要修强循环引用的问题,点击Open in Xcode按钮,打开SearchResultsViewController.swift文件,为闭包heartToggleHandler添加一个捕获列表。

cell.heartToggleHandler = { [weak self] isStarred in

if let strongSelf = self {

strongSelf.collectionView.reloadItemsAtIndexPaths([ indexPath ])

}

}

将self声明为weak类型,意味着即使collection view cell有指向SearchResultsViewController对象的引用,SearchResultsViewController对象也可被释放,因为它们之间是弱引用的关系。SearchResultsViewController的释放会接着引发对collection view的释放,接着是对collection view cell的释放。

从Xcode内部,使用快捷键⌘+I再一次编译并在Instruments中运行app

在Instruments中使用Allocations选项,按照你先前的做法(记住过滤掉一些内容)。执行一个搜索,点击进入搜索结果界面,然后再返回。你应该看到当从导航返回时,SearchResultsViewController和其cell都被释放了。

终于,强循环引用的问题也被解决了。好了,可以上传代码了。

从这里出发,要到哪里去?

这里是使用Instruments改进优化后的代码

现在你已经学会上述知识了,去使用Instruments调试你的代码,看看会发生什么有趣的事情。当然,努力让使用Instruments优化代码成为你工作流程的一部分。

你应该经常使用Instruments检查你的代码,在发布app之前尽量清除掉内存管理和性能上的问题。

现在,去着手制作一些高效而了不起的app吧!


注:

1、在原文中好多地方,作者有的地方method,有的地方用function,其实表达的是同一个意思,不管是理解成“方法”也好、“函数”也罢,都不影响对整体意思的把握。

2、翻译时,文中的某些英文单词在不影响阅读和理解的情况下并没有翻译——毕竟好多东西在业界没有统一的叫法——在意思相同的情况下,每个人按照自己的理解,可能更便于阅读理解。

3、原作者是个很有趣的人,所以文中口语化比较多,语法也不是很严谨,所以翻译力求顺畅的传达意思,而不是逐字逐句的翻译——所谓的意译。

4、建议有兴趣的读者去看看英文原版,毕竟翻译过来的都是二手信息,难免会有不足之处。

要阅读英文原文,请点击:原文链接

5、翻译讲究信达雅,谈不上雅,但希望自己能做到信。水平有限,如有纰漏,还请各位大神多多指教,谢谢。

6、原创翻译,尊重他人的劳动成果,若要转载,请注明本文链接,谢谢。


后记:翻译能让自己的英语水平快速的突飞猛进,同时也可以锻炼自己的意志——尤其是比较长的文章,很折磨人的——光打字都够你受得了,哈哈。也能让你学习新的东西,同时反复的核对内容的过程,能让知识点记得更牢。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,165评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,720评论 1 298
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,849评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,245评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,596评论 3 288
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,747评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,977评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,708评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,448评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,657评论 2 249
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,141评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,493评论 3 258
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,153评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,108评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,890评论 0 198
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,799评论 2 277
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,685评论 2 272

推荐阅读更多精彩内容