iOS 性能优化

前言

本文将从发现问题、解决问题和预防问题进行总结

如何发现性能问题

  1. 业务性能监控,是指在App本地,业务的开始和结束处打点上报,然后后台统计达到监控目的;

  2. 卡顿监控。卡顿监控的实现一般有两种方案:

1)主线程卡顿监控。通过子线程监测主线程的 runLoop,判断两个状态区域之间的耗时是否达到一定阈值。具体原理和实现,这篇文章介绍得比较详细。

(2)FPS监控。要保持流畅的UI交互,App 刷新率应该当努力保持在 60fps。监控实现原理比较简单,通过记录两次刷新时间间隔,就可以计算出当前的 FPS。

  • 在实际应用过程,无论是主线程监控,还是 FPS 监控,抖动都比较大。因此,一套综合的判断方法,结合了主线程监控、FPS监控,以及CPU使用率等指标,作为判断卡顿的标准。

性能问题的解决方法

  1. 优化业务流程。
    性能优化看似高深,真正落到实处才会发现,最大的坑往往都隐藏在于业务不断累积和频繁变更之处。
  2. 合理的线程分配
    由于 GCD实在太方便了,如果不加控制,大部分需要抛到子线程操作都会被直接加到 global 队列,这样会导致两个问题:
    (1). 开的子线程越来越多,线程的开销逐渐明显,因为开启线程需要占用一定的内存空间(默认的情况下,主线程占1M,子线程占用512KB)。
    (2).多线程情况下,网络回调的时序问题,导致数据处理错乱,而且不容易发现。为此,我们项目定了一些基本原则。
  • UI 操作和 DataSource 的操作一定在主线程。
  • DB 操作、日志记录、网络回调都在各自的固定线程。
  • 不同业务,可以通过创建队列保证数据一致性。

合理的线程分配,最终目的就是保证主线程尽量少的处理非UI操作,同时控制整个App的子线程数量在合理的范围内。

  1. 预处理和延时加载

预处理:是将初次显示需要耗费大量线程时间的操作,提前放到后台线程进行计算,再将结果数据拿来显示。

延时加载:是指首先加载当前必须的可视内容,在稍后一段时间内或特定事件时,再触发其他内容的加载。这种方式可以很有效的提升界面绘制速度,使体验更加流畅。(UITableView 就是最典型的例子)

这两种方法都是在资源比较紧张的情况下,优先处理马上要用到的数据,同时尽可能提前加载即将要用到的数据。

  1. 缓存
    cache可能是所有性能优化中最常用的手段,但也是我们极不推荐的手段。cache建立的成本低,见效快,但是带来维护的成本却很高。如果一定要用,也请谨慎使用,并注意以下几点:
  • 并发访问 cache 时,数据一致性问题。
  • cache 线程安全问题,防止一边修改一边遍历的 crash。
  • cache 查找时性能问题。
  • cache 的释放与重建,避免占用空间无限扩大,同时释放的粒度也要依实际需求而定。
  1. 使用正确的API
    使用正确的 API,是指在满足业务的同时,能够选择性能更优的API。
  • 选择合适的容器;
  • 了解 imageNamed: 与 imageWithContentsOfFile:的差异(imageNamed: 适用于会重复加载的小图片,因为系统会自动缓存加载的图片,imageWithContentsOfFile: 仅加载图片)
  • 缓存 NSDateFormatter 的结果。
  • 寻找 (NSDate *)dateFromString:(NSString )string 的替换品。
//#include 
time_t t;
struct tm tm;
strptime([iso8601String cStringUsingEncoding:NSUTF8StringEncoding], "%Y-%m-%dT%H:> %M:%S%z", &tm);
tm.tm_isdst = -1;
t = mktime(&tm);
[NSDate dateWithTimeIntervalSince1970:t + [[NSTimeZone localTimeZone] secondsFromGMT]];
  • 不要随意使用 NSLog().

  • 当试图获取磁盘中一个文件的属性信息时,使用 [NSFileManager attributesOfItemAtPath:error:] 会浪费大量时间读取可能根本不需要的附加属性。这时可以使用stat代替NSFileManager,直接获取文件属性:

#import 
struct stat statbuf;
const char *cpath = [filePath fileSystemRepresentation];
if (cpath && stat(cpath, &statbuf) == 0) {
    NSNumber *fileSize = [NSNumber numberWithUnsignedLongLong:statbuf.st_size];
    NSDate *modificationDate = [NSDate dateWithTimeIntervalSince1970:statbuf.st_mtime];
    NSDate *creationDate = [NSDate dateWithTimeIntervalSince1970:statbuf.st_ctime];
    // etc
}

如何预防性能问题

  1. 内存泄露检测工具
    MLeakFinder是团队成员zepo在github开源的一款内存泄露检测工具,具体原理和使用方法可以参见这篇文章。在此之前,内存泄露引起的性能问题是很难被察觉的,只有泄露到了相当严重的程度,然后通过Instrument工具,不断尝试才得以定位。MLeakFinder能在开发阶段,把内存泄露问题暴露无遗,减少了很多潜在的性能问题。

  2. FPS/SQL性能监测工具条
    该工具条是在DEBUG模式下,以浮窗的形式,实时展示当前可能存在问题的FPS次数和执行时间较长的SQL语句个数,是团队成员tower的杰作。FPS监测的原理并不复杂,前文也有介绍,虽然并不百分百准确,但非常实用,因为可以随时查看FPS低于某个阈值时的堆栈信息,再结合当时的使用场景,开发人员使用起来非常便利,可以很快定位到引起卡顿的场景和原因。SQL语句的监测也非常实用.

  3. UI / DataSource主线程检测工具。
    该工具是为了保证所有的UI的操作和 DataSource 操作一定是在主线程进行,同样是由tower同学贡献。实现原理是通过hook UIView-setNeedsLayout,-setNeedsDisplay,-setNeedsDisplayInRect 三个方法,确保它们都是在主线程执行。子线程操作UI可能会引起什么问题,苹果说得并不清楚,实际开发中我们遇到几种神奇的问题似乎都是跟这个有关。

  • app 突然丢动画,似乎 iOS 系统也有这个 bug。虽然没有确切的证据,但使用这个工具,改完所有的问题后,bug 也好了(不止一次是这样)。

  • UI 操作偶尔响应特别慢,从代码看没有任何耗时操作,只是简单的 push 某个 controller。

  • 莫名的 crash,这当然是因为 UI 操作非线程安全引起的。

  • 更多时候,子线程操作 UI 也并不一定会发生什么问题,也正因为不知道会发生什么,所以更需要我们警惕,这个工具替我们扫除了这些隐患。虽然,苹果表示,现在部分的 UI 操作也已经是线程安全了,但毕竟大部分还不是。DataSource 的监测是因为我们业务定下的原则,保证列表 DataSource 的线程安全。

卡顿产生的原因

图片.png

界面卡顿的原因:在 VSync 信号到来后,系统图形服务会通过 CADisplayLink 等机制通知 App,App 主线程开始在CPU中计算显示内容,比如视图的创建、布局计算、图片解码、文本绘制等。随后 CPU 会将计算好的内容提交到 GPU去,由 GPU 进行变换、合成、渲染。随后 GPU 会把渲染结果提交到帧缓冲区去,等待下一次VSync信号到来时显示到屏幕上。由于垂直同步的机制,如果在一个 VSync 时间内,CPU 或者 GPU 没有完成内容提交,则那一帧就会被丢弃,等待下一次机会再显示,而这时显示屏会保留之前的内容不变。
在开发中,CPU和GPU中任何一个压力过大,都会导致掉帧现象,所以在开发时,也需要分别对CPU和GPU压力进行评估和优化

CPU:加载资源,对象创建,对象调整,对象销毁,布局计算,Autolayout,文本计算,文本渲染,图片的解码, 图像的绘制(Core Graphics)都是在CPU上面进行的。

GPU:GPU是一个专门为图形高并发计算而量身定做的处理单元,比CPU使用更少的电来完成工作并且GPU的浮点计算能力要超出CPU很多。
GPU的渲染性能要比CPU高效很多,同时对系统的负载和消耗也更低一些,所以在开发中,我们应该尽量让CPU负责主线程的UI调动,把图形显示相关的工作交给GPU来处理,当涉及到光栅化等一些工作时,CPU也会参与进来,这点在后面再详细描述。
相对于CPU来说,GPU能干的事情比较单一:接收提交的纹理(Texture)和顶点描述(三角形),应用变换(transform)、混合(合成)并渲染,然后输出到屏幕上。通常你所能看到的内容,主要也就是纹理(图片)和形状(三角模拟的矢量图形)两类。

CPU 和 GPU 的协作:要在屏幕上显示视图,需要CPU和GPU一起协作,CPU计算好显示的内容提交到GPU,GPU渲染完成后将结果放到帧缓存区,随后视频控制器会按照 VSync 信号逐行读取帧缓冲区的数据,经过可能的数模转换传递给显示器显示。

缓冲机制:iOS使用的是双缓冲机制。即GPU会预先渲染好一帧放入一个缓冲区内(前帧缓存),让视频控制器读取,当下一帧渲染好后,GPU会直接把视频控制器的指针指向第二个缓冲器(后帧缓存)。当你视频控制器已经读完一帧,准备读下一帧的时候,GPU会等待显示器的VSync信号发出后,前帧缓存和后帧缓存会瞬间切换,后帧缓存会变成新的前帧缓存,同时旧的前帧缓存会变成新的后帧缓存。

iOS 保持界面流畅的技巧
LLDebugTool是一款针对开发者和测试者的调试工具
iOS 性能优化总结
iOS性能优化系列篇之“优化总体原则”
iOS应用UI线程卡顿监控
UITableView-FDTemplateLayoutCell - 缓存

绘制像素到屏幕上
iOS图形原理与离屏渲染
iOS 保持界面流畅的技巧
Advanced Graphics and Animations for iOS Apps(session 419)

使用 ASDK 性能调优 - 提升 iOS 界面的渲染性能
Designing for iOS: Graphics & Performance
iOS离屏渲染之优化分析
iOS视图渲染以及性能优化总结
iOS 离屏渲染
深刻理解移动端优化之离屏渲染
iOS 流畅度性能优化、CPU、GPU、离屏渲染
iOS 图形性能优化锦集
离屏渲染优化详解:实例示范+性能测试

你可能感兴趣的:(iOS 性能优化)