阅读 328

Xcode Instruments调试swift入门教程

无论您是在许多iOS应用程序上工作,还是仍在开始使用第一个应用程序:您无疑会想出新功能,并且想知道您可以做些什么来使您的应用程序更加出色

除了通过添加功能改进您的应用程序之外,所有优秀的应用程序开发人员都应该做的一件事就是......检测他们的代码

本教程将向您展示如何使用Xcode附带的名为Instruments的工具的最重要功能。它允许您检查代码中的性能问题,内存问题,循环引用和其他问题。

在本教程中,您将学习:

  • 如何使用Time Profiler工具确定代码中的热点,以便提高代码的效率,以及
  • 如何使用Allocations工具和Visual Memory Debugger检测和修复代码中强引用周期等内存管理问题。

注意:本教程假设您熟悉Swift和iOS编程。如果您是iOS编程的完全初学者,您可能希望查看本网站上的其他一些教程。本教程使用故事板,因此请确保您熟悉该概念;一个好的起点是本网站上的教程。

搞定?准备好潜入迷人的Instuments世界! :]

开始

对于本教程,您将不会从头开始创建应用程序;相反,已经为您提供了一个示例项目。您的任务是通过应用程序并使用Instruments作为指南进行改进 - 与您优化自己的应用程序非常相似!

下载入门项目然后解压缩并在Xcode中打开它。

此示例应用程序使用FlickrAPI搜索图像。要使用API,您需要一个API密钥。对于演示项目,您可以在Flickr的网站上生成示例密钥。进入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=6593783 efea8e7f6dfc6b70bc03d2afb&format=rest&api_sig=f24f4e98063a9b8ecc8b522b238d5e2f,API key就是:6593783efea8e7f6dfc6b70bc03d2afb。

将其粘贴到FlickrAPI.swift的顶部,替换现有的API密钥。

请注意,此示例API密钥每天都会更改,因此您偶尔必须重新生成新密钥。只要密钥不再有效,应用程序就会提醒您。

构建并运行应用程序,执行搜索,单击结果,您将看到如下内容:

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

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

Time Profiler

您将看到的第一个工具是Time Profiler。在测量的时间间隔内,Instruments将停止程序的执行并在每个运行的线程上执行堆栈跟踪。可以将其视为单击Xcode调试器中的暂停按钮。

以下是Time Profiler的预览:

此屏幕显示Call Tree。CallTree显示在应用程序中的各种方法中执行所花费的时间。每行都是程序执行路径所遵循的不同方法。在每种方法中花费的时间可以根据每种方法中分析器停止的次数来确定。

例如,如果以1毫秒的间隔完成100个样本,并且发现特定方法位于10个样本中的堆栈顶部,那么您可以推断出总执行时间的大约10% - 10毫秒 - 花费了在那种方法中。这是一个相当粗略的近似,但它又不是不能用!

注意:通常,您应始终在实际设备上而不是模拟器上分析您的应用。 iOS模拟器具有Mac背后的所有功能,而设备将具有移动硬件的所有限制。您的应用程序似乎在模拟器中运行得很好,但是一旦它在真实设备上运行,您可能会发现性能问题。 此外,Xcode 9测试版和使用Instruments的模拟器存在一些问题。

所以没有任何进一步的麻烦,是时候去使用Time Profiler了!

从Xcode的菜单栏中,选择Product \ Profile,或按⌘I。这将构建应用程序并启动Instrument。您将看到一个如下所示的选择窗口:

这些都是Instrument附带的不同模板。

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

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

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

嗯,你很幸运,因为你即将开始修复它!但是,您首先要快速了解您在Instruments中所看到的内容。

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

这将确保所有面板都是打开的。现在研究下面的截图以及它下面每个部分的解释:

  1. 这些是录音控件。点击红色的“记录”按钮将停止或者启动当前正在分析的应用程序(它在记录和停止图标之间切换)。暂停按钮完全符合您的预期,并暂停当前应用程序的执行。
  2. 这是运行计时器。计时器计算正在运行的应用程序运行的时间长度以及运行的次数。单击停止按钮,然后重新启动应用程序,您将看到显示屏现在显示Run 2 of 2。
  3. 这被称为轨道。对于您选择的Time Profiler模板,只有一个仪器,因此只有一个轨道。您将在本教程后面的内容中详细了解该图的具体细节。
  4. 这是细节面板。它显示了您正在使用的特定仪器的主要信息。在这种情况下,它显示的是“最热门”的方法 - 也就是那些耗尽了大部分CPU时间的方法。

单击“Profile”一词上此区域顶部的栏,然后选择“Sample”。在这里,您可以查看每个样本。单击几个样本,您将看到捕获的堆栈跟踪显示在右侧的“扩展详细信息”检查器中。完成后切换回Profile。 5. 这是检查面板。有两个检查项:扩展详细信息和运行信息。您很快就会了解有关这些选项的更多信息。

深入

执行图像搜索,并深入查看结果。我个人喜欢搜索“狗”,但选择你想要的任何东西 - 你可能是那些爱猫人士之一! :]

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

没有桌面视图可以运送,直到它像黄油一样滚动!

为了帮助查明问题,您需要设置一些选项。单击“停止”按钮,然后在详细信息面板下单击“Call Tree”按钮。在出现的弹出窗口中,选择“按线程分隔”,“反转调用树”和“隐藏系统库”。它看起来像这样:

以下是每个选项对左侧表格中显示的数据的作用:

  • Seprate by State:此选项按应用程序的生命周期状态对结果进行分组,这是检查应用程序正在执行的工作量和时间的有用方法。
  • Seprate by Thread:每个线程都应该单独考虑。这使您可以了解哪些线程负责最大量的CPU使用。
  • Invert Call Tree:使用此选项,堆栈跟踪将被视为从最远到最近。
  • Hide System Libraries:选择此选项后,仅显示您自己的应用程序中的符号。选择此选项通常很有用,因为通常只关心CPU在您自己的代码中花费时间的位置 - 您无法对系统库使用的CPU量做多少工作!
  • Flatten Recursion:此选项将递归函数(自称为自身的函数)视为每个堆栈跟踪中的一个条目,而不是多个。
  • Top Function:启用此功能会使Instruments将在函数中花费的总时间视为该函数内直接时间的总和,以及该函数调用的函数所花费的时间。因此,如果函数A调用B,则A的时间被报告为在A PLUS中花费的时间在B中花费的时间。这可能非常有用,因为它允许您在每次下降到调用堆栈时选择最大的时间数字,归零在你最耗时的方法。

扫描结果以确定“Weight”列中哪些行具有最高百分比。请注意,具有主线程的行占用了相当大比例的CPU周期。通过单击文本左侧的小箭头展开此行,然后向下钻取,直到您看到自己的方法之一(标有“人物”符号)。虽然某些值可能略有不同,但条目的顺序应与下表类似:

嗯,这当然看起来不太好。绝大部分时间都用在将“色调”滤镜应用于缩略图照片的方法中。这不应该对你造成太大的冲击,因为表格加载和滚动是UI中最笨重的部分,而且当表格单元格不断更新时。

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

那很有意思,不是吗! applyTonalFilter()是一个在扩展中添加到UIImage的方法,并且,在应用图像过滤器之后,花费了大量时间来调用创建CGImage输出的方法。

没有太多可以做的事情来加快速度:创建图像是一个非常密集的过程,并且需要花费很长时间。让我们试着退后一步,看看调用applyTonalFilter()的位置。单击代码视图顶部的痕迹导航路径中的Root以返回上一个屏幕:

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

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

现在您可以看到问题所在。看看第74行;应用色调过滤器的方法需要很长时间才能执行,并且它直接从collectionView(_:cellForItemAt :)调用,每当它请求过滤后的图像时,它将阻塞主线程(以及整个UI)。

卸载工作

要解决这个问题,您需要执行两个步骤:首先,使用DispatchQueue.global().async将图像过滤卸载到后台线程上;然后在每个图像生成后对其进行缓存。初学者项目中包含一个简单的小型图像缓存类(引人注目的名称为ImageCache),它只是将图像存储在内存中并使用给定的密钥检索它们。

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

我去! Xcode在恰当的位置打开。Boom!

现在,在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 {
          if let cachedImage = ImageCache.shared.image(forKey: "\(flickrPhoto.id)-filtered") {
            cell.imageView.image = cachedImage
           }
           else {
             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加载图像并将其存储在缓存中。

通过导航到Product \ Profile(或⌘I - 重新运行Instruments中的应用程序 - 请记住,这些快捷方式将为您节省一些时间)。

请注意,这次Xcode不会询问您使用哪种仪器。这是因为您仍然为此应用程序打开了一个窗口,而Instruments假定您希望使用相同的选项再次运行。

再执行一些搜索,注意这次UI不是那么笨重!图像过滤器现在异步应用,图像在后台缓存,因此一旦只需要过滤一次。您将在调用树中看到许多dispatch_worker_threads - 这些正在处理应用图像过滤器的繁重工作。

看起来很棒!是时候发货了吗?还没! :]

Allocations, Allocations, Allocations

那你接下来要追查什么错误? :]

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

  1. 真正的内存泄漏是一个对象不再被任何东西引用但仍被分配的东西 - 这意味着永远不能重用内存。 即使使用Swift和ARC帮助管理内存,最常见的内存泄漏类型是循环持有或循环强引用。这是当两个对象彼此持有强引用时,每个对象使另一个对象不被释放。这意味着他们的记忆永远不会被释放。
  2. 无限的内存增长是继续分配内存并且永远不会被释放的机会。如果这种情况持续下去,那么在某些时候系统的内存将被填满,你的手上会有很大的内存问题。在iOS上,这意味着该应用程序将被系统杀死。

本教程中涉及的下一个工具是Allocations工具。这将为您提供有关正在创建的所有对象以及支持它们的内存的详细信息。它还显示您保留每个对象的计数。

要重新开始使用新仪器配置文件,请退出Instruments应用程序,不要担心保存此特定运行。现在按⌘I,从列表中选择Allocations仪器,然后单击Choose。

现在,您应该看到Allocations工具。它应该看起来很熟悉,因为它看起来很像Time Profiler。
单击左上角的“录制”按钮以运行该应用程序。这次你会注意到两个轨道。出于本教程的目的,您将只关注名为All Heap和Anonymous VM的那个。
在应用程序上运行Allocations工具后,在应用程序中进行五次不同的搜索,但不会深入查看结果。确保搜索有一些结果。现在让应用程序等待几秒钟。
您应该已经注意到All Heap和Anonymous VM轨道中的图形一直在上升。这告诉你正在分配内存。正是这个功能将引导您找到无限的内存增长。

您将要执行的是“生成分析”。为此,请单击名为Mark Generation的按钮。您可以在详细信息面板底部找到按钮:

单击它,您将看到轨道中出现一个红色标记,如下所示:
生成分析的目的是多次执行操作,并查看内存是否以无限制的方式增长。深入搜索,等待几秒钟以加载图像,然后返回主页面。然后再次标记一代。对不同的搜索重复执行此操作。

经过几次搜索后,仪器将如下所示:

此时,你应该开始怀疑。请注意您钻取的每个搜索的蓝色图表是如何上升的。嗯,那当然不好。但等等,内存警告怎么样?你了解那些,对吗?内存警告是iOS告诉应用程序内存部门事情变得紧张的方式,你需要清除一些内存。

这种增长可能不仅仅是因为你的应用程序;它可能是UIKit深处持有内存的东西。在指向任何一个之前,先给系统框架和你的应用程序一个清除内存的机会。

通过选择仪器菜单栏中的仪器\模拟内存警告或模拟器菜单栏中的硬件\模拟内存警告来模拟内存警告。您会注意到内存使用量略有下降,或者根本没有下降。当然不会回到它应该的位置。所以在某个地方仍然存在无限的内存增长。

Instruments: 谈论我的Generation

在每次迭代钻取到搜索之后标记生成的原因是您可以看到在每一个generation之间分配了哪些内存。看看细节面板,你会看到好几个generation。

在每个generation中,您将看到所有已分配的对象,并且在生成标记时仍然驻留。之后的generation将仅包含自上一generation标记后的对象。

看看增长列,你会发现某处确实存在增长。打开其中一代,你会看到:

哇,那有很多对象!你该从哪里开始呢?

简单。单击“增长”标题按大小排序,确保最重的对象位于顶部。在每一代的顶部附近,您会注意到一行标记为ImageIO_jpeg_Data,这听起来像您应用中处理的内容。单击ImageIO_jpeg_Data左侧的箭头以显示与此项目关联的内存地址。选择第一个内存地址以在右侧面板的“扩展详细信息”检查器中显示关联的堆栈跟踪:

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

看看这个方法,你会看到第81行调用set(_:forKey :)。请记住,这个方法会缓存一个图像,以防以后在应用程序中再次使用它。啊!那肯定听起来可能是个问题! :]

再次单击“在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: Notification.Name.UIApplicationDidReceiveMemoryWarning, object: nil, queue: .main) { [weak self] notification in
    self?.images.removeAll(keepingCapacity: false)
  }
}
  
deinit {
  NotificationCenter.default.removeObserver(self)
}
复制代码

这会注册UIApplicationDidReceiveMemoryWarningNotification的观察者来执行上面的闭包,从而清除图像。

代码需要做的就是删除缓存中的所有对象。这将确保不再有任何东西保留在图像上,它们将被解除分配。

要测试此修复程序,请再次启动仪器(从Xcode使用⌘I)并重复之前执行的步骤。不要忘记最后模拟内存警告。

注意:确保从Xcode启动,触发构建,而不是仅仅点击Instruments中的红色按钮,以确保您使用的是最新代码。您可能还希望在分析之前首先构建并运行,因为有时Xcode似乎不会将模拟器中应用程序的构建更新为最新版本(如果您只是Profile)。

这次生成分析应如下所示:

您会注意到内存警告后内存使用率下降。总体上仍有一些内存增长,但远不及以前那么多。

之所以还有一些增长的原因,实际上是由于系统库,你可以做的并不多。似乎系统库没有释放所有内存,这可能是设计上的,也可能是一个bug。您在应用程序中所能做的就是释放尽可能多的内存,而您已经完成了! :]

做得好!修补了另外一个问题!现在必须是出货的时候了!哦,等等 - 你还没有解决第一种泄漏问题。

强引用循环

如前所述,当两个对象彼此保持强引用时会发生强引用循环,因此永远不能释放内存。您可以使用Allocations仪器以不同的方式检测这些循环。

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

这一次,您将不会使用生成分析。相反,您将查看内存中不同类型的对象数量。单击“录制”按钮开始此运行。您应该已经看到大量的对象填满了细节面板 - 太多无法看清楚!要缩小到感兴趣的对象,请在左下角的字段中键入“Instruments”作为过滤器。
仪器中值得注意的两个栏目是#Persistent和#Transient。 Persistent列保留内存中当前存在的每种类型的对象数的计数。 “Transient”列显示已存在但已取消分配的对象数。持久对象正在耗尽内存,瞬态对象已释放内存。

您应该看到有一个ViewController的持久实例 - 这是有道理的,因为那是您当前正在查看的屏幕。还有应用程序AppDelegate的一个实例。

回到应用程序!执行搜索并深入查看结果。请注意,现在仪器中出现了一堆额外的对象:SearchResultsViewController和ImageCache等。 ViewController实例仍然是持久的,因为它的导航控制器需要它。没关系。

现在点按应用中的后退按钮。 SearchResultsViewController现已从导航堆栈弹出,因此应该取消分配。但它仍然在分配总结中显示#引用数为1!它为什么还在那里?

尝试执行另外两次搜索,然后在每次搜索后点击后退按钮。现在有3个SearchResultsViewControllers ?!这些视图控制器在内存中闲置的事实意味着某些内容正在强烈引用它们。看起来你有一个严重的循环引用!

您在这种情况下的主要线索是,不仅SearchResultsViewController持久存在,而且所有SearchResultsCollectionViewCells也是如此。循环引用可能在这两个类之间。

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

获取视图化

退出Allocations仪器并退出仪器套件。

在启动Visual Memory Debugger之前,在Xcode方案编辑器中启用Malloc Stack日志记录,如下所示:单击窗口顶部的Instruments Tutorial方案(停止按钮旁边),然后选择Edit Scheme。在出现的弹出窗口中,单击“运行”部分,然后切换到“诊断”选项卡。选中显示Malloc Stack的框,然后选择仅限实时分配选项,然后单击关闭。

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

现在激活Visual Memory Debugger,如下所示:

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

Visual Memory Debugger暂停您的应用程序并显示内存中对象的可视化表示以及它们之间的引用。

如上面的屏幕截图所示,Visual Memory Debugger显示以下信息:

  • 堆内容(调试导航器面板):显示应用程序暂停时在内存中分配的所有类型和实例的列表。单击某个类型会展开该行,以显示内存中类型的单独实例。
  • 内存图(主面板):主窗口显示内存中对象的直观表示。对象之间的箭头表示它们之间的引用(强关系和弱关系)。
  • 内存检查器(“实用程序”面板):这包括类名称和层次结构等详细信息,以及引用是强还是弱。

请注意Debug导航器中的某些行如何在它们旁边加上括号括起来的数字。括号中的数字表示该特定类型的实例在内存中的数量。在上面的屏幕截图中,您可以看到,经过少量搜索后,Visual Memory Debugger会确认您在Allocations工具中看到的结果,即从20到(如果您滚动到搜索结果的末尾)60个SearchResultsCollectionViewCell实例每个SearchResultsViewController实例都保留在内存中。

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

注意指向SearchResultsViewController实例的箭头。看起来有一些Swift闭包上下文实例引用了同一个视图控制器实例。看起来有点怀疑,不是吗?让我们仔细看看。选择其中一个箭头以在“实用工具”窗格中显示有关其中一个闭包实例与SearchResultsViewController之间的引用的更多信息。
在Memory Inspector中,您可以看到Swift闭包上下文和SearchResultsViewController之间的引用是强引用。如果选择SearchResultsCollectionViewCell和Swift闭包上下文之间的引用,您将看到它也标记为强引用。您还可以看到闭包的名称是“heartToggleHandler”.A-ha!这是在SearchResultsCollectionViewCell类中声明的!

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

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

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

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

当点击集合视图单元格中的心形按钮时,此闭包处理。这是强引用循环所在,但除非你之前遇到过,否则很难发现。但是由于Visual Memory Debugger,您可以跟踪到这段代码的所有路径!

闭包Cell使用self引用了SearchResultsViewController,它创建了一个强引用。闭包持有了Self。 Swift实际上强迫你在闭包中明确使用self这个词(而你通常可以在引用当前对象的方法和属性时删除它)。这有助于您更加了解持有它的事实。 SearchResultsViewController还通过其集合视图对单元格进行了强引用。

要打破强引用循环,可以将捕获列表定义为闭包定义的一部分。捕获列表可用于将闭包捕获的实例声明为弱引用或无主引用:

  • 当捕获的参考可能在将来变为零时,应该使用Weak。如果它们引用的对象被释放,则引用变为零。因此,它们是可选类型。
  • 当闭包及其引用的对象将始终具有相同的生命周期并且将同时取消分配时,应使用Unowned。无主引用永远不会置为nil。

要修复此强引用周期,请将捕获列表添加到heartToggleHandler,如下所示:

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

将self声明为Weak表示即使集合视图单元格对其进行引用也可以释放SearchResultsViewController,因为它们现在只是弱引用。释放SearchResultsViewController将取消分配其集合视图,进而取消分配单元格。

在Xcode中,再次使用⌘+ I在Instruments中构建和运行应用程序。

使用Allocations仪器再次在Instruments中查看应用程序(请记住过滤结果以仅显示作为初始项目一部分的类)。执行搜索,导航到结果,然后再返回。您应该看到,当您向后导航时,SearchResultsViewController及其单元格现在已被释放。它们显示瞬态实例,但没有持久实例。

循环被打破了,提交它吧!!

接下来该做什么?

这是项目的最终优化版本的下载链接,这完全归功于Instruments。

既然您已经掌握了本教程中的知识,那就去测试自己的代码,看看有什么有趣的东西出现了!此外,尝试使仪器成为您通常的开发工作流程的一部分。

您应该经常通过Instruments运行代码,并在发布之前对应用程序进行全面扫描,以确保尽可能多地捕获内存管理和性能问题。

现在去制作一些非常棒且高效的应用! :]

PS:

最近加了一些iOS开发相关的QQ群和微信群,但是感觉都比较水,里面对于技术的讨论比较少,所以自己建了一个iOS开发进阶讨论群,欢迎对技术有热情的同学扫码加入,加入以后你可以得到:

1.技术方案的讨论,会有在大厂工作的高级开发工程师尽可能抽出时间给大家解答问题

2.每周定期会写一些文章,并且转发到群里,大家一起讨论,也鼓励加入的同学积极得写技术文章,提升自己的技术

3.如果有想进大厂的同学,里面的高级开发工程师也可以给大家内推,并且针对性得给出一些面试建议

关注下面的标签,发现更多相似文章
评论