iOS 性能调试

一、简介

性能调优的方式可以分为:

  • 通过专门的性能调优工具;
  • 通过代码优化;

二、内容

1、性能调优工具介绍

1.1、静态分析工具 – Analyze

相信 iOS 开发者在 App 进行 BuildArchive 时,会产生很多编译警告,这些警告是编译时产生的,静态分析的过程也类似,在 XCode Product 菜单下,点击 AnalyzeApp 进行静态分析。

Analyze 主要分析以下四种问题:
1、逻辑错误:访问空指针或未初始化的变量等;
2、内存管理错误:如内存泄漏;
3、声明错误:从未使用的变量;
4、API调用错误:未包含使用的库和框架。

1.2、内存泄漏分析工具 – Leaks

点击 XCodeProduct 菜单 Profile 启动 Instruments ,使用 Leaks 开始动态分析。
选择 Leaks ,会自动启动 Leaks 工具和 IOS 模拟器,Leaks 启动后会开始录制,随着对模拟器运行的 App 的操作,可以在 Leaks 中查看内存占用的情况。

注:如果项目使用了 ARC,随着操作,不断地开启或关闭视图,内存可能持续上升,但这不一定表示存在内存泄漏,ARC 释放的时机是不固定的。

Leaks 顶部分为两栏:All Heap & Anonymous VMLeaks ChecksAll Heap & Anonymous VM 中的曲线代表内存分配和内存泄漏曲线。

点击第二栏 Leaks Checks 展示内存泄漏,进行内存泄漏分析,将光标放置在上图的小红叉上可看到 leak 数量,右下方是leaks 调试的选项:

建议把 Snapshot Interval 间隔时间设置为10秒,勾选 Automatic SnapshottingLeaks 会自动进行内存捕捉分析。
在你怀疑有内存泄漏的操作前和操作后,可以点击 Snapshot Now 进行手动捕捉。
Leaked Object的表格中显示了内存泄漏的类型、数量及内存空间等。
点击具体的某个内存泄漏对象,在右侧 Detail 窗口中会出现导致泄漏可能的位置,其中 黑色头像(现在是蓝色头像) 代表了最可能的位置,具体使用可以参考我的文章 Instruments之Leaks的简单使用

内存泄漏动态分析技巧:

  • 熟练使用 Leaks 后会对内存泄漏判断更准确,在可能导致泄漏的操作里,多使用 Snapshot Now 手动捕捉。
  • 开始时如果设备性能较好,可以把自动捕捉间隔设置为 5 秒钟。
  • 使用 ARC 的项目,一般内存泄漏都是 malloc 、自定义结构、资源引起的,多注意这些地方进行分析。

开启ARC后,内存泄漏的原因:

  • 开启了ARC并不是就不会存在内存问题,苹果有句名言: ARC is only for NSObject
    iOS 中使用 malloc 分配的内存,ARC 是不会处理的,需要自己进行处理。
    例子中的 CGImageRef 也是一个 Image 的指针,ARC 也不会进行处理。
1.3 不合理内存分析工具 – Allocation

关于内存的问题,除了内存泄漏以外,还可能存在内存不合理使用的情况,也会导致 iOS 内存警告。

内存的不合理使用往往比内存泄漏更难发现,内存泄漏可以更多借助于工具的判断,而内存的不合理运用更多需要开发者结合代码、架构来进行分析。

明确说明一下两者的区别:

  • 内存泄漏:指内存被分配了,但是程序中已经没有指向该内存的指针,导致该内存无法被释放,一直占用着内存,产生内存泄漏。
  • 内存不合理运用:苹果官方称这种情况为 abandoned memory ,也就是存在已分配内存的引用,但实际上程序中并不会使用,比如图片等对象进行了缓存,但是缓存中的对象一直没有被使用。

Xcode 提供的 Instruments 中的 Allocation 工具可以用来帮你了解内存的分配情况,当你的 App 收到内存警告时,首先应该用Allocation 进行内存分析,了解哪些对象占用了太多内存。

1.4 干掉僵尸对象 – Zombies

僵尸对象,也就是我们会遇到的 EXC_BAD_ACCESS 错误,由于内存已经被释放,而这个对象仍旧保留这那个坏地址而导致的。
MRC 的开发中,这个错误比较常见,ARC 下面在使用到 C++ 的代码也会遇到。
不过这个工具比较简单,遇到这类错误,打开这个位于 Instruments 下的工具,直接就能帮你定位到,这里就不赘述了。

1.5 性能提升工具 – Time Profile

既然是性能调优,那么怎么提升代码的运行效率其实才是我们程序员最直接的诉求,而这个工具可以辅助我们办到这件事

Time Profiler 分析原理:
它按照固定的时间间隔来跟踪每一个线程的堆栈信息,通过统计比较时间间隔之间的堆栈状态,来推算某个方法执行了多久,并获得一个近似值。它将各个方法消耗的时间统计起来,形成了我们直接定位需要进行优化的代码的好帮手。

选择 Time Profiler 工具开始测试,这时会自动启动模拟器和 Time Profiler 录制。

  1. 先进行一些 App 的操作,让 Time Profiler 收集足够的数据,尤其是你觉得那些有性能瓶颈的地方。
  2. 是扩展面板,用来跟踪显示堆栈;
  3. 里面有设置和详情,可以从这里对录制做些配置, detail 下查看到 cpu 运行的时间都消耗在哪里;

    通过对应用的操作,可以在详细面板中看到那些最耗时的操作是哪些,并可以逐行展开查看:

图标为黑色头像的就是 Time Profiler 给我们的提示,有可能存在性能瓶颈的地方,可以逐渐向下展开,找到产生的根本原因。

Time Profiler 参数设置

这里边几个选项的含义如下:

  • Separate by Thread : 每个线程应该分开考虑。只有这样你才能揪出那些大量占用 CPU 的”重”线程
  • Invert Call Tree : 从上倒下跟踪堆栈,这意味着你看到的表中的方法,将已从第 0 帧开始取样,这通常你是想要的,只有这样你才能看到 CPU 中话费时间最深的方法.也就是说 FuncA{FunB{FunC}} 勾选此项后堆栈以 C->B-A 把调用层级最深的 C 显示在最外面
  • Hide System Libraries : 勾选此项你会显示你 app 的代码,这是非常有用的. 因为通常你只关心 cpu 花在自己代码上的时间不是系统上的
  • Flatten Recursion : 递归函数, 每个堆栈跟踪一个条目
  • Top Functions : 一个函数花费的时间直接在该函数中的总和,以及在函数调用该函数所花费的时间的总时间。因此,如果函数 A 调用 B ,那么 A 的时间报告在 A 花费的时间加上 B 花费的时间,这非常真有用,因为它可以让你每次下到调用堆栈时挑最大的时间数字,归零在你最耗时的方法。

上面的参数在实践中合理设置,也没有什么太多技巧,就是通过数据的隐藏、显示让我们更关注于想找到的数据。

2、性能调优之代码优化

2.1 views 设置为不透明 opaque=YES

opaque 这个属性给渲染系统提供了一个如何处理这个 view 的提示。如果设为 YES, 渲染系统就认为这个view是完全不透明的,这使得渲染系统优化一些渲染过程和提高性能。
如果设置为NO,渲染系统正常地和其它内容组成这个view。默认值是YES。
给你们看个图,你们就知道了,如果这个属性为NO,那么:


注:

2.2 正确使用容器的特性
  • Arrays: 有序的一组值,允许有重复值。使用 index 来查找很快,使用 value 查找很慢, 插入/删除很慢。
  • Dictionaries : 存储键值对。 用键来查找比较快。
  • Sets: 无序的一组值,不允许有重复值。用值来查找很快,插入/删除很快。
2.3 提前调整 ImageView 中的图片大小(同图片和动画的渲染)

如果要在 UIImageView 中显示一个图片,你应保证图片的大小和 UIImageView 的大小相同。
因为在运行中缩放图片是很耗费资源的,特别是 UIImageView 嵌套在 UIScrollView 中的情况下。
如果图片是从远端服务加载的,则你不能控制图片大小,如果在下载前不能将图片调整到合适大小的话,你可以在下载完成后,最好是用子线程缩放一次,然后在 UIImageView 中使用缩放后的图片。这个类比到图片和动画的渲染中,是通用的。
具体方法参考上面的 GCD 操作。

2.4 重用和延迟加载(lazy load) Views

更多的view意味着更多的渲染,也就是更多的CPU和内存消耗,对于那种嵌套了很多view在UIScrollView里边的app更是如此。
这里我们用到的技巧就是模仿UITableView和UICollectionView的操作: 不要一次创建所有的subview,而是当需要时才创建,当它们完成了使命,把他们放进一个可重用的队列中。
这样的话你就只需要在滚动发生时创建你的views,避免了不划算的内存分配。
创建views的能效问题也适用于你app的其它方面。想象一下一个用户点击一个按钮的时候需要呈现一个view的场景。有两种实现方法:

  1. 创建并隐藏这个view当这个screen加载的时候,当需要时显示它;
  2. 当需要时才创建并展示。

每个方案都有其优缺点。用第一种方案的话因为你需要一开始就创建一个view并保持它直到不再使用,这就会更加消耗内存。然而这也会使你的app操作更敏感因为当用户点击按钮的时候它只需要改变一下这个view的可见性。第二种方案则相反-消耗更少内存,但是会在点击按钮的时候比第一种稍显卡顿。

注:懒加载在cell的运用

cell 里面嵌套太多的 view,会非常影响滑动的流畅感,而且更多的 view 也需要花费更多的 CPU 跟内存。假如由于 view 太多而导致了滑动不流畅,那就不要一次就把所有的 view 都创建出来,把部分 view 放到需要显示 cell 的时候再去创建。

2.5 重用大的开销对象

这里的大开销是指一些初始化很慢的对象,如:NSDateFormatter和NSCalendar。但是,你又不可避免地需要使用它们,比如从JSON或者XML中解析数据。

想要避免使用这个对象的瓶颈你就需要重用他们,可以通过添加属性到你的class里或者创建静态变量来实现。

注意如果你要选择第二种方法,对象会在你的app运行时一直存在于内存中,和单例(singleton)很相似。

下面的代码说明了使用一个属性来延迟加载一个date formatter. 第一次调用时它会创建一个新的实例,以后的调用则将返回已经创建的实例:

// in your .h or inside a class extension

@property (nonatomic, strong) NSDateFormatter *formatter;
 
// inside the implementation (.m)
// When you need, just use self.formatter
- (NSDateFormatter *)formatter {
    if (! _formatter) {
        _formatter = [[NSDateFormatter alloc] init];
        _formatter.dateFormat = @"EEE MMM dd HH:mm:ss Z yyyy"; // twitter date format
    }
    return _formatter;
}

还需要注意的是,其实设置一个NSDateFormatter的速度差不多是和创建新的一样慢的!所以如果你的app需要经常进行日期格式处理的话,你会从这个方法中得到不小的性能提升。

2.6 排查内存泄漏

目前开发基本上都使用的 ARC,不太需要去关注内存的创建和释放这块,但假如使用的是 MRC,并且跟其它语言混杂在一起(比如。c++lua )等的时候,如何确保内存正确释放就是你需要考虑的问题了。有时候一些内存泄漏 instruments 可能无法准确的分析出来,那么就需要自己去排查了,可以使用 method swizzling 方法来辅助我们排查内存泄漏的问题,确保程序的正确运行。

2.7 正确设定背景图片

View 里放背景图片就像很多其它 iOS 编程一样有很多方法:

  1. 使用 UIColorcolorWithPatternImage 来设置背景色;
  2. view 中添加一个 UIImageView 作为一个子 View

如果你使用全画幅的背景图,你就必须使用 UIImageView ,因为 UIColorcolorWithPatternImage 是用来创建小的重复的图片作为背景的。这种情形下使用 UIImageView 可以节约不少的内存:

// You could also achieve the same result in Interface Builder
UIImageView *backgroundView = [ [UIImageView alloc] initWithImage:[UIImage imageNamed:@"background"]];
[self.view addSubview:backgroundView];

如果你用小图平铺来创建背景,你就需要用 UIColorcolorWithPatternImage 来做了,它会更快地渲染也不会花费很多内存:

self.view.backgroundColor = 
[UIColor colorWithPatternImage:[UIImage imageNamed:@"background"]];
2.8 针对原生和 h5 混合的项目的优化

可以把 h5 所有的页面压缩成一个 zip 包放到服务器,项目第一次启动时去下载这个 zip 包,然后解压到本地沙盒,后续原生加载 h5 界面直接去沙盒加载就可以了,涉及到 h5 zip包的更新,可以留言或私我。

2.9 处理内存警告

一旦系统内存过低,iOS 会通知所有运行中 app 。在官方文档中是这样记述:

如果你的app收到了内存警告,它就需要尽可能释放更多的内存。最佳方式是移除对缓存,图片object和其他一些可以重创建的objects的strong references.

幸运的是,UIKit提供了几种收集低内存警告的方法:

AppDelegate 中使用 applicationDidReceiveMemoryWarning: 的方法 在你的自定义 UIViewController 的子类中覆盖 didReceiveMemoryWarning 注册并接收 UIApplicationDidReceiveMemoryWarningNotification 的通知,一旦收到这类通知,你就需要释放任何不必要的内存使用。

例如, UIViewController 的默认行为是移除一些不可见的 view , 它的一些子类则可以补充这个方法,删掉一些额外的数据结构。一个有图片缓存的 app 可以移除不在屏幕上显示的图片。

这样对内存警报的处理是很必要的,若不重视,你的 app 就可能被系统杀掉。

然而,当你一定要确认你所选择的 object 是可以被重现创建的来释放内存。一定要在开发中用模拟器中的内存提醒模拟去测试一下。

2.10 解决在CDN上放资源文件有缓存的问题

CDN上已存在的资源,如果是替换,会存在缓存问题。

解决办法:
在url链接后拼个时间戳,比如:

http://xxx/loan.json?1548210395
2.11 避免过于庞大的XIB

当你加载一个 xib 的时候所有内容都被放在了内存里,包括任何图片。如果有一个不会即刻用到的 view ,你这就是在浪费宝贵的内存资源了。

Storyboards 就是另一码事儿了,storyboard 仅在需要时实例化一个 view controller .

当加载 xib 时,所有图片都被缓存,如果你在做 OS X 开发的话,声音文件也是。 Apple在相关文档中的记述是:

当你加载一个引用了图片或者声音资源的 nib 时,nib 加载代码会把图片和声音文件写进内存。在 OS X 中,图片和声音资源被缓存在 named cache 中以便将来用到时获取。在 iOS 中,仅图片资源会被存进 named caches 。取决于你所在的平台,使用 NSImageUIImageimageNamed: 方法来获取图片资源。

很明显,同样的事情也发生在 storyboards 中,但我并没有找到任何支持这个结论的文档。

另外,快速打开 app 是很重要的,特别是用户第一次打开它时,对 app 来讲,第一印象太太太重要了。

你能做的就是使它尽可能做更多的异步任务,比如加载远端或者数据库数据,解析数据。

还是那句话,避免过于庞大的 xib ,因为他们是在主线程上加载的。所以尽量使用没有这个问题的 Storyboards 吧!

注意

  • Xcode debugwatchdog 并不运行,一定要把设备从 Xcode 断开来测试启动速度。
  • watchdog(即 iOS 看门狗机制),具体可以参看我的文章:iOS watchdog (看门狗机制)
2.12 选择是否缓存图片

常见的从 bundle 中加载图片的方式有两种,一个是用 imageNamed ,二是用 imageWithContentsOfFile ,第一种比较常见一点。
既然有两种类似的方法来实现相同的目的,那么他们之间的差别是什么呢?
imageNamed 的优点是当加载时会缓存图片。imageNamed 的文档中这么说:
这个方法用一个指定的名字在系统缓存中查找并返回一个图片对象如果它存在的话。如果缓存中没有找到相应的图片,这个方法从指定的文档中加载然后缓存并返回这个对象。
相反的,imageWithContentsOfFile 仅加载图片。
下面的代码说明了这两种方法的用法:

UIImage *img = [UIImage imageNamed:@"myImage"];

UIImage *img = [UIImage imageWithContentsOfFile:@"myImage"];

那么我们应该如何选择呢?
如果你要加载一个大图片而且是一次性使用,那么就没必要缓存这个图片,用 imageWithContentsOfFile足矣,这样不会浪费内存来缓存它。
然而,在图片反复重用的情况下 imageNamed 是一个好得多的选择。

2.13 尽量避免日期格式的转换

如果你要用NSDateFormatter来处理很多日期格式,应该小心以待。就像先前提到的,任何时候重用NSDateFormatters都是一个好的实践。
然而,如果你需要更多速度,那么直接用 C 是一个好的方案。Sam Soffes有一个不错的 帖子 里面有一些可以用来解析 ISO-8601 日期字符串的代码,简单重写一下就可以拿来用了。
嗯,直接用 C 来搞,看起来不错了,但是你相信吗,我们还有更好的方案!
如果你可以控制你所处理的日期格式,尽量选择 Unix 时间戳。你可以方便地从时间戳转换到 NSDate:

- (NSDate*)dateFromUnixTimestamp:(NSTimeInterval)timestamp {
     return [NSDate dateWithTimeIntervalSince1970:timestamp];
 }

这样会比用 C 来解析日期字符串还快!
需要注意的是,许多 web API 会以微秒的形式返回时间戳,因为这种格式在 javascript 中更方便使用。记住用 dateFromUnixTimestamp 之前除以 1000 就好了。

2.13 cell上显示图片的优化思想

tableviewcell 中展示图片,如果二级页面要用到这个图片或这个图片需要被展示大图,很多时候很多初级的开发者都是要求后台传一个原图,这样话比计较吃内存,有可能还会出现内存警告。
优化:
由于后台传的是原图的 url ,所以导致每次下载图片占用的内存特别大,以至于出现了内存警告。所以以后要在 tableviewcell 上显示网络图片的时候,最好要后台先给缩略图的 url ,点击放大的时候再根据原图的 url 去下载展示原图。二级页面要用到这个图片时同理。

2.X 持续更新中...

Author

如果你有什么建议,可以关注我,直接留言,留言必回。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容