学计算机的那个

不是我觉到、悟到,你给不了我,给了也拿不住;只有我觉到、悟到,才有可能做到,能做到的才是我的.

0%

【译】Improve Performance in Your iOS Applications 二

翻译几篇iOS性能优化的文章 ⅠⅠ,这是本系列的第二篇文章,重点介绍iOS性能技巧,帮助你改进UI交互、媒体播放、动画和视觉效果等,提供流畅、无缝的体验。

当你的iOS应用面向任何数量的用户开发和发布时,它的性能都是至关重要的。你的用户希望它是令人愉快的、快速的、有响应的,所以如果你的应用看起来迟钝或无响应,它就会影响你的评价,你可能会失去有价值的用户。在为应用解决这个问题时,很容易忽略在整个开发过程中所做的选择对性能的影响。

在尝试优化任何代码之前,确保有一个问题需要修复是很重要的!不要被“预先优化”代码的诱惑所迷惑,这是错误的。定期使用Instruments来分析代码并发现任何缺陷。Matt Galloway有一节关于如何使用Instruments优化代码的精彩课程。

本文中提出的一些建议可能需要付出很大的努力才能实现,或者可能会使您的代码更加困难;因此选择谨慎。让我们开始吧。

你可以在这里阅读本系列的第一篇文章,它关注的是性能技巧,帮助你提高iOS应用程序的编译时间,更快地构建应用程序,并专注于在构建系统中改进iOS性能。

监控应用程序行为

苹果的iOS应用商店是一个拥有数百万应用程序的巨大市场,它为任何组织或个人开发和发布应用程序提供了多种机会。然而,有这么多可访问的应用程序,确定你的“最佳点”——吸引新客户的最佳地点——可能是困难的。

在App Store营收增长之前,你必须制定一套指标,让我们能够根据你的目标来监测你的成功情况。在定义了度量指标和衡量性能的明确目标后,保持警惕并了解应用在部署新功能时的反应非常重要。

Sentry是维护应用程序代码运行状况和性能的必要监视工具。错误跟踪和性能监视特性使您能够以更清晰、快速和有效的方式从前端到后端查看、纠正和了解应用程序。

Sentry对iOS应用程序的iOS性能监控允许你确定性能问题的根源,如API调用和数据库查询缓慢。

iOS UI的基本原理

在了解这些建议之前,必须定义和理解主线程的工作方式,以便优化呈现UI的性能。

根据经验,主线程不应该用于非常繁重的任务。相反,它应该主要用于以下行为:

  • 接受用户输入和交互
  • 显示结果并更新UI

大多数时候,主线程处理太多的事情,这导致帧的丢失或下降。当设备不能处理标准帧速率和用户看到延迟或屏幕卡住时,就会发生这种情况。

因此,确定哪些帧丢失了,哪些帧没有丢失就变得很重要了,但是为何丢失呢?

有时候很容易发现他们,因为最重要的是他们没有回应。其他时候,它不是,你需要更精确的东西来追踪他们。如果您想要快速跟踪它们,您可以使用分析工具来识别问题。

一旦你确定了帧丢失的根本原因,你就可以在开发过程中解决它们。以下是你可以想到的解决帧下降的一些可能性:

  • 限制在移动屏幕上呈现的视图数量
  • 尽可能避免元素的透明度
  • 减少请求作业的时间和频率
  • 解码JPEG图像
  • 执行后台线程操作

让我们一个一个地看一遍。

如何减少视图层次结构

最简单的方法是减少视图层次结构中的视图数量,尽可能避免透明度。为了达到同样的效果,下面的代码片段帮助绘制一个白色背景,它指示渲染者避免任何复杂的透明度处理:

1
2
label.layer.opacity = 1.0 
label.backgroundColor = .white

要验证透明度重叠的问题已经修复,你可以使用Xcode中的Debug- >View Debugging- >Rendering menu中的Color Blended Layers (颜色混合图层)工具。

减少操作频率

cellForItemAt, indexPathscrollViewDidScroll这样的函数必须非常流畅和快速,因为它们经常被调用,几乎每时每刻都在调用,所以它们需要非常快。

您应该始终确保您拥有可以创建和维护的最简单的视图和单元格。你还应该确保你使用的配置方法总是很轻的,比如有约束的布局。

图像解码

说到丢失帧,最常见的原因之一是图像解码。通常,imageview在主线程中,在幕后完成这个过程。然而,这可能会偶尔导致你的应用程序持续变慢,特别是当图像的大小或分辨率相当大时。

这个问题的一个解决方案是将解码任务委派给后台线程或队列。操作将不会像UIImageView的标准解码那样有效,但是主线程将是空闲的。

在这种方法中,重要的是同步特定的更新,如错误或警告,因为它们由主线程处理,而图像处理发生在后台线程,以避免任何故障或崩溃。

  • 额外提示:处理应用程序在后台

来自Apple Developer的官方文档表明,你应该准备你的应用程序在后台运行。即使是最简单的实例,当用户退出前台应用时,你的应用在UIKit挂起它之前移动到后台。根据文档指南,当你的应用程序在后台时,它应该做的越少越好,最好什么都不做。

在这种情况下,所有的进程,包括非主线程操作,也应该释放正在使用的资源-不损害你的应用程序的性能。当你的应用程序进入后台,UIKit调用你的应用程序的以下方法之一:

1
2
sceneDidEnterBackground(_:)  //method of the appropriate scene delegate for apps that support Scenes
applicationDidEnterBackground(_:) //method for all other apps

好消息是,您不需要丢弃从应用程序的asset(资产目录)中加载的图像。

  • 额外建议:缩放图像
    UIImageView只能显示与UIImageView具有相同尺寸的图像。封装在UIScrollView中的UIImageView使动态放大和缩小照片变得非常昂贵。

如果它是从远程服务加载的,并且在下载之前你不能控制它的大小,那么你可以使用后台线程来缩放已经下载并在UIImageView中使用的图像。

执行优化操作

在处理UI项的某些属性时,你可能会遇到一些离屏渲染的困难,因为你必须在暴露这些元素之前准备它们。换句话说,它大量使用了CPUGPU。那么,如何识别这个问题呢?

Color Offscreen-Rendered Yellow(离屏渲染黄色)非常方便!你可以在Debug - > View Debugging - > Rendering menu中找到相同的选项。

使用这个工具,您可以根据资源的重度来识别黄色或红色的元素。

一个常见的例子,例如使用圆角半径属性,在你的应用程序中表明这样的东西:

1
imageView.layer.cornerRadius = imageHeight / 2.0

上面的代码行导致对每个图像的操作多消耗50%的资源。为了避免这种情况,您应该适应以下性能最佳实践:

  • 避免使用CornerRadius 属性,用其他方法替换它
  • 除非非常必要,否则避免使用ShouldRasterize属性
  • 避免在大多数情况下使用Shadows,因为它会导致离屏渲染
  • 避免使用boundingRectWithSize进行文本测量,因为它会导致繁重的处理
  • 使用.round(),因为它们更轻,计算时的资源负担更小

设置有效背景

来自UIColor类的colorWithPatternImage被设计用来生成小的重复图片作为背景,如果你想使用全帧背景,你必须使用UIImageView。在这种情况下,切换到UIImageView可能会节省大量的内存。

1
2
UIImageView *bgView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"background"]]; 
[self.view addSubview:bgView];

如果你的背景是由小瓷砖组成的,你应该利用UIColor类的colorWithPatternImage函数,它显示更快,消耗更少的内存。

1
self.view.backgroundColor = [uiicolor colorWithPatternImage:[UIImage imageNamed:@"background"]];

缩短应用启动时间

在进行任何优化之前,评估iOS应用程序的启动时间是一个好主意。你的应用需要多少时间启动?开始时间延迟了,或者您对现有的阈值是否满意?

在你完成研究之后,你需要确定应用的实际启动窗口。根据苹果在2019年WWDC大会上的说法,应用程序的初始帧生成时间不应超过400毫秒。如果您对您的申请有很高的期望,您应该将此设置为最低目标。

有三种不同类型的启动:

当您的应用程序很长时间没有启动或在重新启动之后,您可能会经历“冷启动”。冷启动之后,每次后续的启动都被称为热启动。系统端服务目前正在运行,您的程序已经被放入内存。从主屏幕或应用程序切换器重新启动应用程序将恢复之前的启动。我们不需要等待您的应用程序上线太久,因为它已经在运行了!

要检查你的代码如何影响启动时间,可以使用XcodeApp Launch time模板。使用模板可以让我们在应用最初发布时了解它的生命周期。这是一种很好的方法,可以帮助你尽早了解哪些阶段会产生最大的延迟。Instruments对于处理产生性能问题的代码特别有用。

Sentry跟踪应用程序性能,度量吞吐量和延迟等指标,并在捕获由事务和跨度组成的分布式跟踪的同时跨多个服务显示错误的影响,以度量这些服务中的单个服务和操作。

对于iOS, Sentry允许不同的SDK安装方法,包括Cocoapods SDK, Swift Package Manager和Carthage。SDK还能够测量应用程序启动活动,跨越从应用程序启动到第一个自动生成的UI事务的不同阶段。

应用程序启动指标跟踪移动应用程序启动所需的时间。为此,Sentry测量冷启动和热启动。应用程序开始的开始是由进程开始时间标记的,而结束是由UIWindowDidBecomeVisibleNotification标记的。结果生成如下:

  • Pre main: From the beginning of the process time to the runtime init.

  • UIKit and Application Init: From the runtime init to the didFinishLaunchingNotification.

  • Initial Frame Render: From didFinishLaunchingNotification to UIWindowDidBecomeVisibleNotification.

延迟加载和重用您的视图

那些在UIScrollView中嵌套了很多视图的人比那些应用程序没有很多视图的人消耗更多的CPU和内存。这个想法是重复了UITableViewUICollectionView的行为。

不要一次创建所有的子视图,而是根据需要创建它们,并在完成时将它们放在一个队列中。

通过这种方式,您只需要在有人滚动时创建视图,这节省了内存。在创建视图时产生的能源效率问题也会影响到软件的其他方面。假设用户点击了一个按钮并希望显示一个视图。有两种选择:

  • 当屏幕加载时,创建并隐藏视图,然后在需要时显示它

  • 根据需要制作和展示items

第一种方法的优点是要求您从一开始就建立一个 viewpoint(观点),并一直维护它,直到不再需要它。这个选项比第二个选项消耗更多的内存。因为当用户点击一个按钮时,你的程序只需要重新启用视图。第二个选项是相反的——它使用更少的内存,但当按下按钮时运行速度会稍微慢一些。

在这种情况下,Mobile Vitals为您提供iOS应用程序上的信息,允许您优先考虑重大问题,并快速解决它们。Sentry检测迟缓帧和冻结帧以跟踪用户界面响应。手机或平板电脑通常渲染60帧/秒,即每帧渲染速度为16.67毫秒

苹果可以使用更快的帧率,特别是在120帧/秒的面板变得越来越普遍的情况下。Sentry检测帧率并更改这些程序的慢帧计算。

重用标识符以提高性能

UITableViewCellsUICollectionViewCells甚至UITableViewHeaderFooterViews设置错误的重用标识符是一个常见的应用程序开发错误。在给tableView中的行分配单元格时,数据源应该经常重用UITableViewCell实例。

如果没有reuseIdentifier,每次显示表视图时,必须在UI上设置一个新单元格。这可能会对性能产生重大影响,特别是应用程序的滚动体验。除了UICollectionView单元格cells和supplemental views(补充视图)外,reuseidentifier应该用于headerfooter视图。

如果需要,此过程将从队列中删除单元格或使用先前注册的nibclasses生成新单元格。如果没有可重用单元格,也没有设置classes或nib,则此方法返回nil。Sentry允许您设置警报,实时洞察错误,问题,甚至自定义的标准,您可以为自己的应用程序指定:

  • 新的问题出现
  • 问题越来越频繁
  • 过去已解决或被忽视的问题变得无法解决并再次浮出水面

您还可以为特定的失败率、操作的延迟等定义警报和接收通知。您可以从Sentry仪表板上的alerts页面创建和管理警报。

默认情况下,Sentry将提供相当数量的数据作为问题通知的一部分。在某些情况下,这些数据可能是源代码或其他用户数据。启用“增强隐私”选项将允许您规范这一点。为此,请访问组织的仪表板,选择设置,然后选中允许增加隐私的选项。当您激活此选项时,电子邮件提醒和系统的其他元素将开始将数据限制为问题的标题和描述。

如何进一步改进UI滚动

如果在复杂的层次结构中,如Table视图中滚动时,用户体验卡顿,请确保执行以下操作以保持Table视图滚动顺畅:

  • reuseIdentifier在重用cells时以适当的方式
  • 使所有视图都opaque(不透明),包括cells
  • 避免使用gradients(渐变)、zooming(缩放)或selecting a backdrop color(选择背景色)。
  • 如果cell中的数据是从Internet获取的,则使用异步加载和缓存。
  • shadowPath应该只在必要的时候用来绘制阴影。
  • 尽量避免使用子视图。
  • 避免使用applycellForRowAtIndexPath。如果您需要使用它,只需执行一次,并将结果保存在缓存中。
  • 提供userowHeight.section固定的高度,并在设置高度时避免对委托的请求。

缓存的使用

在iOS平台上,有多种方法可以创建具有视觉吸引力的UI。可以使用全尺寸图片或可调整大小的图片,它们可以使用CALayerCoreGraphics甚至OpenGL进行渲染。当然,每种解决方案在复杂性和性能方面都有所不同。

利用预渲染的图形可以节省时间,因为不需要iOS创建图片,在上面画画,然后显示在屏幕上。问题是,你必须在程序包中包含所有相关的照片,这就增加了它的容量——这就是为什么使用可变大小的图像更好:你可以节省不必要的空间,避免为应用程序的不同部分开发不同的图像,例如按钮。

另一方面,使用图像意味着你失去了改变图像可用性的能力,你将不得不反复重做它们,这将消耗时间和资源,如果你想创建动画效果,即使每张图像只代表一小部分更改,你将需要大量的图像,这将增加bundle的大小。

作为一种普遍的权衡,平衡性能和管理不断增长的bundle大小是可取的。

从bundle中加载图像最流行的方法之一是imageNamed,另一种是image with content file,这是不太常见的。

加载图像时,imageNamed将它们保存在缓存中。它在系统缓存中搜索具有指定名称的图像对象,如果找到就返回该对象。缓存中没有匹配图片的对象将从提供的文档中获取,然后使用此函数进行缓存和返回。imageWithContentsOfFile,另一方面,它只是加载图片。

问题是,你如何选择使用哪一个?

如果你只打算使用一次重图像,就没有必要缓存它们。相反,使用image with a Content file 的图像来节省内存。另一方面,ImageNamed在经常重用图像(如列表)的情况下是一个更好的选项。

如何有效地显示阴影

为了在一个layer或view上显示阴影,开发人员通常会选择如下示例的QuartzCore框架:

1
2
3
4
5
#import <QuartzCore/QuartzCore.h> 
UIView *view = [[UIView alloc] init];
view.layer.shadowOffset = CGSizeMake(-1.0f, 1.0f);
view.layer.shadowRadius = 5.0f;
view.layer.shadowOpacity = 0.6;

这对系统来说似乎更容易表示。不是吗?遗憾的是,上述策略存在缺陷。因为Core Animation必须先执行一个屏幕外的传递来确定你的视图的确切形状,这是很重的资源。

使用shadowPath解决了这个问题:

1
view.layer.shadowPath = [[UIBezierPath bezierPathWithRect:view.bounds] CGPath];

有了shadowPath, iOS不再需要每次都重新计算如何绘制,而是利用先前确定的路径。当您必须在某些视图中手动计算和更新阴影路径时,问题就出现了,这是耗时的。

注意内存警告

当系统内存不足时,iOS会通知所有正在运行的应用程序。当你的应用程序收到一个内存警告,它必须清除尽可能多的内存。这与苹果团队制定的指导方针是一致的。最有效的方法是删除对可重用缓存、图像对象和其他东西的强引用。在大多数情况下,使用UIKit的通知跟踪低内存变得简单。

另一方面,Sentry在问题发展之前添加面包屑。与传统日志一样,这些事件可能包含大量结构化数据。你可以允许SDK自动捕捉应用程序的面包屑,如点击和按键事件,或手动添加使用以下代码执行的特定操作的面包屑

1
2
3
4
5
6
7
8
9
10
11
import Sentry

let breadcrumb = Breadcrumb()

breadcrumb.level = SentryLevel.info

breadcrumb.category = "auth"

breadcrumb.message = "Authenticated user \(user.email)"

SentrySDK.addBreadcrumb(crumb: breadcrumb)

您可以参考本文档来理解和配置面包屑的数据和属性。从系统获得这些警报后,您应该从内存中删除任何不必要的对象。此外,还可以从应用程序的图像缓存中删除当前没有显示在屏幕上的图像。如果你不以这种方式处理内存警报,操作系统可能会关闭你的应用程序。要释放内存,对象必须能够被重新创建。在创建时,要注意基于仿真的渲染。

对于终端用户来说,响应时间是至关重要的,因为由于内存溢出和其他事情导致的延迟或无响应可能会让用户感到沮丧,导致用户完全放弃应用程序。

Sentry Metrics可以让你更好地了解用户与应用的交互方式。你可以更快地量化应用的健康状况,并发现可能出现的故障或性能问题,如内存不足错误(OOMs)。

在应用程序终止之前,Apple SDK会写一个报告到磁盘,其中包含堆栈跟踪、标签和面包屑等数据。这是因为OOM崩溃迫使操作系统在没有通知的情况下停止您的软件。在一次OOM之后,要恢复应用程序的状态并不容易。所以我们需要常规地存储应用程序状态。这将产生大量的I/O,降低应用程序的速度。OOM事件缺乏上下文。

管理历史遗留项目

Storyboards已经取代xib成为iOS 5的主要视觉设计工具。虽然xib在某些情况下可能是方便的,但它们并不总是必需的。你必须处理它们,除非你的目标是ios 5之前的设备或利用自定义可重用视图。

如果您必须使用xib,请尽可能保持它们的简单性。如果视图控制器的视图层次结构需要创建多个XIB,那么为每个XIB构造一个。XIB中包含的任何图像也包含在内存负载中,应该予以考虑。删除当前不用于保存内存的所有视图。Storyboards防止了这种情况的发生,因为它们只在绝对必要的时候构建视图控制器。

当加载包含对图像或声音资源的引用的nib文件时,nib加载进程将加载并缓存实际的图像或声音文件。OS X将图片和声音资源保存在临时缓存中,以便以后可以访问。在iOS上,命名缓存只存储图片资源,不存储其他类型的数据。根据你的平台,可以使用NSImageUIImageimageNamed:方法来获取图像。

Sentry通过趋势来识别那些随着时间的推移其性能发生了显著变化的事务。当您有大量计数的事务时,此视图非常适合提供见解。您可以通过单击页面右上角的选项卡从性能主页访问趋势视图。随着时间的推移,这个页面突出显示了盈利能力发生显著变化的交易。

总结

尽管应用程序性能非常重要,但开发人员经常忽略它。因此,在开发应用程序时,我们倾向于只使用最强大的设备和最昂贵的数据计划。结果,我们对表现不佳的后果视而不见。

在本文中,您了解了如何为UI、动画等提高iOS应用程序的性能。本文试图确保所有的UI更新、前端渲染、动画以及Sentry如何帮助您加速性能提升之旅。