前言
对于每位 iOS 开发者来说,代码性能是个避不开的话题。随着项目的扩大和功能的增多,没经过认真调试和优化的代码,要么任性地卡顿运行,要么低调地崩溃。一般性能测试都是从CPU、内存、响应时间(反应时间)来进行测试和以及后续优化的切入点。Xcode自帶的Instruments 提供了丰富的测试工程性能的工具,本文就为大家带来几个实用的工具使用。Apple关于Instuments的介绍
先来谈谈CPU和GPU
* 在屏幕成像的过程中,CPU和GPU起着至关重要的作用
* CPU( Central Processing Unit, 中央处理器)就是机器的“大脑”,也是布局谋略、发号施令、控制行动的“总司令官”。
* CPU的结构主要包括运算器(ALU, Arithmetic and Logic Unit)、控制单元(CU, Control Unit)、寄存器(Register)、
高速缓存器(Cache)和它们之间通 讯的数据、控制及状态的总线。
* GPU全称为Graphics Processing Unit,中文为图形处理器,就如它的名字一样,
GPU最初是用在个人电脑、工作站、游戏机和一些移动设备(如平板电脑、智能手机等)上运行绘图运算工作的微处理器。
* 为什么GPU特别擅长处理图像数据呢?这是因为图像上的每一个像素点都有被处理的需要,
而且每个像素点处理的过程和方式都十分相似,也就成了GPU的天然温床。
在iOS中是双缓冲机制,有前帧缓存、后帧缓存,即GPU会预先渲染好一帧放入一个缓冲区内(前帧缓存),
让视频控制器读取,当下一帧渲染好后,GPU会直接把视频控制器的指针指向第二个缓冲器(后帧缓存)。
当你视频控制器已经读完一帧,准备读下一帧的时候,GPU会等待显示器的VSync信号发出后,前帧缓存和后帧缓存会瞬间切换,
后帧缓存会变成新的前帧缓存,同时旧的前帧缓存会变成新的后帧缓存。
屏幕成像原理
卡顿产生的原因
在Sync信号到来后,系统图形服务会通过CADisplayLink等机制通知App,App主线程开始在CPU中计算显示内容,
比如视图的创建,布局计算,图片解码,文本绘制等。随后CPU会将计算好的内容提交到GPU去,由GPU进行交换,合成,渲染。
随后GPU会把渲染结果提交到帧缓冲区,等待下一次VSync信号(垂直同步信号)到来时显示到屏幕上。由于垂直同步机制,
如果在一个VSync时间内,CPU或者GPU没有完成内容提交,则那一帧就会被丢弃,等待下一次机会再显示,
而这时显示屏因为没有新的刷新,会保留之前的内容不变。这就造成了卡顿。
* 按照60FPS的刷帧率,每隔16ms就会有一次VSync信号
卡顿优化 -CPU
* 尽量用轻量级的对象,比如用不到事件处理的地方,可以考虑使用CALayer取代UIView
* 不要频繁地调用UIView的相关属性,比如frame、bounds、transform等属性,尽量减少不必要的修改
* 尽量提前计算好布局,在有需要时一次性调整对应的属性,不要多次修改属性
* Autolayout会比直接设置frame消耗更多的CPU资源
* 图片的size最好刚好跟UIImageView的size保持一致
* 控制一下线程的最大并发数量
* 尽量把耗时的操作放到子线程
卡顿优化 -GPU
* 尽量避免短时间内大量图片的显示,尽可能将多张图片合成一张进行显示
* 尽量减少视图数量和层次
* 减少透明的视图(alpha<1),不透明的就设置opaque为YES
* 尽量避免出现离屏渲染
在做性能测试的时候我们需要注意几点:
- 测量,而不是猜测
不光是性能优化,调Bug,修复崩溃等很多工作中,很多人凭借“经验”自信地改动代码以为就能完美解决发现的问题,往往是浪费了很多的时间或者创作出新的Bug或者问题。测量,而不是猜测,才是正确的姿势。
- 真机测试,而不是模拟器
当你开始做一些性能方面的工作时候,一定要在真机上测试,而不是模拟器,模拟器运行在Mac上,然而Mac上的cpu比ios设备要快很多。另外 在使用 Core Animation 时,只有真机才可以测试,模拟器无法测试。
- 如何使用 Instuments
连上真机,如下执行 Profile【快捷键 command+I 】即可
选择某个测试工具界面如下:
点击红色的按钮,会启动真机上的应用,运行应用即可,即可进入测试性能模式。
几个实用的测试工具
- 静态性能检测 Analyze
Analyze主要分析以下四种问题:
1、逻辑错误:访问空指针或未初始化的变量、未使用的变量等;
2、内存管理错误:如内存泄漏等;
3、声明错误:从未使用过的变量;
4、Api调用错误:未包含使用的库和框架。
最简单、轻便的内存检测方式。
-
Allocations:监测内存使用 / 分配情况
迅速膨胀的内存可以很快让程序毙命,所以要多加防范。
管理内存是app开发中最重要的一个方面,对于开发者来说,在程序架构中减少内存的使用通常都是使用Allocations去定位和找出减少内存使用的方式,接下来谈一下内存泄漏的两种情况
第一种:为对象A申请了内存空间,之后再也没用过对象A,也没释放过A导致内存泄漏,这种是Leaked Memory内存泄漏。第二种:类似于递归,不断地申请内存空间导致的内存泄漏,这种情况是Abandoned Momory此工具可以让开发者很好的了解每个方法占用内存的情况,并定位相关的代码
右键就可以打开Xcode自动定位到相关占用内存方法的代码上
第二种情况可以根据下图的操作清晰的找到对用的代码问题
解释一下,第二种情况我们应该如何操作,重复的执行一系列的操作时候内存不会继续增加,比如打开和关闭一个窗口,这样的操作,每一次操作的前后,内存应该是相同的,通过多次循环操作,内存不会递增下去,通过这种分析结果,观察内存分配趋势,当发现不正确的结果或者矛盾的结果,就可以研究是不是Abandoned Momory的问题,并可以修正这个问题了。
</br>
我在测试一个地图相关的项目时,进入一个有地图的页面后,再次返回,激增的内存并没有降到原来的水平,于是看得出代码造成中没有释放地图的内存。
#查阅资料了解到removeFromSuperview可以释放内存,并且需要把对象指针置空,这样对应的对象才会被释放,
#以下代码缺一不可。
- (void)viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:YES];
[myMapView removeFromSuperview];
myMapView = nil;
self.walkManager = nil;
}
-
Core Animation
理想的FPS值为60左右,过低的话就用该进性优化了,根据WWDC的说法,当FPS 低于45的时候,用户就会察觉到到滑动有卡顿
圈着数字红色方框中的数字,代表着FPS值,理论上60最佳,实际过程中59就可以了,说明就是很流畅的,说明一下操作方式:在手指不离开屏幕的情况下,上下滑动屏幕列表介绍一下Deug Display中选项的作用
Color Blended Layers(混合过度绘制)
打开此选项屏幕的效果图如下:
这个选项基于渲染程度对屏幕中的混合区域进行绿到红的高亮(也就是多个半透明图层的叠加),由于重绘的原因,混合对GPU性能会有影响,同时也是滑动或者动画掉帧的罪魁祸首之一GPU每一帧的绘制的像素有最大限制,这个情况下可以轻易绘制整个屏幕的像素,但如果发生重叠像素的关系需要不停的重绘同一区域的,掉帧和卡顿就有可能发生GPU会放弃绘制那些完全被其他图层遮挡的像素,但是要计算出一个图层是否被遮挡也是相当复杂并且会消耗CPU的资源,同样,合并不同图层的透明重叠元素消耗的资源也很大,所以,为了快速处理,一般不要使用透明图层,1). 给View添加一个固定、不透明的颜色2). 设置opaque 属性为true但是这对性能调优的帮助并不大,因为UIView的opaque 属性默认为true,也就是说,只要不是认为设置成透明,都不会出现图层混合而对于UIIimageView来说,不仅需要自身需要不是透明的,它的图片也不能含有alpha通道,这也上图9张图片是绿色的原因,因此图像自身的性质也可能会对结果有影响,所以你确定自己的代码没问题,还出现了混合图层可能就是图片的问题了而针对于屏幕中的文字高亮成红色,是因为一没有给文字的label增加不透明的背景颜色,而是当UILabel内容为中文时,label的实际渲染区域要大于label的size,因为外围有了一圈的阴影,才会出现图层混合我们需要给中文的label加上如下代码:
retweededTextLab?.layer.masksToBounds = true
retweededTextLab?.backgroundColor = UIColor.groupTableViewBackground
statusLab.layer.masksToBounds = true
statusLab.backgroundColor = UIColor.white
看下效果图:
那些label的颜色也变成蓝色的了,这里有一点需要说明一下,
(1). statusLab.layer.masksToBounds = true 单独使用不会出现离屏渲染
(2). 如果对label设置了圆角的话,圆角部分会离屏渲染,离屏渲染的前提是位图发生了形变
Color Hits Green and Misses Red(光栅化缓存图层的命中情况)
这个选项主要是检测我们有无滥用或正确使用layer的shouldRasterize属性.成功被缓存的layer会标注为绿色,没有成功缓存的会标注为红色。很多视图Layer由于Shadow、Mask和Gradient等原因渲染很高,因此UIKit提供了API用于缓存这些Layer,self.layer.shouldRasterize = true系统会将这些Layer缓存成Bitmap位图供渲染使用,如果失效时便丢弃这些Bitmap重新生成。图层Rasterization栅格化好处是对刷新率影响较小,坏处是删格化处理后的Bitmap缓存需要占用内存,而且当图层需要缩放时,要对删格化后的Bitmap做额外计算。 使用这个选项后时,如果Rasterized的Layer失效,便会标注为红色,如果有效标注为绿色。当测试的应用频繁闪现出红色标注图层时,表明对图层做的Rasterization作用不大。在测试的过程中,第一次加载时,开启光栅化的layer会显示为红色,这是很正常的,因为还没有缓存成功。但是如果在接下来的测试,。例如我们来回滚动TableView时,我们仍然发现有许多红色区域,那就需要谨慎对待了。
- Leaks:找到引发内存泄漏的起点
一个灰常重要的工具,主要检查内存泄漏,在前面Allcations里面我们提到内存泄漏分两种,现在我们研究Leaked Memory, 从用户使用角度来看,内存泄漏本身不会产生什么危害,作为用户,根本感觉不到内存泄漏的存在,真正的危害在于内存泄漏的堆积,最终会耗尽系统所有的内存。我们直接看图:
在 instruments 中,虽然选择了 Leaks 模板,但默认情况下也会添加 Allocations 模板.基本上凡是内存分析都会使用 Allocations 模板, 它可以监控内存分布情况。选中 Allocations 模板3区域会显示随着时间的变化内存使用的折线图,同时在4区域会显示内存使用的详细信息,以及对象分配情况.点击 Leaks 模板, 可以查看内存泄露情况。如果在3区域有 红X 出现, 则有内存泄露, 4区域则会显示泄露的对象.打用leaks进行监测:点击泄露对象可以在(下图)看到它们的内存地址, 占用字节, 所属框架和响应方法等信息.打开扩展视图, 可以看到右边的跟踪堆栈信息,4 黑色代码最有可能出现内存泄漏的方法。
-
Time Profiler:分析代码的执行时间,找出导致程序变慢的原因。
右侧选项的意义
Separate by Thread: 按线程分开做分析,这样更容易揪出那些吃资源的问题线程。特别是对于主线程,它要处理和渲染所有的接口数据,一旦受到阻塞,程序必然卡顿或停止响应。
Invert Call Tree:反向输出调用树。把调用层级最深的方法显示在最上面,更容易找到最耗时的操作。
Hide Missing Symbols:隐藏缺失符号。如果 dSYM 文件或其他系统架构缺失,列表中会出现很多奇怪的十六进制的数值,用此选项把这些干扰元素屏蔽掉,让列表回归清爽。
Hide System Libraries:隐藏系统库文件。过滤掉各种系统调用,只显示自己的代码调用。
Flattern Recursion:拼合递归。将同一递归函数产生的多条堆栈(因为递归函数会调用自己)合并为一条。
Top Functions:找到最耗时的函数或方法。
在开发的过程中,我们经常能感觉到,点击某一按钮,或者做了某一操作,有卡顿,这就是延迟,那使用此工具,就可以揪出耗时的函数,先看一下,调试界面介绍:
根据查看的相关耗时操作,我们就可以右键定位当耗时的方法:
下图是我的一个测量结果,可以看到有在主线程中有个耗时5S的操作,通过层层查找我们最后找到了这个耗时巨大的方法在 TwoViewContorller ViewDidLoad 中,右键 Reveal in Xcode 中我们可以看到,是我故意写的一个主线程中的for循环。
- (void)viewDidLoad {
[super viewDidLoad];
#找到这个耗时巨大的方法所在
for (int i = 0; i<50000; i++) {
NSLog(@"YYYY" );
}
}
-
UIKit性能调优实战讲解:
fps表示frames per second,也就是每秒钟显示多少帧画面。对于静止不变的内容,我们不需要考虑它的刷新率,但在执行动画或滑动时,fps的值直接反映出滑动的流畅程度.
个人认为比opaque属性更重要的是backgroundColor属性,如果不设置这个属性,控件依然被认为是透明的,所以我们做的第一个优化是 设置控件的背景颜色。
小结
APP性能调试在App的开发中是很重要的,后续有新的收获或者新的方法用到,会持续更新的。
本文参考文章
iOS 性能调优,成为一名合格iOS程序员必须掌握的技能