2019独角兽企业重金招聘Python工程师标准>>>
本文由Mr_cyz(博客)翻译自raywenderlich,欢迎参与我们的翻译活动。
原文:Instruments Tutorial with Swift: Getting Started
更新记录:该教程由 James Frost 更新至iOS8,swift语言。 原版本 由我们队伍中的一员Matt Galloway编写。
无论你写过许多iOS应用,还是刚刚开始你的第一个应用,毫无疑问,你都会想出一些新点子,或者想去弄明白你该怎么做,来让你的app变得更好。
除去添加新特性来优化你的应用,有一件事是所有好的开发者都回去做的,那就是诊断他们的代码。
该教程将向你展示怎么样去使用Xcode提供的工具"Instrument"中最重要的一些功能。帮助你检查自己代码中的性能问题、内存管理问题、循环引用问题以及其他种种。
在本篇教程中,你将学到:
-
怎样使用Time Profiler工具来定位你的代码中的"高消耗点(hot-spot)",从而让你的代码更加有效率。
-
怎样使用Allocations工具来检测和改正代码中的内存管理问题,例如循环强引用。
注意:本教程假定你已经上手了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=",接下来的参数同样加到&后面。
例如,如果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的界面。
首先,确保右手边工具栏上的视图选择器的每一个选项都被选中,如下:
这样就确保所有的面板都被打开。现在看一下下面的截图和每一部分的说明。
1、这里控制记录过程,点击红色的"记录"按钮可以停止或开始当前正在分析的app(在记录和停止按钮之间切换),暂停键,如你所想,暂停当前正在运行的app。
2、这里是执行计时器(run timer),计时器记录着正在分析的app执行了多长时间、执行了多少次。如果你使用记录控制按钮来停止你的app,然后重启,这将创建一个新的运行记录,同时会显示"Run 2 of 2"。
3、这里被称作路径(track),就你选择的Time Profiler工具而言,因为只有一个工具,所以这里只有一条路径,关于这里显示的图标的详情,一会你就会在接下来的教程中了解更多。
4、这里是详情面板,展示的是你正在使用的工具的主要信息。就现在而言,这里展示的是最"笨重(hottest)"的方法--换句话说,占用CPU时间最长的方法。点击上方的bar会看到Call Tree(左手边的那个)并选中Sample List,然后你会看到数据的不同视图。视图展示了每一个示例。点击其中几个,你会在Extended Detail inspector中看到被捕获的堆栈跟踪。
5、这里是检查器(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呢?
有些事被隐藏在工程中,你可能不知道它的存在。你可能听说过内存泄露,但不知道其实有两种泄露:
1、"真正的内存泄露(True memory leaks)"是指一个对象不再被引用但却没有被释放--这说明内存永远不能被复用,即使有swift和ARC帮助管理内存,最常见的内存泄露问题是保留环,或称为强引用环。当两个对象互相持有对方的强引用时,每个对象保证另一个不会被释放,这样它们的内存将永远不能被释放!
2、"无限内存增长(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都被释放了。它们现在是临时对象,而不是持久对象。循环打破!再一次跨越!:]
何去何从?
从这里下载工程的最终优化版本,全都多亏了Instruments。
既然你已经掌握了这些知识,去分析自己的代码然后看一下有什么有趣的事情发生吧。同时,试着将分析应用作为你平常开发工作流中的一个环节。
你应该经常通过Instruments来运行你的代码,并在发布之前对你的app进行一次彻底的清理,以确保你已经尽可能多地找到了内存管理问题和性能问题。
现在去做一些优秀并且高效的app吧!:]