Instruments的使用

Instruments Tutorial: Getting Started

学习如何使用工具来捕获和修复应用程序中的内存问题和性能bug,使它们更快、响应更快。

除了通过添加特性来改进他们的应用程序之外,所有优秀的应用程序开发人员都应该做一件事:检验他们的代码!

本教程将向您展示如何使用随Xcode附带的Instruments工具的最重要特性。它允许您检查代码的性能问题、内存问题、引用周期和其他问题。

一: 准备开始

在这个Instruments教程中,您不会从头开始创建一个应用程序。相反,下载材料为您提供了一个完整的示例项目。您的任务是通过应用程序和改进它使用工具作为您的指导,就像您将为自己的应用程序做!

使用本教程顶部或底部的“下载材料”按钮下载项目材料。在Xcode中打开启动项目。

这个示例应用程序使用Flickr API搜索图像。要使用API,您需要一个API密钥。对于演示项目,您可以在Flickr的网站上生成一个示例密钥。只需在Flickr API Explorer中执行任何搜索,并从底部的URL复制API键。它遵循“&api_key=”一直到下一个“&”。

Then the API key is: ff417a50b95180cb0c7e3b68a8749fba.

打开FlickrAPI.swift并将现有的API键值替换为您的新值。

请注意,API密钥大约每天都在变化,因此偶尔需要重新生成一个新密钥。当密钥无效时,应用程序会提醒你。

建立和运行,执行搜索,点击结果,你会看到像这样:

[图片上传失败...(image-56407-1592311788965)]

玩一下这个应用程序,看看它的基本功能。您可能会认为,一旦UI看起来很好,应用程序就准备好提交到商店了。然而,您将看到使用工具可以为您的应用程序增加的价值。

本教程的其余部分将向您展示如何查找和修复应用程序中仍然存在的问题。您将看到工具如何使调试问题变得更加容易!

二: Time for Profiling

[图片上传失败...(image-b3fc72-1592311788965)]

您将看到的第一个工具是时间分析器。每隔一定的时间,仪器就会停止程序的执行,并对每个正在运行的线程进行堆栈跟踪。可以把它看作是在Xcode的调试器中单击暂停按钮。

以下是Time Profiler的截图:

[图片上传失败...(image-830618-1592311788965)]

这个屏幕显示了调用树。调用树显示了在应用程序中执行各种方法所花费的时间。每一行都是程序执行路径所遵循的不同方法。工具通过计算分析器在每种方法中停止的次数来估计在每种方法中花费的时间。

例如,如果以1毫秒的间隔抽取100个样本,并且在10个样本中有一个特定的方法出现在堆栈的顶部,那么可以推断该应用程序在该方法中花费了大约总执行时间的10%(10毫秒)。这是一个相当粗略的近似,但它是有效的!

注意:通常,你应该总是在实际设备上配置你的应用程序,而不是在模拟器上。iOS模拟器拥有你的Mac的所有配置,而设备拥有移动硬件的所有限制。您的应用程序在模拟器中可能运行得很好,但一旦它在实际设备上运行,您可能会发现性能问题。

三: Instrumenting

从Xcode的菜单栏中,选择产品short - term Profile或按Command-I键。这将构建应用程序并启动仪器。你会看到一个选择窗口,看起来像这样:

[图片上传失败...(image-b2bb73-1592311788965)]

这些都是仪器自带的不同模板。

选择Time Profiler,然后单击选择。这会打开一个新的Instruments文档。点击左上方的红色记录按钮,开始记录并启动app。macOS可能会要求您输入密码,授权Instruments分析其他进程。不要害怕,在这里提供是安全的!

在仪器窗口中,您可以看到时间在增加,屏幕中央的图形上方有一个小箭头从左向右移动。这表明应用程序正在运行。

现在,开始使用该应用程序。搜索一些图片,并深入到一个或多个搜索结果。你可能已经注意到进入搜索结果非常缓慢,滚动搜索结果列表也非常烦人。这是一个非常笨重的应用程序!

你很幸运。你就要着手修理它了!然而,你首先要对你在仪器中看到的东西有一个快速的纲要。

首先,确保工具栏右边的视图选择器同时选择了两个选项,像这样:

[图片上传失败...(image-961c8d-1592311788965)]

这确保了所有面板都是打开的。现在,看看下面的截图和下面每个部分的解释:

[图片上传失败...(image-cb594d-1592311788965)]

  • 1: Recording controls:红色的record按钮停止和启动当前测试中的应用程序。pause按钮暂停应用程序的当前执行。
  • 2: Run timer:定时器计数多长时间的profiled应用程序已经运行,它运行了多少次。点击停止按钮,然后重新启动应用程序。你会看到现在的显示显示运行2的2。
  • 3: Instrument track:在你选择的时间分析器模板的情况下,只有一个instrument,所以只有一个痕迹。在本教程的后面部分,您将了解有关该图形的更多细节
  • 4: Detail panel:显示有关您正在使用的特定仪器的主要信息。在本例中,它显示了“最热门”的方法——即使用最多CPU时间的方法。
    在细节面板的顶部,单击Profile并选择Samples。在这里,您可以查看每一个样本。点击一些示例;您将看到捕获的堆栈跟踪显示在扩展细节检查器的右侧。完成后切换回配置文件。
  • 5: Inspectors panel:有两个检查器—扩展细节和运行信息—您将很快了解到更多。

深入探索

执行图像搜索,并查看结果。我个人喜欢搜索“狗”,但选择你想要的-你可能是一个猫宠!

现在,上下滚动列表几次,这样您就可以在时间分析器中获得大量的数据。您应该注意到屏幕中间的数字正在变化,图形正在填充。这告诉你你的应用程序正在使用CPU周期。

直到它像黄油一样滚动之前,任何收藏视图都无法交付!

为了帮助查明问题,您将设置一些选项。单击“停止”,然后在细节面板下面单击Call Tree。在弹出窗口中,选择Separate by ThreadInvert Call TreeHide System Libraries。它会是这样的:

[图片上传失败...(image-b5bd8-1592311788965)]

下面是每个选项对左边表格中显示的数据所做的操作:

  • Separate by State: 这个选项根据应用程序的生命周期状态对结果进行分组,是检查应用程序在什么时候做了多少工作的有用方法。
  • Separate by Thread: 分别处理每个线程。这使您能够了解哪些线程对CPU的使用量最大。
  • Invert Call Tree : 使用此选项,堆栈跟踪首先显示最近的帧。
  • Hide System Libraries: 当你选择这个选项时,只会显示来自你自己的应用程序的符号。选择这个选项通常很有用,因为通常您只关心CPU在您自己的代码中花费的时间——对于系统库使用了多少CPU,您无法做太多事情!
  • Flatten Recursion: 此选项显示调用自己的递归函数,每个堆栈跟踪中只有一个条目,而不是多次。
  • Top Functions: 启用这一功能后,工具会将在一个函数中花费的总时间考虑为该函数中直接使用的时间的总和,以及该函数调用的函数所花费的时间。如果函数调用B,那么工具报告的时间所花费的时间在B +所花费的时间,这才会真正有用,因为它允许您选择最大的时间图每次陷入调用堆栈中,重点关注在你最耗时的方法

扫描结果,以确定权重列中哪些行的百分比最高。请注意,带有主线程的行占用了很大一部分CPU周期。通过单击文本左边的小箭头展开这一行,并向下钻取,直到看到您自己的方法之一,标有“person”符号。虽然有些值可能略有不同,但条目的顺序应类似于下表:

[图片上传失败...(image-582c85-1592311788965)]

嗯,这看起来肯定不太好。这款应用程序花了大量时间在对缩略图应用“色调”滤镜的方法上。这应该不会让您感到吃惊,因为表格加载和滚动是UI中最笨重的部分,而这正是表格单元格不断更新的时候。

要了解该方法的更多内容,请双击表中该方法所在的行。这样做将产生以下观点:

[图片上传失败...(image-54e5ee-1592311788965)]

applyTonalFilter()是在扩展中添加到UIImage的一个方法,它在应用图像过滤器后花费大量时间调用创建CGImage输出的方法。

对于加速这个过程,您可以做的不多。创建图像是一个密集的过程,需要多长时间就花多长时间。尝试后退一步查看应用程序调用applytonalfilter()的位置。在代码视图顶部的breadcrumb路径中单击Root返回到上一个屏幕:

[图片上传失败...(image-aae3a9-1592311788965)]

现在,单击表顶部applyTonalFilter行左边的小箭头。这将显示applyTonalFilter的调用者——您可能还需要展开下一行。在分析Swift时,有时会在调用树中出现以@objc为前缀的重复行。你感兴趣的第一行带有person图标,表示它属于你的应用程序的目标:

[图片上传失败...(image-1b0e89-1592311788965)]

在本例中,此行引用结果collectionView的(_:cellForItemAt:)。双击该行以查看来自项目的关联代码。

现在你可以看到问题是什么了。看一下第70行:应用色调过滤器的方法需要很长时间来执行,并且您直接从collectionView(_:cellForItemAt:)调用它。这将阻塞主线程,因此每次请求过滤后的图像时,将阻塞整个UI。

四: 卸货工作

为了解决这个问题,您将采取两个步骤:首先,使用DispatchQueue.global().async将图像过滤卸载到一个后台线程。然后,在生成后缓存每个图像。在starter项目中包含了一个简单的图像缓存类——它有一个引人注目的名称ImageCache,它只是将图像存储在内存中,并使用给定的键检索它们。

现在,您可以切换到Xcode,手动在Instruments中找到您要查看的源文件。但是,在Instruments中有一个方便的打开Xcode按钮。定位它在面板上面的代码,并点击它:

[图片上传失败...(image-cc1-1592311788965)]

可以Xcode在正确的位置打开。

现在,在collectionView(_:cellForItemAt:)中,用以下代码替换对loadThumbnail(for:completion:)的调用:

ImageCache.shared.loadThumbnail(for: flickrPhoto) { result in
  switch result {
  case .success(let image):
    if cell.flickrPhoto == flickrPhoto {
      if flickrPhoto.isFavourite {
        cell.imageView.image = image
      } else {
        // 1
        if let cachedImage = 
          ImageCache.shared.image(forKey: "\(flickrPhoto.id)-filtered") {
          cell.imageView.image = cachedImage
        } else {
          // 2
          DispatchQueue.global().async {
            if let filteredImage = image.applyTonalFilter() {
              ImageCache.shared.set(filteredImage, 
                                    forKey: "\(flickrPhoto.id)-filtered")
            
              DispatchQueue.main.async {
                cell.imageView.image = filteredImage
              }
            }
          }
        }
      }
    }
  case .failure(let error):
    print("Error: \(error)")
  }
}

这段代码的第一部分与之前一样,从web加载Flickr照片的缩略图。如果照片是收藏,单元格将不加修改地显示缩略图。但是,如果照片不是最喜欢的,它会应用色调滤镜。

这就是你改变的地方:

  • 检查此照片的已过滤图像是否存在于图像缓存中。如果是的那就显示图像。
  • 如果不是,则分派调用将色调过滤器应用于后台队列。这允许UI在过滤器运行时保持响应。筛选完成后,将图像保存在缓存中并更新主队列上的图像视图。

这是过滤后的图像,但仍有原始Flickr缩略图需要解决。打开Cache.swift并找到loadThumbnail(for:completion:)。将其改为:

func loadThumbnail(for photo: FlickrPhoto,
                   completion: @escaping FlickrAPI.FetchImageCompletion) {
  if let image = ImageCache.shared.image(forKey: photo.id) {
    completion(Result.success(image))
  } else {
    FlickrAPI.loadImage(for: photo, withSize: "m") { result in
      if case .success(let image) = result {
        ImageCache.shared.set(image, forKey: photo.id)
      }
      completion(result)
    }
  }
}

这与处理过滤后的图像非常相似。如果缓存中已经存在一个映像,那么可以直接用缓存的映像调用完成闭包。否则,从Flickr加载图像并将其存储在缓存中。

Command-I再次在仪器中运行应用程序。注意,这次Xcode并没有要求您使用哪个工具。这是因为您的应用程序仍然有一个打开的窗口,而且Instruments假设您希望使用相同的选项再次运行。

执行更多的搜索。UI现在不那么笨重了!该应用程序现在在背景中应用图像过滤器并缓存结果,所以图像只需要过滤一次。您将在调用树中看到许多dispatch_worker_threads。这些都是处理重型应用图像过滤器。

看起来太棒了!是时候出货了吗?没有!

五: Allocations, Allocations and Allocations

那么你接下来要追踪什么bug呢?

这个项目中隐藏着一些你可能不知道的东西。您可能听说过内存泄漏。但你可能不知道的是,实际上有两种泄漏:

  • 真正的内存泄漏:
    当对象不再被任何对象引用,但仍被分配时发生。这意味着内存永远不能被重用。
    即使使用Swift和ARC帮助管理内存,最常见的内存泄漏类型还是保留循环或强引用循环。当两个对象保持对彼此的强引用,以便每个对象避免释放另一个对象时,就会发生这种情况。因此,他们的记忆永远不会被释放。
  • 无限内存增长:
    当内存被连续分配并且没有机会被释放时发生。如果继续不进行检查,您将耗尽内存。在iOS上,这意味着系统会终止你的应用。

现在你们要研究分配工具。这个工具提供了你的应用程序创建的所有对象的详细信息,以及支持它们的内存。它还显示您保留了每个对象的计数。

六: Instrumenting Allocations

要重新创建新的instruments配置文件,请退出instruments应用程序。不要担心保存这次运行。现在,在Xcode中按Command-I键,从列表中选择Allocations,然后单击Choose。

[图片上传失败...(image-ddaf9f-1592311788965)]

过一会儿,你会看到分配工具。它看起来应该很熟悉,因为它看起来很像Time Profiler.

[图片上传失败...(image-f40fa2-1592311788965)]

点击左上角的Record按钮来运行应用程序。这次你会注意到两条音轨。出于本教程的目的,您将只关心称为All HeapAnonymous VM的方法。

[图片上传失败...(image-88ee59-1592311788965)]
通过在应用程序上运行分配工具,在应用程序中进行五次不同的搜索,但还没有深入到结果。确保搜索有一些结果。现在,让应用程序稍等几秒钟。

[图片上传失败...(image-3f85bc-1592311788965)]

您应该注意到All HeapAnonymous VM跟踪中的图形一直在上升。这告诉你你的应用程序正在分配内存。正是这个特性将指导您寻找无限的内存增长。

七: Generation Analysis

你要做的是生成分析。为此,单击细节面板底部标记生成按钮:

[图片上传失败...(image-5754df-1592311788965)]

点击它,你会看到一个红旗出现在轨道上,像这样:

[图片上传失败...(image-84dcd4-1592311788965)]

生成分析的目的是多次执行一个操作,看看内存是否以无限制的方式增长。打开搜索结果,等待几秒钟图像加载,然后返回主页。再次为这一代人做标记。对于不同的搜索重复这样做。

在检查了几次搜索后,instruments看起来是这样的:

[图片上传失败...(image-a23acb-1592311788965)]

在这一点上,你应该开始怀疑了。请注意,随着每次深入搜索,蓝色图是如何上升的。这当然不好。但是,等等,内存警告呢?

内存警告是iOS告诉应用程序内存部门的情况越来越紧张,需要清空一些内存的一种方式。

这种增长可能不仅仅是由于你的应用。它可能是在UIKit深处的一些东西抓住了内存。在对系统框架和应用程序进行攻击之前,先给它们一个清除内存的机会。

模拟记忆警告,在仪器菜单栏中选择Instrument ▸ Simulate Memory Warning,或在Hardware ▸ Simulate Memory Warning模拟记忆警告。您会注意到内存使用有所下降,或者根本没有下降。当然不会回到它应有的位置。所以在某些地方仍然存在无限的记忆增长。

您在检查搜索的每次迭代之后标记了代,这样您就可以看到在每代之间分配了哪些内存。查看细节面板,您会看到很多代。

在每一代中,您将看到在标记该代时已分配并仍然驻留的所有对象。后续代将只包含上一代标记后的对象。

看看生长柱,你会发现在某个地方确实有生长。打开其中一个世代,你会看到

[图片上传失败...(image-b3f13f-1592311788965)]

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

推荐阅读更多精彩内容