INSTRUMENTS调试工具的使用(七十六) —— 解决内存和性能问题简单示例(一)

版本记录

版本号 时间
V1.0 2019.10.11 星期五

前言

我们在做app的时候,不是做完功能就结束了,很多时候是需要进行检查和优化的,而xcode自带了一个很好的检查工具,可以检测内存泄漏。还可以查看哪一个方法比较耗时。还可以检测离屏渲染等等,随后的几篇我们就说一下这个工具的使用。感兴趣的可以看这几篇。
1.INSTRUMENTS调试工具的使用(一)
2.INSTRUMENTS调试工具的使用(二)
3.INSTRUMENTS调试工具的使用(三)
4.INSTRUMENTS调试工具的使用(四)
5.INSTRUMENTS调试工具的使用(五)
6.INSTRUMENTS调试工具的使用(六)
7.INSTRUMENTS调试工具的使用(七)
8.INSTRUMENTS调试工具的使用(八)
9.INSTRUMENTS调试工具的使用(九)
10. INSTRUMENTS调试工具的使用(十)
11. INSTRUMENTS调试工具的使用(十一) —— 简介(一)
12. INSTRUMENTS调试工具的使用(十二) —— 通常任务之启动Instruments(一)
13. INSTRUMENTS调试工具的使用(十三) —— 通常任务之简单了解Instruments(二)
14. INSTRUMENTS调试工具的使用(十四) —— 通常任务之创建、保存和打开跟踪文档(三)
15. INSTRUMENTS调试工具的使用(十五) —— 通常任务之指定目标应用和设备(四)
16. INSTRUMENTS调试工具的使用(十六) —— 通常任务之访问和使用个别仪器(五)
17. INSTRUMENTS调试工具的使用(十七) —— 通常任务之记录、暂停和停止跟踪(六)
18. INSTRUMENTS调试工具的使用(十八) —— 导航收集的数据之关于数据分析(一)
19. INSTRUMENTS调试工具的使用(十九) —— 导航收集的数据之导航时间轴窗格(二)
20. INSTRUMENTS调试工具的使用(二十) —— 导航收集的数据之导航详细面板(三)
21. INSTRUMENTS调试工具的使用(二十一) —— 导航收集的数据之将数据映射到源代码(四)
22. INSTRUMENTS调试工具的使用(二十二) —— 导航收集的数据之查看您应用的源代码(五)
23. INSTRUMENTS调试工具的使用(二十三) —— 分析你App的性能之测量CPU使用情况(一)
24. INSTRUMENTS调试工具的使用(二十四) —— 分析你App的性能之测量图形性能(二)
25. INSTRUMENTS调试工具的使用(二十五) —— 分析你App的性能之监视网络和文件I / O(三)
26. INSTRUMENTS调试工具的使用(二十六) —— 分析你App的内存使用之关于内存分析(一)
27. INSTRUMENTS调试工具的使用(二十七) —— 分析你App的内存使用之检测内存使用(二)
28. INSTRUMENTS调试工具的使用(二十八) —— 分析你App的内存使用之找到废弃的内存(三)
29. INSTRUMENTS调试工具的使用(二十九) —— 分析你App的内存使用之找到内存泄露(四)
30. INSTRUMENTS调试工具的使用(三十) —— 分析你App的内存使用之找到僵尸对象(五)
31. INSTRUMENTS调试工具的使用(三十一) —— 分析你App的能源之测量能源影响(一)
32. INSTRUMENTS调试工具的使用(三十二) —— 高级任务之导出和导入跟踪数据(一)
33. INSTRUMENTS调试工具的使用(三十三) —— 高级任务之创建自定义Instruments(二)
34. INSTRUMENTS调试工具的使用(三十四) —— 分析模板和工具之分析模板(一)
35. INSTRUMENTS调试工具的使用(三十五) —— 分析模板和工具之Activity Monitor工具(二)
36. INSTRUMENTS调试工具的使用(三十六) —— 分析模板和工具之Allocations工具(三)
37. INSTRUMENTS调试工具的使用(三十七) —— 分析模板和工具之蓝牙开关日志工具(四)
38. INSTRUMENTS调试工具的使用(三十八) —— 分析模板和工具之Carbon Events工具(五)
39. INSTRUMENTS调试工具的使用(三十九) —— 分析模板和工具之Cocoa Events工具(六)
40. INSTRUMENTS调试工具的使用(四十) —— 分析模板和工具之Connections工具(七)
41. INSTRUMENTS调试工具的使用(四十一) —— 分析模板和工具之Core Animation工具(八)
42. INSTRUMENTS调试工具的使用(四十二) —— 分析模板和工具之Core Data Cache Misses工具(九)
43. INSTRUMENTS调试工具的使用(四十三) —— 分析模板和工具之Core Data Faults工具(十)
44. INSTRUMENTS调试工具的使用(四十四) —— 分析模板和工具之Core Data Fetches工具(十一)
45. INSTRUMENTS调试工具的使用(四十五) —— 分析模板和工具之Core Data Saves工具(十二)
46. INSTRUMENTS调试工具的使用(四十六) —— 分析模板和工具之Counters工具(十三)
47. INSTRUMENTS调试工具的使用(四十七) —— 分析模板和工具之CPU Activity Log工具(十四)
48. INSTRUMENTS调试工具的使用(四十八) —— 分析模板和工具之Directory I/O工具(十五)
49. INSTRUMENTS调试工具的使用(四十九) —— 分析模板和工具之Dispatch工具(十六)
50. INSTRUMENTS调试工具的使用(五十) —— 分析模板和工具之Display Brightness Log工具(十七)
51. INSTRUMENTS调试工具的使用(五十一) —— 分析模板和工具之Displayed Surfaces工具(十八)
52. INSTRUMENTS调试工具的使用(五十二) —— 分析模板和工具之Energy Usage Log工具(十九)
53. INSTRUMENTS调试工具的使用(五十三) —— 分析模板和工具之GPS On/Off Log工具(二十)
54. INSTRUMENTS调试工具的使用(五十四) —— 分析模板和工具之GPU Driver工具(二十一)
55. INSTRUMENTS调试工具的使用(五十五) —— 分析模板和工具之GPU Hardware工具(二十二)
56. INSTRUMENTS调试工具的使用(五十六) —— 分析模板和工具之Graphics Driver Activity工具(二十三)
57. INSTRUMENTS调试工具的使用(五十七) —— 分析模板和工具之I/O Activity工具(二十四)
58. INSTRUMENTS调试工具的使用(五十八) —— 分析模板和工具之Leaks工具(二十五)
59. INSTRUMENTS调试工具的使用(五十九) —— 分析模板和工具之Location Energy Model工具(二十六)
60. INSTRUMENTS调试工具的使用(六十) —— 分析模板和工具之Metal Application工具(二十七)
61. INSTRUMENTS调试工具的使用(六十一) —— 分析模板和工具之Network Activity Log工具(二十八)
62. INSTRUMENTS调试工具的使用(六十二) —— 分析模板和工具之OpenGL ES Analyzer工具(二十九)
63. INSTRUMENTS调试工具的使用(六十三) —— 分析模板和工具之Sampler工具(三十)
64. INSTRUMENTS调试工具的使用(六十四) —— 分析模板和工具之Scheduling工具(三十一)
65. INSTRUMENTS调试工具的使用(六十五) —— 分析模板和工具之Sleep/Wake Log工具(三十二)
66. INSTRUMENTS调试工具的使用(六十六) —— 分析模板和工具之Spin Monitor工具(三十三)
67. INSTRUMENTS调试工具的使用(六十七) —— 分析模板和工具之System Calls工具(三十四)
68. INSTRUMENTS调试工具的使用(六十八) —— 分析模板和工具之Time Profiler工具(三十五)
69. INSTRUMENTS调试工具的使用(六十九) —— 分析模板和工具之VM Operations工具(三十六)
70. INSTRUMENTS调试工具的使用(七十) —— 分析模板和工具之VM Tracker工具(三十七)
71. INSTRUMENTS调试工具的使用(七十一) —— 分析模板和工具之Wi-Fi On/Off Log工具(三十八)
72. INSTRUMENTS调试工具的使用(七十二) —— 分析模板和工具之废弃的模板和工具(三十九)
73. INSTRUMENTS调试工具的使用(七十三) —— 偏好设置和菜单之偏好设置(一)
74. INSTRUMENTS调试工具的使用(七十四) —— 偏好设置和菜单之菜单和键盘快捷键(二)
75. INSTRUMENTS调试工具的使用(七十五) —— 相关资源(一)

开始

题外话:徐S希望你未来幸福,我想你也会自己放在自己心里,十一加上请假十几天瘦了七八斤,希望未来可期~~

首先看主要内容

主要内容:了解如何使用Instruments来捕获和修复应用中的内存问题和性能bug,以使其更快,响应速度更快。翻译来自地址

然后看下写作环境

Swift 5, iOS 13, Xcode 11

除了通过添加功能来改进其应用程序之外,所有优秀的应用程序开发人员还应该做一件事:Instrument代码!

Instrument教程将向您展示如何使用Xcode随附的Instrument工具的最重要功能。它使您可以检查代码中是否存在性能问题,内存问题,引用循环和其他问题。

可以了,准备好进入迷人的Instrument世界!

在本Instrument教程中,您不会从头开始创建应用程序。相反,打开下载好的完整的示例项目。您的任务就是浏览应用程序,并以Instrument为指导进行改进,就像您自己开发应用程序一样!

在Xcode中打开启动项目。

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

例如,如果URL为:

http://api.flickr.com/services/rest/?method=flickr.photos.search
&api_key=ff417a50b95180cb0c7e3b68a8749fba
&format=rest&api_sig=f24f4e98063a9b8ecc8b522b238d5e2f

那么API key就是ff417a50b95180cb0c7e3b68a8749fba

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

请注意,API密钥每天都会更改,因此有时您需要重新生成一个新密钥。 只要密钥无效,该应用程序都会提醒您。

Build并运行,执行搜索,单击结果,您将看到类似以下的内容:

玩该应用程序并查看其基本功能。 您可能会认为,一旦UI看起来很棒,该应用就可以提交商店了。 但是,您将看到使用Instruments可以为您的应用添加的价值。

本教程的其余部分将向您展示如何查找和修复应用程序中仍然存在的问题。 您将看到Instruments如何使调试问题大为简化!


Time for Profiling

您要查看的第一个instrument是时间分析器(Time Profiler)。 每隔一定的时间间隔,Instruments将暂停程序的执行,并在每个正在运行的线程上进行堆栈跟踪。 您可以将其视为单击Xcode调试器中的暂停按钮。

这是Time Profiler的预览:

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

例如,如果您以1毫秒的间隔进行100个采样,并且在10个采样中的某个特殊方法出现在堆栈的顶部,则可以推断出该方法花费了该应用程序大约10%的总执行时间(10毫秒)。这是一个粗略的近似值,但是就是这个原理并起作用!

注意:通常,您应该始终在实际设备上而不是模拟器上配置应用程序。 iOS模拟器具有Mac的全部功能,而设备将具有移动硬件的所有限制。您的应用似乎可以在模拟器中正常运行,但是一旦在真实设备上运行,您可能会发现性能问题。

因此,事不宜迟,该花些时间进行检测了!

1. Instrumenting

在Xcode的菜单栏中,选择Product ▸ Profile或按Command-I。 这将构建应用程序并启动Instruments。 您会看到一个选择窗口,如下所示:

这些是Instruments随附的所有不同模板。

选择Time Profiler仪器,然后单击Choose。这将打开一个新的Instruments文档。单击左上角的红色录制按钮开始录制并启动该应用程序。 macOS可能会要求您输入密码以授权Instruments分析其他进程。别担心,可以在这里安全提供!

Instruments窗口中,您可以看到时间在向上计数,并且在屏幕中心的图形上方有一个小箭头从左向右移动。这表明该应用程序正在运行。

现在,开始使用该应用程序。搜索一些图像,然后向下钻取一个或多个搜索结果。您可能已经注意到,进入搜索结果的过程非常缓慢,并且滚动浏览搜索结果列表也非常令人讨厌。这是一个非常笨拙的应用!

好吧,你很幸运。您即将着手修复它!但是,您首先需要快速了解一下Instruments中的内容。

首先,确保工具栏右侧的视图选择器选择了两个选项,如下所示:

这样可以确保所有面板均打开。 现在,研究下面的屏幕截图以及下面每个部分的说明:

  • 1) Recording controls:红色的record按钮停止并启动当前正在测试的应用。暂停按钮可暂停应用程序的当前执行。
  • 2) Run timer:计时器计算配置文件应用已运行多长时间以及已运行多少次。单击停止按钮,然后重新启动应用程序。您会看到现在显示Run 2 of 2
  • 3) Instrument track:对于您选择的Time Profiler模板,只有一个instrument,因此只有一个轨道。您将在本教程的后面部分详细了解图形的详细信息。
  • 4) Detail panel:显示有关您正在使用的特定instrument的主要信息。在这种情况下,它显示了“最热”的方法,即使用最多CPU时间的方法。在详细信息面板的顶部,单击Profile,然后选择Samples。在这里您可以查看每个采样。点击一些采样;您会看到捕获的堆栈跟踪显示在右侧的扩展详细信息(Extended Detail)检查器中。完成后,切换回Profile
  • 5) Inspectors panel:有两个检查器 - Extended DetailRun Info - 您将稍后了解更多信息。

Drilling Deep

执行图像搜索,然后深入搜索结果。 我个人喜欢搜索“狗”,但请选择您想要的任何东西-您可能是其中喜欢猫的人之一!

现在,上下滚动列表几次,以便在Time Profiler中获得大量数据。 您应该注意到屏幕中间的数字在变化,并且图形在填充。这表明您的应用程序正在使用CPU周期。

为了帮助查明问题,您将设置一些选项。 单击Stop,然后在详细信息面板下方,单击Call Tree。 在出现的弹出窗口中,选择Separate by Thread, Invert Call TreeHide System Libraries。 它看起来像这样:

以下是每个选项对左侧表格中显示的数据的处理方式:

  • Separate by State:此选项按应用程序的生命周期状态对结果进行分组,是一种检查应用程序正在进行的工作量和时间的有用方法。
  • Separate by Thread:分别看每个线程。这使您能够了解哪些线程导致最大的CPU使用量。
  • Invert Call Tree:使用此选项,堆栈跟踪将首先显示最近的帧。
  • Hide System Libraries:选择此选项时,仅显示您自己的应用程序中的符号。选择此选项通常很有用,因为通常您只关心CPU在自己的代码中花费的时间,而对于系统库正在使用多少CPU却不那么关心!
  • Flatten Recursion:此选项显示递归函数,这些函数调用自己,每个堆栈跟踪中只有一个条目,而不是多次。
  • Top Functions:启用此功能可使Instruments将在函数中花费的总时间视为直接在该函数中花费的时间之和,以及在该函数调用的函数中花费的时间。因此,如果函数A调用B,则Instruments将A的时间报告为A所花费的时间加上B中所花费的时间。这非常有用,因为它使您每次下降到调用堆栈时都选择最大的时间数字,将其清零使用最耗​​时的方法。

扫描结果以识别Weight列中哪些行具有最高百分比。请注意,带有Main Thread的行正在消耗大量的CPU周期。通过单击文本左侧的小箭头来展开该行,然后向下看,直到看到您自己的方法之一,并标有“person”符号。尽管某些值可能会略有不同,但是条目的顺序应类似于下表:

好吧,那看起来当然不好。 该应用程序花费大量时间使用将“ tonal”滤镜应用于缩略图的方法。 表格加载和滚动是用户界面中最笨拙的部分,而表格单元不断更新时,就没那么流畅感了。

要了解有关该方法中发生的事情的更多信息,请双击表中的该行。 这样做将显示以下视图:

applyTonalFilter()是扩展中添加到UIImage的方法,并且在应用图像滤镜之后花费大量时间调用创建CGImage输出的方法。

您实际上没有什么可以加快的速度。 创建图像是一个密集的过程,需要花费很长时间。 尝试返回查看应用程序在哪里调用applyTonalFilter()。 单击代码视图顶部的痕迹中的Root以返回上一屏幕:

现在,单击表顶部applyTonalFilter行左侧的小箭头。 这将显示applyTonalFilter的调用者-您可能还需要展开下一行。 在对Swift进行性能分析时,有时在Call Tree中会有重复的行,并以@objc为前缀。 您对第一行带有“person”图标(表示该行属于您的应用目标)感兴趣:

在这种情况下,该行引用结果收集视图的(_:cellForItemAt :)。双击该行以查看项目中的关联代码。

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

1. Offloading the Work

为解决此问题,您将采取两个步骤:首先,使用DispatchQueue.global()async将图像过滤卸载到后台线程中。然后,在生成每个图像后对其进行缓存。入门项目中包含一个小型的简单图像缓存类-易记的名称ImageCache-该类仅将图像存储在内存中并使用给定的键检索它们。

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

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)")
  }
}

该代码的第一部分与之前相同,并从网络上加载Flickr照片的缩略图。 如果照片是您最爱的,则单元格将显示缩略图而无需修改。 但是,如果照片不是您的最爱,则会应用tonal滤镜。

这是您更改内容的地方:

  • 1) 检查图像缓存中是否存在针对该照片的滤镜图像。 如果是,那就太好了; 显示该图像。
  • 2) 如果不是,请分派将tonal滤镜应用到后台队列的调用。 这允许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)
    }
  }
}

这与您处理已滤镜图像的方式非常相似。 如果高速缓存中已经存在图像,则您可以立即与高速缓存的图像一起调用completion闭包。 否则,您可以从Flickr加载图像并将其存储在缓存中。

Command-I再次在Instruments中运行该应用程序。 请注意,这次Xcode不会询问您要使用哪种Instruments。 这是因为您仍然为应用程序打开了一个窗口,并且Instruments假设您想使用相同的选项再次运行。

再进行几次搜索。 用户界面就不那么笨拙不流畅了! 该应用程序现在在后台应用图像滤镜并缓存结果,因此图像仅需滤镜一次。 您会在Call Tree中看到许多dispatch_worker_threads。 这些正在处理应用图像滤镜的繁重工作。

看起来很棒!


Allocations, Allocations and Allocations

那么,您接下来要查找什么bug?

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

  • 1) True memory leaks - 真实内存泄漏:当对象不再被任何东西引用但仍被分配时发生。这意味着该内存将永远无法重复使用。

即使借助Swift和ARC帮助管理内存,最常见的内存泄漏类型仍然是retain cyclestrong reference cycle。当两个对象相互之间拥有强引用时,就会发生这种情况,从而使每个对象都不会释放另一个对象。结果,他们的内存永远不会释放。

  • 2) Unbounded memory growth - 无限的内存增长:在连续分配内存且从未被释放过的情况下发生。如果持续发生这种情况,则会耗尽内存。在iOS上,这意味着系统将终止您的应用。

现在,您将探索Allocations工具。该工具为您提供有关应用程序创建的所有对象以及支持它们的内存的详细信息。它还显示每个对象的retain counts

1. Instrumenting Allocations

要以新的instruments配置文件重新开始,请退出instruments应用。不必担心保存此特定运行。现在,在Xcode中按Command-I,从列表中选择Allocations,然后单击Choose

片刻之后,您会看到Allocations工具。 它对您应该很熟悉,因为它看起来很像Time Profiler

单击左上角的Record按钮以运行该应用程序。 这次您会注意到两条轨道。 在本教程中,您只需要关注All Heap and Anonymous VM

在应用程序上运行Allocations工具后,可以在应用程序中进行五次不同的搜索,但仍不深入查询结果。 确保搜索得到一些结果。 现在,让应用程序等待几秒钟。

您应该已经注意到All Heap and Anonymous VM轨道中的图形一直在上升。 这告诉您您的应用正在分配内存。 正是这一功能将引导您找到无限的内存增长。


Generation Analysis

您要执行的是世代分析(generation analysis)。 为此,请单击详细信息面板底部的Mark Generation的按钮:

单击它,您将看到一条红旗出现在轨道中,如下所示:

世代分析的目的是多次执行一个动作,并查看内存是否以无限的方式增长。 打开搜索结果,等待几秒钟以加载图像,然后返回主页。 再次标记一代。 重复执行此操作以进行不同的搜索。

检查了几次搜索后,Instruments将如下所示:

此时,您应该变得疑惑了。请注意,您每次进行深入的搜索时,蓝色图形都会随着上升。好吧,那当然不是很好。但是,等等,内存警告呢?你知道这些,对吧?

1. Simulating a Memory Warning

内存警告是iOS告知应用程序内存出现问题的方式,您需要清除一些内存。

这种增长可能不仅取决于您的应用。这可能是UIKit保留内存的原因。让系统框架和您的应用有机会先清除它们的内存。

通过选择Instruments菜单栏中的Instrument ▸ Simulate Memory Warning或从模拟器菜单栏中选择Hardware ▸ Simulate Memory Warning来模拟内存警告。您会注意到,内存使用量有所下降一点点,甚至根本没有下降。当然不会回到应该的位置。因此,某处仍然存在无限的内存增长。

您在检查搜索的每个迭代之后都标记了一个世代,这样您就可以看到每个世代之间分配了什么内存。在详细信息面板中查看,您会看到很多代。

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

查看Growth列,您肯定会发现某处确实有增长。打开其中的几代,您将看到以下内容:

哇,好多对象! 从哪里开始?

简单。 单击Growth标题以按大小排序。 确保最占内存的对象在顶部。 在每一代的顶部附近,您都会注意到一列标有VM:CoreImage的行,听起来确实很熟悉! 单击VM:CoreImage左侧的箭头以显示与此项目关联的内存地址。 选择第一个内存地址以在右侧面板上的Extended Detail检查器中显示关联的堆栈跟踪:

此堆栈跟踪显示了创建此特定对象的时间。 灰色的堆栈跟踪部分位于系统库中。 黑色部分在您的应用代码中。 嗯,看起来有些熟悉:一些黑色条目显示了您的老朋友collectionView(_:cellForItemAt :)。 双击其中任何一项; 仪器将在其上下文中显示代码。

看一下该方法。 它在第79行调用set(_:forKey :)。请记住,此方法会缓存图片,以便应用中再次使用该图片。 听起来确实是个问题!

再次,单击Open in Xcode以跳回Xcode。 打开Cache.swift并查看set(_:forKey :)的实现:

func set(_ image: UIImage, forKey key: String) {
  images[key] = image
}

这会将图像添加到字典,并在Flickr照片的照片ID作为键。 但是,您会发现该图片从未从该词典中清除!

那就是无限内存增长的来源。 一切都按预期进行,但是该应用程序永远不会从缓存中删除内容-它只会添加内容!

要解决此问题,您需要做的就是让ImageCache侦听UIApplication触发的内存警告通知。 当ImageCache收到此消息时,它必须清除其缓存。

要使ImageCache监听通知,请打开Cache.swift并将以下初始化程序添加到该类:

init() {
  NotificationCenter.default.addObserver(
    forName: UIApplication.didReceiveMemoryWarningNotification,
    object: nil,
    queue: .main) { [weak self] notification in
      self?.images.removeAll(keepingCapacity: false)
  }
}

这为UIApplicationDidReceiveMemoryWarningNotification注册了一个观察者,以执行清除图像images的闭包。

该代码所需要做的就是删除缓存中的所有对象。 这样可以确保不再保留任何图像并将它们释放。

要测试此修复,请再次启动仪器,然后重复之前执行的步骤。 最后不要忘了模拟内存警告。

注意:为了确保您使用的是最新代码,请确保您从Xcode启动并触发构建,而不仅仅是点击Instruments中的红色按钮。 您可能还需要先构建并运行,然后再进行性能分析。 如果您只是进行性能分析,有时Xcode似乎不会将模拟器中的应用程序版本更新为最新版本。

这次,世代分析应如下所示:

出现内存警告后,您会注意到内存使用率下降了。总体而言,内存仍在增长,但远未达到以前。

仍然有一定增长的原因实际上是由于系统库,您对此无能为力。看来系统库并未释放它们的所有内存,这可能是设计使然,也可能是bug。您在应用中可以做的就是释放尽可能多的内存,而您已经做到了!

做得好!修补了另一个问题!您尚未解决的第一类泄漏问题。


Strong Reference Cycles

如前所述,当两个对象彼此保持强引用时,就会发生强引用循环,从而防止两个对象都被释放。您可以使用Allocations工具以其他方式检测这些引用。

关闭仪器并返回到Xcode。再次选择Product ▸ Profile,然后选择Allocations模板。

这次,您将不再使用世代分析。 取而代之的是,您将查看在内存中驻留的不同类型的对象的数量。 单击Record按钮开始运行。 您应该已经看到大量对象填充了详细信息面板-太多了! 为了缩小感兴趣对象的范围,请在左下角的字段中输入Instruments作为过滤器。

仪器中需要注意的两列是#Persistent#Transient#Persistent列保留内存中当前存在的每种类型的对象数。#Transient显示了已存在但已释放的对象数。Persistent对象正在耗尽内存;transient对象不是。

1. Finding Persistent Objects

您应该看到有一个ViewController的持久实例。这很有意义,因为这是您当前正在查看的屏幕。该应用的AppDelegate实例也是如此。

回到应用程序!执行搜索并深入研究结果。请注意,Instruments中现在显示了一堆额外的对象:SearchResultsViewControllerImageCache等。ViewController实例仍是持久性的,因为其导航控制器需要它。

现在点击应用程序中的后退按钮。这会将SearchResultsViewController从导航堆栈中弹出,因此应将其释放。但是在Allocations摘要中,它仍显示# Persistent计数为1!为什么还在那里?

尝试执行另外两个搜索,然后在每个搜索之后点击“后退”按钮。现在有三个SearchResultsViewControllers?!这些视图控制器在内存中保持着的事实意味着某些东西一直在强引用它们。看来您有很强的参考周期!

在这种情况下,您的主要线索不仅是持久化SearchResultsViewController的持久化,而是所有SearchResultsCollectionViewCells。引用循环可能介于这两类之间。

幸运的是,Xcode 8中引入的Visual Memory Debugger是一个简洁的工具,可以帮助您进一步诊断内存泄漏和引用循环。Visual Memory Debugger不是Xcode仪器套件的一部分,但它是一个非常有用的工具,值得在本教程中讲述。来自Allocations工具和Visual Memory Debugger的交叉使用是一项强大的技术,可以使您的调试工作流更加有效。


Getting Visual

退出Instruments

在启动Visual Memory Debugger之前,请像下面这样在Xcode scheme编辑器中启用Malloc Stack日志记录:Option-单击窗口顶部(stop按钮旁边)的InstrumentsTutorial scheme。在出现的弹出窗口中,单击Run部分,然后切换到Diagnostics选项卡。选中显示Malloc Stack的框,然后选择Live Allocations Only选项,然后单击Close

直接从Xcode启动应用程序。 与以前一样,至少执行三个搜索以累积一些数据。

现在,像这样激活Visual Memory Debugger

  • 1) 切换到Debug导航器。
  • 2) 单击此图标,然后从弹出窗口中选择View Memory Graph Hierarchy
  • 3) 单击SearchResultsCollectionViewCell的条目。
  • 4) 您可以单击图形上的任何对象,以在检查器窗格中查看详细信息。
  • 5) 您可以在此区域中查看详细信息。在此处切换到Memory inspector

Visual Memory Debugger会暂停您的应用程序,并以直观的方式显示内存中的对象及其之间的引用。

如上面的屏幕快照中突出显示的,Visual Memory Debugger显示以下信息:

  • Heap contents 堆内容(Debug窗格):这将显示您暂停应用程序时在内存中分配的所有类型和实例的列表。单击一种类型将展开该行,以向您显示该类型在内存中的单独实例。
  • Memory graph 内存图(主窗口):主窗口以可视方式表示内存中的对象。对象之间的箭头表示它们之间的引用(强关系和弱关系)。
  • Memory inspector内存检查器(Utilities窗格):包括类名称和层次结构以及引用是强引用还是弱引用等详细信息。

请注意,Debug导航器中的某些行旁边如何带有括号。括号中的数字表示内存中有多少个特定类型的实例。在上面的屏幕截图中,您可以看到经过几次搜索后,Visual Memory Debugger会确认您在Allocations工具中看到的结果。换句话说,从20到(如果您滚动到搜索结果的末尾)的任何位置,每个SearchResultsViewController实例的60SearchResultsCollectionViewCell实例都将保留在内存中。

使用该行左侧的箭头展开该类型,并显示内存中的每个SearchResultsViewController实例。单击单个实例将在主窗口中显示该实例及其任何引用。

注意箭头指向SearchResultsViewController实例。 似乎有一些Swift closure contexts实例引用了相同的视图控制器实例。 看起来有点怀疑,不是吗? 细看。 选择箭头之一,以在Utilities窗格中显示有关这些闭包实例之一与SearchResultsViewController之间的引用的更多信息。

Memory Inspector中,您可以看到Swift closure contextSearchResultsViewController之间的引用很强。 如果您在SearchResultsCollectionViewCellSwift closure context之间选择引用,您还将看到它也标记为strong。 您还可以看到闭包的名称是heartToggleHandler。哈! SearchResultsCollectionViewCell声明了这一点!

在主窗口中选择SearchResultsCollectionViewCell的实例,以在检查器窗格上显示更多信息。

在回溯中,您可以看到单元实例已在collectionView(_:cellForItemAt :)中初始化。 当您将鼠标悬停在回溯中的这一行上时,将出现一个小箭头。 点击箭头将带您进入Xcode的代码编辑器中的此方法。

collectionView(_:cellForItemAt :)中,找到设置每个单元格的heartToggleHandler属性的位置。 您将看到以下代码行:

cell.heartToggleHandler = { isStarred in
  self.collectionView.reloadItems(at: [indexPath])
}

当用户在collection view cell中单击“心形”按钮时,将执行此闭包。这是强引用所在,但是除非您之前遇到过,否则很难找到。但是多亏了Visual Memory Debugger,您才能够一直追踪到这段代码!

闭包单元格使用self引用SearchResultsViewController,这会创建一个强引用。闭包捕捉self。 Swift实际上会强迫您在闭包中显式使用self一词,而在引用当前对象的方法和属性时,通常可以将其删除。这可以帮助您更加了解要捕获的事实。 SearchResultsViewController通过其collection view还对cell有很强的引用。

1. Break That Cycle

为了打破强引用,您可以将capture list定义为闭包定义的一部分。capture list可用于将闭包捕获的实例声明为weakunowned

  • Weak:在将来捕获的引用可能为nil时使用。如果它所引用的对象被释放,则该引用将变为nil。因此,它们是可选类型。
  • Unowned:当闭包及其引用的对象始终具有相同的生命周期并在同一时间被释放时,使用此属性。无主引用永远不会为nil

要解决这个强引用,请像这样将捕获列表添加到heartToggleHandler

cell.heartToggleHandler = { [weak self] isStarred in
  self?.collectionView.reloadItems(at: [indexPath])
}

self声明为weak表示意味着,即使集合视图单元格拥有对它的引用,也可以将SearchResultsViewController释放,因为它们现在只是弱引用。 销毁SearchResultsViewController将会销毁其collection view,进而销毁cell

在Xcode中,再次按Command-IInstruments中构建并运行该应用程序。

像以前一样,使用Allocations工具在Instruments中再次查看该应用。 切记将结果向下过滤,以仅显示入门项目中的类。 执行搜索,导航至结果,然后再次返回。 向后导航时,您应该看到SearchResultsViewController及其cell已被释放。 它们显示瞬时transient实例,而不是persistent实例了。

循环打破了!

后记

本篇主要讲述了解决内存和性能问题简单示例,感兴趣的给个赞或者关注~~~

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

推荐阅读更多精彩内容