本文由Mr_cyz(博客)翻译自raywenderlich,欢迎参与我们的翻译活动。
原文:Instruments Tutorial with Swift: Getting Started
更新记录:该教程由 James Frost 更新至iOS8,swift语言。 原版本 由我们队伍中的一员Matt Galloway编写。
无论你写过许多iOS应用,还是刚刚开始你的第一个应用,毫无疑问,你都会想出一些新点子,或者想去弄明白你该怎么做,来让你的app变得更好。
除去添加新特性来优化你的应用,有一件事是所有好的开发者都回去做的,那就是诊断他们的代码。
该教程将向你展示怎么样去使用Xcode提供的工具"Instrument"中最重要的一些功能。帮助你检查自己代码中的性能问题、内存管理问题、循环引用问题以及其他种种。
在本篇教程中,你将学到:
-
怎样使用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做多少改变。
-
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,errorin
ifcell.flickrPhoto==flickrPhoto{
ifflickrPhoto.isFavourite{
cell.imageView.image=image
}else{
ifletcachedImage=ImageCache.sharedCache.imageForKey("\(flickrPhoto.photoID)-filtered"){
cell.imageView.image=cachedImage
}else{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0),{
ifletfilteredImage=image?.applyTonalFilter(){
ImageCache.sharedCache.setImage(filteredImage,forKey:"\(flickrPhoto.photoID)-filtered")
dispatch_async(dispatch_get_main_queue(),{
cell.imageView.image=filteredImage
})
}
})
}
}
}
}
|
funcloadThumbnail(completion:ImageLoadCompletion){
ifletimage=ImageCache.sharedCache.imageForKey(photoID){
completion(image:image,error:nil)
}else{
loadImageFromURL(URL:flickrImageURL(size:"m")){image,errorin
ifletimage=image{
ImageCache.sharedCache.setImage(image,forKey:self.photoID)
}
completion(image:image,error:error)
}
}
}
|
@H_404_318@
funcsetImage(image:UIImage,forKeykey:String){
images[key]=image
}
|
@H_404_318@
init(){
NSNotificationCenter.defaultCenter().addObserverForName(
UIApplicationDidReceiveMemoryWarningNotification,
object:nil,queue:NSOperationQueue.mainQueue()){notificationin
self.images.removeAll(keepCapacity:false)
}
}
deinit{
NSNotificationCenter.defaultCenter().removeObserver(self,
name:UIApplicationDidReceiveMemoryWarningNotification,
object:nil)
}
|
@H_404_318@
cell.heartToggleHandler={[weakself]isStarredin
ifletstrongSelf=self{
strongSelf.collectionView.reloadItemsAtIndexPaths([indexPath])
}
}
|
@H_404_318@