浅谈 iOS 中 App 的性能检测和优化

请注意,本文编写于 673 天前,最后修改于 291 天前,其中某些信息可能已经过时。

一、前言

笔者当前的工作项目目前还处于快速迭代、不断地添加新业务的过程中,性能问题也在业务的不断累积迭代中逐渐体现出来。但,随着项目业务需求暂时稳定下来,用户量开始上升,关于App性能问题逐渐显得重要。为此,我开始做一些针对性的性能问题优化。本文将从发现问题、解决问题进行总结。

二、发现问题

1. 内存泄露检测工具

  • MLeakFinder是在 github 开源的一款内存泄露检测工具,具体原理和使用方法可以参见这篇文章。在此之前,内存泄露引起的性能问题是很难被察觉的,只有泄露到了相当严重的程度,然后通过Instrument工具,不断尝试才得以定位。MLeakFinder 能在开发阶段,把内存泄露问题暴露无遗,减少了很多潜在的性能问题。
  • FBRetainCycleDetectorFacebookgithub 开源的一款寻找内存泄漏的工具,该工具的目的在于在运行时定位到循环引用。配合MLeakFinder使用,效果还是很好的。但缺点是内存泄漏并不一定是由循环引用发生的,在 ARC 下的CoreFoundation或者CoreGraphics的不当使用都可能导致内存泄漏。而最常见的循环引用也可能有以下几种情况:
  1. Block 内引用 self
  2. Delegate 中的 propertystrong
  3. NSTimer创建的时候被self持有,而在创建的类方法里面又引用了self

2.静态Analyze项目

  1. 逻辑错误:访问空指针或未初始化的变量等;
  2. 内存管理错误:如内存泄漏等(主要在 MRC 下);
  3. 声明错误:从未使用过的变量;
  4. Api 调用错误:未包含使用的库和框架。

项目中主要处理了很多Dead Store,少量的Logic errorCore Foundation/Objective-C问题。

3.使用Instrument工具

需要注意的是,使用Profile来检测 App 性能的时候,SchemeProfile选项卡的Build Configuration一定要设为Release,因为很多类的方法或者宏只有在Debug下才启用。

很容易让人忽略的是NSLog语句也应该设定为在Release下禁用。因为性能影响很大,在真机下,运行的耗时比printf高100多倍,项目里面如果存在大量的NSLog势必会对项目的流畅性造成一定影响。详情可以查看NSLog效率低下的原因及尝试lldb断点打印Log,这篇文章写的很详细。

一般情况下主要使用以下几个工具:

  1. Time Profiler

    1. 时间分析工具,它会按照设定的时间间隔(默认1毫秒)来跟踪每一线程的堆栈信息(stacktrace),并通过比较时间间隔之间的堆栈状态,来推算出某个方法执行了多久,给出一个近似值。
    2. 根据这些时间对应代码便可以做一些优化,减少重复的耗时逻辑优化CPU。
  2. Core Animation

    1. 要注意的一点是必须是真机调试,用于调试离屏渲染,绘图,动画,等操作。
    2. Color Blended Layers:显示出被混合的图层BlendedLayer(用红色标注),BlendedLayer是因为这些Layer是透明的(Transparent),系统在渲染这些view时需要将该view和下层view混合(Blend)后才能计算出该像素点的实际颜色。所以红色越少越好。
    3. Color Offscreen-Rendered Yellow:离屏渲染意思是 iOS 要显示一个视图时,需要先在后台用 CPU 计算出视图的Bitmap,再交给 GPU 做Onscreen-Rendering显示在屏幕上,因为显示一个视图需要两次计算,所以这种Offscreen-Rendering会导致 app 的图形性能下降。所以黄色越少越好
    4. Color Hits Green and Misses Red:很多视图 Layer 由于 Shadow、Mask 和 Gradient 等原因渲染很久,因此 UIKit 提供了 Api 用于缓存这些Layer:[layersetShouldRasterize:YES],系统会将这些Layer 缓存成 Bitmap 位图供渲染使用,如果失效时便丢弃这些 Bitmap 重新生成。所以绿色越多,红色越少越好。
  3. Leaks

    1. 是 Profile 中用来检测内存泄漏的工具,灵活的运用 Leaks 可以帮助我们预防程序中的内存泄漏防止程序内存耗用过大被挂起。
    2. 首先双击Leaks点击左上角红色圆点运行,并且选中CallTree,在CallTree选项中勾选InvertCallTreeHideSystemLibraries

三、解决问题

产生性能问题的原因多种多样,因此解决的办法也不尽相同,比较常用的大概有以下几种:

1.优化业务流程

性能优化看似高深,真正落到实处才会发现,最大的坑往往都隐藏在于业务不断累积和频繁变更之处。优化业务流程就是在满足需求的同时,提出更加高效优雅的解决方案,从根本上解决问题。从实践来看,这种方法解决问题是最彻底的,但通常也是难度最大的。

2.合理的线程分配

由于 GCD 实在太方便了,如果不加控制,大部分需要抛到子线程操作都会被直接加到 global 队列,这样会导致两个问题

  1. 开的子线程越来越多,线程的开销逐渐明显,因为开启线程需要占用一定的内存空间(默认的情况下,主线程占1M,子线程占用512KB)。
  2. 多线程情况下,网络回调的时序问题,导致数据处理错乱,而且不容易发现。为此,我们项目定了一些基本原则。
  • UI 操作和 DataSource 的操作一定在主线程。
  • DB 操作、日志记录、网络回调都在各自的固定线程。
  • 不同业务,可以通过创建队列保证数据一致性。例如,想法列表的数据加载、书籍章节下载、书架加载等。

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

3.预处理和延时加载

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

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

这两种方法都是在资源比较紧张的情况下,优先处理马上要用到的数据,同时尽可能提前加载即将要用到的数据。在微信读书中阅读的排版是优先级最高的,所在在阅读过程中会预处理下一页、下一章的排版,同时可能会延时加载阅读相关的其它数据(如想法、划线、书签等)。

4.缓存

cache 可能是所有性能优化中最常用的手段,但也是我们极不推荐的手段。cache建立的成本低,见效快,但是带来维护的成本却很高。如果一定要用,也请谨慎使用,并注意以下几点:

  • 并发访问 cache 时,数据一致性问题。
  • cache 线程安全问题,防止一边修改一边遍历的 crash。
  • cache 查找时性能问题。
  • cache 的释放与重建,避免占用空间无限扩大,同时释放的粒度也要依实际需求而定。

5.使用正确的API

使用正确的 API,是指在满足业务的同时,能够选择性能更优的API。

  • 选择合适的容器;
  • 了解 imageNamed:imageWithContentsOfFile:的差异(imageNamed: 适用于会重复加载的小图片,因为系统会自动缓存加载的图片,imageWithContentsOfFile: 仅加载图片)
  • 缓存 NSDateFormatter 的结果,Apple 的文档是推荐做一个单例
  • 不要随意使用 NSLog().
  • 当试图获取磁盘中一个文件的属性信息时,使用 [NSFileManager attributesOfItemAtPath:error:] 会浪费大量时间读取可能根本不需要的附加属性。这时可以使用 stat 代替 NSFileManager,直接获取文件属性:

    #import <sys/stat.h>
    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
    }

四、总结

目前由于开发前期没有良好的代码管理,包括:命名规范与代码风格要求、高质量的代码复审等。目前项目里面不可避免存在一些历史遗留问题,只能靠每次迭代抽出时间专门做优化才能让App性能更加优越。当然平时良好的代码习惯和复审也是很重要的,最好的情况是提高整个团队成员性能优化意识,借助性能查找工具,将性能问题尽早暴露在开发阶段,达到预防为主的效果。

添加新评论

评论列表