打破Mail.Ru的iOS电子邮件应用程序的启动时间的创纪录故事

但是,这种基于日志的分析存在一些陷阱。 如果您在我们的轨道上,并注意到序列中的某些子步骤意外地花费了很多时间,请不要急于对其进行优化。 首先,仅通过执行此子步骤确保整个时间间隙都被占用。 可能是通过等待主队列中的dispatch_async来限制子步骤的完成,而主队列当前正忙于其他任务。 如果您在此处删除dispatch_async ,则子步骤现在可能会花费更少的时间,而所有子步骤所花费的总时间将被重新分配,并且将保持不变。

请记住,执行某个功能所需的时间不足以发现优化候选者。 您还需要考虑应用程序的高级体系结构。 重要的是要了解子步骤的相互依赖性,即子步骤可以同时执行还是只能连续执行。 知道了这一点,如果并行执行连续的子步骤,或者推迟以后执行的可选子步骤,则可以提高速度。 在那儿,基于日志的分析很有用。

输入输出

即使使用闪存,从磁盘读取文件的时间也比使用RAM或CPU的时间长十倍,因此,在启动时尽量减少磁盘操作的数量是有意义的。 由磁盘读取数据引起的节流可能在间隙中显示为时间间隙,但更多情况下根本不显示节流,因此需要其他方法来进行节流。

  • Xcode中的“ I / O活动”工具(请参阅“系统使用情况”分析模板)仅在实际设备上有效。 它捕获I / O事件并显示有关所有与I / O相关的系统调用的信息,访问文件的路径以及这些操作期间执行的代码的堆栈跟踪。
  • 如果您对上述方法不满意,可以通过在__open函数上添加断点来获取类似的信息。 在这种情况下,可以使用以下LLDB命令获取被访问文件的名称: p(char *)$ r0 (存储用于此调用的第一个参数的寄存器的名称将取决于体系结构)。

布局和东西

可能会发生一些CPU密集型调用,这些调用无法使用Time Profiler进行分析,因为它们的堆栈跟踪并不总是包含指示这些调用所属的应用程序部分的信息。 例如,这些是用于遍历视图层次结构的布局调用。 它们的堆栈跟踪通常仅包含系统方法,而我们很想知道哪些视图具有耗时的布局调用。 您可以使用毛毛雨来测量此类呼叫,并将其与它们处理的对象匹配。 更具体地说,您可以在调用之前和之后添加一些代码,该代码将在控制台中显示一些有关已处理对象和每个调用执行时间的信息。 使用此日志,可以很容易地创建一个Google表格表格,该表格显示如何分配每个视图的布局时间。 为了解决问题,我们使用了Aspects库。

在着手优化启动时间之前,请记住要通过删除所有代码来确定最短的启动时间,除了那些只会使您的应用看起来像已启动的代码(保留功能并不是这里的目的)。 在我们的案例中,最小的应用程序是一个屏幕,其中包含一个带有导航栏和按钮的UINavigationController ,一个UITableView和几个单元格。

为了进行测试,请使用与App Store软件包尽可能接近的应用程序版本,禁用所有运行时检查,日志和声明,并在软件包设置中启用优化。 在我们的例子中,用于内部测试的程序包包括许多方法的混乱,以在主线程中对这些方法启用运行时检查。 当然,这极大地影响了测试结果。

要优化的第一件事是在主线程中执行的代码,因为整个启动过程的目的是在屏幕上显示某些内容,而这只能在主线程中进行。 后台线程也值得考虑,因为硬件并行化的能力是有限的:大多数情况下,设备只有两个内核,并且占用大量CPU的后台代码可能会严重影响总启动时间。 在调查某些分析库对应用程序启动时间的影响时,我们遇到了这一问题。 初始化之后,该库完全在后台线程中工作,但是在禁用该库后,我们获得了令人鼓舞的时间。

钓鱼寻求优化UI启动时间的更多方法,我们发现加载,配置和显示电子邮件列表中的单元格完全是一个费时的工作。 造成这种情况的原因有很多:我们有很多单元格,每个单元格包含多个子视图,并且主要的耗时者有图标和标签。 图标导致速度下降,因为图像加载通常比其他操作慢,而标签的速度下降是由于计算其大小以适合标签的内容而引起的。

电子邮件单元格中显示的图标指示电子邮件的属性(未读,已标记,具有附件)和可能的操作(在滑动电子邮件时显示在操作面板中)。 我们更改了创建操作面板的逻辑,并使其变得“懒惰”:只有在发生滑动后才创建面板。 然后,我们对属性图标进行了类似的更改:如果列表中没有具有相应属性的电子邮件,则不会加载特定的图标。

实际上,“延迟加载”优化对于启动时执行的所有操作都很方便。 您真正不需要启动的所有内容都应该以惰性(或延迟)的方式创建:仅在特定条件下显示的各种视图,以及在初始显示的UI中保持不可见的存根,图标和视图控制器。 在我们的例子中,一个很好的例子是使用+ [UIImage imageNamed:]加载图像。 乍一看, imageNamed:应该不会花很长时间,因为实际上只在显示时加载和解码了图像。 但是,由于调用数量众多,它们的总执行时间已累积。 在我们的应用程序中,所有UI元素的外观都在启动时进行了集中配置(此逻辑在AppearanceConfigurator类中实现)。 理想情况下,我们也必须以懒惰的方式执行此配置,但是我们未能找到一种精益的解决方案,该解决方案无法让我们将所有设置从已配置的视图类中删除到一个地方,并且可以很好地应用于懒惰中在相应的视图或控制器上的第一个命中的方式。 为了优化imageNamed:,我们进行了两项更改:将对imageNamed:的调用替换为对imageNamed:inBundle:compatibleWithTraitCollection:的调用,该调用花费了更少的时间,并在启动时删除了对imageNamed:的所有直接调用。 相反,我们将所有配置逻辑调用委托给代理对象,这些代理对象将这些调用转发到真实的UIImage ,该UIImage是在第一次调用其任何方法时以惰性方式创建的。

以下是在启动时管理用户的主观感知的一些技巧。

  • 通常,开发人员经常滥用iOS人机界面指南中的以下建议:“启动屏幕不是表达艺术的机会。 过去我们也经常滥用此建议,在启动时显示一个漂亮的Mail.Ru徽标,但是在我们的电子邮件应用程序的较新版本中,我们放置了该名称。这项建议付诸行动。
  • 请注意动画负载指示器。 根据一些研究,动画使用户责怪您的应用程序运行缓慢。
  • 通常的做法是在主屏幕之后显示一个额外的启动屏幕。 额外的屏幕可帮助显示负载指示器,或从启动屏幕到基本UI进行动画过渡,或者如果初始化花费太长时间,则可以阻止系统停止应用程序。 如果您优先考虑启动速度,并且没有特殊原因需要额外的启动屏幕,请考虑避免使用多余的屏幕,因为它们需要更多的时间来加载和显示。

测量自动化

随着新功能的添加和应用程序代码的重构,应用程序的启动时间会随着时间的流逝而逐渐增长。 每次增加可能仅需几毫秒,因此累积效果只会在一段时间内变得明显。 如果您花了很多时间来优化应用程序的启动时间,那么您肯定会讨厌多余的时间可能会虚度回来的想法。 我们避免这种回弹的方法是构建一个图表,该图表显示每次提交到存储库主分支时应用程序的启动时间,因此我们可以轻松地发现所有更改。

我们建议在您能找到的最慢的设备上运行启动时间测量,但不要使用模拟器。

我们的自动测量是通过Jenkins启动的,类似于持续集成范围内的所有其他任务。 为此,我们使用了专用的iPhone。 我们尝试了两种测试方案:有越狱和没有越狱。 从长远来看,我们选择了越狱方案,因为它给我们带来了以下好处。

  • 无需通过USB将设备永久连接到我们的其中一台Jenkins服务器,而开发人员无需物理访问其中的大多数服务器。 越狱后,可以通过ssh与设备进行交互。
  • 无需将Jenkins中的任务与设备连接到的特定从设备相关联。
  • 无需为Jenkins从站安装任何特殊软件即可通过USB与设备进行交互。
  • 构建应用程序包时,签名和配置文件的问题更少。

无论您使用的设备是否具有越狱功能,应用程序包的构建都是类似的。 唯一的区别是,如果越狱,您对签名和配置文件的要求就不太严格。 我们正在使用“ App Store Release”配置通过xcodebuild构建Xcode项目,并为所有启动子步骤启用了日志记录。 ENABLE_TIME_LOGGER不仅启用了日志记录,还让我们在启动过程完成后自动停止我们的应用程序。

结果

在优化活动的过程中,我们将电子邮件应用程序的实际启动时间减少了大约三分之一,并提高了用户对应用程序启动速度的主观感知,这得到了保留率的提高的支持。 但是,更重要的成就是在将来控制和避免了发射时间回弹的机制。