【OC梳理】性能检测及优化汇总
启动时间
启动时间可谓是用户对你的APP的第一印象,启动时间过长很可能会让用户直接把APP打入冷宫。苹果的watch dog机制(Xcode在debug模式下是没有开启watch dog的)也会kill掉启动时间过长的APP,这种情况下给用户的感觉就是这APP怎么一启动就卡死然后崩溃了。
首先大概了解一下APP的启动过程:
app启动过程我们计算的启动时间就是从main()
到applicationDidBecomeActive:
的时间。
在Xcode的Edit scheme中增加DYLD_PRINT_STATISTICS这个环境变量,设置值为YES,如下图所示:
运行项目后在控制台会打印出每个阶段都耗时多少:
main()调用之后的加载时间:
1.准备阶段,主要是图片的解码
2.布局阶段,-(void)layoutSubViews()
3.绘制阶段,-(void)drawRect:(CGRect)rect
4.启动阶段必要服务的启动、必要数据的创建和读取。
优化启动时间
1.内嵌的dylib(tbd)尽可能少,或者合并起来。
2.Rebase/Binding减少__DATA中需要修正的指针。 对于oc来说减少 class, selector, category 这些元数据的数量,对与c++来说,减少虚函数数量。swift结构体需要修正的比较少。
3.将不必须在+load中做的事延迟到+ initialize中。
4.不使用xib,直接用代码加载首页视图。
5.release版不要用NSLog输出。
6.启动时的网络请求尽可能异步。
内存占用量,内存告警次数
我们知道,一个进程占用的内存空间,包含5种不同的数据区:
BSS段:通常是存放未初始化的全局变量;
数据段:通常是存放已初始化的全局变量。
代码段:通常是存放程序执行代码。
堆:通常是用于存放进程运行中被动态分配的内存段,OC对象(所有继承自NSObject的对象)就存放在堆里。
栈:由编译器自动分配释放,存放函数的参数值,局部变量等值。
栈内存是系统来管理的,因此我们常说的内存管理,指的是堆内存的管理,也就是所有OC对象的创建和销毁的管理。
在iOS应用中的内存泄露,原因一般有循环引用、错用Strong/copy等。
检测方法
Analyze — 静态分析
使用command + shift + B
编译项目,或者点击Xcode - Product - Analyze
即可使用。
Analyze通常用于检测常见的三种泄露情形:
- 创建了一个对象,但是并没有使用。Xcode提示信息:
Value Stored to 'XXX' is never read。
- 创建了一个(指针可变的)对象,且初始化了,但是初始化的值一直没读取过。Xcode提示信息:
Value Stored to 'str' during its initialization is never read
- 调用了让某个对象引用计数加1的函数,但没有调用相应让其引用计数减1的函数。Xcode提示信息:
Potential leak of an object stored into 'subImageRef'
。
Leaks — 内存泄露
Leaks是动态的内存泄露检查工具,需要一边运行程序,一边检测。
使用command + control + I
调出Instruments,然后选择Leaks
即可调出Leaks界面:
Allocations — 内存分配
Allocations是检测程序运行过程中的内存分配情况的,也在Instruments工具列表中,界面如下:
右键就可以打开Xcode自动定位到相关占用内存方法的代码上,根据这些信息,可以对程序里不同代码的内存占用情况有一些认识,并进行针对性的优化。
重复的执行一系列的操作时候内存不会继续增加,比如打开和关闭一个窗口,这样的操作,每一次操作的前后,内存应该是相同的,通过多次循环操作,内存不会递增下去,通过这种分析结果,观察内存分配趋势,当发现不正确的结果或者矛盾的结果,就可以研究是不是Abandoned Momory的问题,并可以修正这个问题了:
Zombies — 僵尸对象
Zombies是检测僵尸对象的工具,也在Instruments,用于定位僵尸对象导致的崩溃:
开启Zombies并运行,直至崩溃,如果是僵尸对象导致,则可以通过Zombies定位到崩溃的位置:
CPU使用率
影响CPU使用情况的主要是计算密集型的操作,比如动画、布局计算和Autolayout、文本的计算和渲染、图片的解码和绘制。比较常见的一种优化方式就是缓存tableview的cell高度,避免每次计算。想要降低CPU的使用率就要尽量避免大量的计算,能缓存的缓存,不得不计算的,看看是否可以使用一些算法进行优化,降低时间复杂度。
CPU方面的优化还可以参见iOS 保持界面流畅的技巧
检测方式
- 使用Xcode调试程序时可以在Debug Session界面中看到CPU的使用率
- 使用Instruments中的Activity Monitor(必须使用真机)监控进程级别的CPU,内存,磁盘,网络使用情况
页面渲染时间
对于静态页面来讲,页面的渲染时间就是从viewDidLoad第一行到viewDidAppear最后一行代码的时间。但是大多数页面是需要网络请求回数据才能正常展示,因此优化的方向主要有两个:
- 对网络请求进行优化,加快获取数据的速度,如使用多线程等。
- 未获取到数据时,先展示加载动画,可以是加载框,更好的做法是使用一些色块填充数据展示的位置并作出加载动画(实现起来比较费事)。
刷新帧率
刷新帧率可以通过Instrument里的Core Animation查看,也可以使用CADisplayLink(已经有许多现成的封装控件,代码也很简单),它是一个以和屏幕刷新率相同的频率将内容画到屏幕上的定时器,最快能每秒调用60次,在正常情况下会在每次刷新结束都被调用,精确度相当高。如果是CPU或是GPU某个步骤耗时导致渲染错过了一次垂直信号,那这个方法就不会被调用了,之后统计的帧数也就随之降低了。
理想的FPS值为60左右,过低的话就用该进性优化了,根据WWDC的说法,当FPS 低于45的时候,用户就会察觉到到滑动有卡顿。
Core Animation(必须使用真机)
运行Instrument中的Core Animation,可以录制运行过程中的帧率:
左侧红框中的数字,代表着FPS值,理论上60最佳。
以下是右侧框中,Debug Options中选项的作用:
-
Color Blended Layers
(混合过度绘制):这个选项基于渲染程度对屏幕中的混合区域进行绿到红的高亮(也就是多个半透明图层的叠加),由于重绘的原因,混合对GPU性能会有影响,同时也是滑动或者动画掉帧的罪魁祸首之一 -
Color Hits Green and Misses Red
(光栅化缓存图层的命中情况):这个选项主要是检测我们有无滥用或正确使用layer的shouldRasterize属性.成功被缓存的layer会标注为绿色,没有成功缓存的会标注为红色。当测试的应用频繁闪现出红色标注图层时,表明对图层做的Rasterization作用不大。 -
Color Copied Image
(拷贝的图片):这个选项主要检查我们有无使用不正确图片格式,如果图片的格式不正确,则系统将图片转化为像素的时间就有可能变长。 -
Color Immediately
(颜色立即更新):通常 Core Animation 以每秒10次的频率更新图层的调试颜色,对于某些效果来说,这可能太慢了,这个选项可以用来设置每一帧都更新(可能会影响到渲染性能,所以不要一直都设置它) -
Color Misaligned Image
(图片对齐方式):这里会高亮那些被缩放或者拉伸以及没有正确对齐到像素边界的图片,即图片Size和imageView中的Size不匹配,会使图过程片缩放,而缩放会占用CPU。 -
Color Offscreen- Rendered Yellow
(离屏渲染):这里会把那些需要离屏渲染的到图层高亮成黄色。
发生离屏渲染的可能有:
/* 圆角处理 */ view.layer.maskToBounds = truesomeView.clipsToBounds = true /* 设置阴影 */ view.shadow.. /* 栅格化 */ view.layer.shouldRastarize = true
针对栅格化处理,我们需要指定屏幕的分辨率
//离屏渲染 - 异步绘制 耗电 self.layer.drawsAsynchronously = true /**栅格化 - 异步绘制之后 ,会生成一张独立的图片 cell 在屏幕上滚动的时候,本质上滚动的>是这张图片 cell 优化,要尽量减少图层的数量,想当于只有一层 停止滚动之后,可以接受监听**/ self.layer.shouldRasterize = true //使用 “栅格化” 必须指定分辨率 self.layer.rasterizationScale = UIScreen.main.scale
指定阴影的路径,可以防止离屏渲染
// 指定阴影曲线,防止阴影效果带来的离屏渲染 imageView.layer.shadowPath = UIBezierPath(rect: imageView.bounds).cgPath
这行代码制定了阴影路径,如果没有手动指定,Core Animation会去自动计算,这就会触发离屏渲染。如果人为指定了阴影路径,就可以免去计算,从而避免产生离屏渲染。
设置cornerRadius本身并不会导致离屏渲染,但很多时候它还需要配合layer.masksToBounds = true使用。根据之前的总结,设置masksToBounds会导致离屏渲染。解决方案是尽可能在滑动时避免设置圆角,如果必须设置圆角,可以使用光栅化技术将圆角缓存起来:// 设置圆角 label.layer.masksToBounds = true label.layer.cornerRadius = 8 label.layer.shouldRasterize = true label.layer.rasterizationScale = layer.contentsScale
如果界面中有很多控件需要设置圆角,比如tableView中,当tableView有超过25个圆角,使用如下方法
view.layer.cornerRadius = 10 view.maskToBounds = Yes
那么fps将会下降很多,特别是对某些控件还设置了阴影效果,更会加剧界面的卡顿、掉帧现>象,对于不同的控件将采用不同的方法进行处理:
1). 对于label类,可以通过CoreGraphics来画出一个圆角的label
2). 对于imageView,通过CoreGraphics对绘画出来的image进行裁边处理,形成一个圆角的imageView
-
Color Compositing Fast-Path Blue
:这个选项会对任何直接使用OpenGL绘制的图层进行高亮,如果仅仅使用UIKit或者Core Animation的API,不会有任何效果。 -
Flash Updated Regions
(Core Graphics 绘制的图层):此选项会对重绘的内容进行高亮成黄色,也就是软件层面使用Core Graphics 绘制的图层。
模拟器中可以直接设置上面的某些选项:
网络请求时间,流量消耗
大部分的网络请求都是通过HTTP完成,使用成熟的第三方库诸如AFNetworking很容易搭建一个功能简易的网络模块。
对于网络模块,通用的优化方面大致如下:
-
DNS映射
无论是HTTP还是Socket长连接,第一步都是DNS解析。域名根据层级「主机名.次级域名.顶级域名.根域名」去解析,每一级缓存生命周期不同。在iOS设备上几乎每次断网重连,重启设备都会使DNS缓存失效,触发重新查询。这一步的优化对请求的延迟来说至关重要。 -
请求压缩
DNS查询之后是TCP握手建立连接,并发送请求数据。对于TCP来说,单个IP包大小受限于MSS值,大部分用户所处网络环境下每个包的大小约在1.5KB,新建立的HTTP连接由于TCP的slow start特性,会导致本地的部分IP包本临时缓存,从而增加了整体request的延迟。所以我们应该尽可能尝试去压缩我们的网络请求业务数据,减少一个Request的IP包数量,或许可以让用户少经历一个RTT,降低请求延迟的用户感知。 -
请求合并
对于非关键性的业务数据,或者对实时性要求不高的请求来说,通过合并请求的方式可以减少和服务器交互的次数,一则降低服务器压力,二则合并之后再压缩能节约客户端的流量。这类请求一般见于打点SDK,crash日志收集等非业务型请求。 -
请求的安全性
- 仅仅用HTTPS + POST请求提交用户的隐私数据,不能完全解决安全问题,我们还需要对请求的参数进行一定的加密,例如登录:
事先生成一对公私钥(RSA等等),用户在登录时用公钥将用户的密码加密,将密文传输到服务器,服务器使用私钥将密码解密,然后加盐(在密码学中是指通过在密码任意固定位置插入特定的字符串,让散列后的结果和使用原始密码的散列结果不相符)之后再进行MD5等操作,之后再和服务器原来存储的用同样方法处理过的密码匹配,如果一致则登陆成功. - 可以选择类似Protobuf之类的二进制通讯协议或者自己实现通讯协议,对于传输的内容进行一定程度的加密,以增加黑客破解的难度.
- 应用内支付的安全问题,客户端在拿到凭证后,还需将凭证上传到自己的服务器进行二次验证,以保证真实性(二次验证的过程中也需要考虑返回结果被篡改的可能).
- 仅仅用HTTPS + POST请求提交用户的隐私数据,不能完全解决安全问题,我们还需要对请求的参数进行一定的加密,例如登录:
-
合理的并发数
有些业务场景会出现多个Request集中产生的情况,此时我们需要设置一个合理的并发数。并发数如果太小,会导致“劣质”的请求block住“优质”的请求。如果并发数太大,带宽有限的场景下,会增加请求的整体延迟。 -
可靠性保障
可靠性保障也是个容易被忽视的方面,在深入探讨之前,可以先将Request按业务属性分类:
第一类:关键核心的业务数据,期望能100%送达服务器。
第二类:重要内容请求,需要较高的请求成功率。
第三类:一般性内容请求,对成功率无要求。
之所以要将请求分为三类,是要在可靠性保障上做区分。理论上我们应该尽可能让所有的请求成功率达到最高,但客户端的流量,带宽,手机电量,服务器的压力等都是有限的资源,所以我们采取的策略是只对关键性的网络请求做高强度的可靠性保障。
第一类请求类似大家用微信时发送的消息,消息数据一旦从输入框发出,从用户来的角度感知这个消息数据是一定会到达对方的。如果网络环境差,网络模块会自动在后头悄悄重试,一段时间后仍无法成功就通过产品交互的方式告知用户发送失败了,即使失败,请求的数据(消息本身)一直存在客户端。
对于这类请求的处理方式第一步不是通过网络发送,而是持久化到Database当中。一旦入了DB,即使断网,断电,重启,请求数据依然还在,只需在App重启的时候还原请求数据,再次发送即可。
如果判断为第一类请求,第一步我们先将请求持久化到DB当中。
第二步发送请求,如果请求失败则将请求加入重试队列,成功则从重试队列中移除。重试队列背后也需要一套通用机制,比如多久重试一次,重试几次之后放弃。
遇到最恶劣的场景,请求发送失败之后,App被kill。我们需要在App重启之后从DB当中重新load所有失败的请求再次重试。
第二类请求的例子可以是我们App启动时用户看到的首页,首页的内容从服务器获取,如果第一次请求就失败体验较差,这种场景下我们应该允许请求有机会多试几次,增加一个retryCount即可。一般3次的重试基本可以排除网络抖动的情况。三次失败之后即可认为请求失败,通过产品交互告知用户。
第三类请求的重要性最低,比如进入Controller的UV采集打点。这类请求只需要做一次,即使失败也不会对产品体验产生什么负面影响。
-
多通道
现在不少有技术条件的团队都有自己的tcp长连接通道,技术再硬点的甚至配有UDP通道,UDP在丢包率高的网络环境下能极大的提高请求成功的概率。如果能同时具备HTTP,TCP,UDP三条网络通道,在某些场景下,如果不考虑流量(比如wifi),可以针对某个网络请求,两通道或者三通道齐发,对请求成功的速度和可靠性有明显的疗效,不过客户端和服务器都需要针对业务场景做去重。 -
网络环境监控
现在网络环境虽然越来越好,Wifi,4G,3G在一二线城市都有很好的普及,但还是有不少场景会导致网络状态突然变差,比如进电梯,做火车、地铁,人多的集会场所,从公司回家4G切Wifi等等,这些场景在生活当中并不少见,健壮的网络模块需要仔细的检测网络的变化,针对性的做请求重试。 -
请求成功率监控
网络模块应该能监控当前App的网络请求成功率,对于失败率较高的请求,带上业务数据,手机网络环境,系统参数等等,在用户不活跃的时候能打包上报给server端,一则能找出更多需要优化的业务场景,二则能实时监控server端的健康状态,三则能从数据层面精确判断每一次网络优化是否有成效。
UI阻塞次数,不可操作时长,主线程阻塞超过400毫秒次数
主线程阻塞超过400毫秒就会让用户感知到卡顿,跟用户交互的操作如渲染,管理触摸反应,回应输入等都是在主线程的,所以不要让主线程承担过多耗时操作,耗时操作放到子线程中进行。
Time Profiler
使用此工具,就可以揪出耗时的函数,调试界面介绍:
同样,可以右键打开定位到方法的代码:
耗电功率
耗电功率是个比较综合的指标,影响因素很多。跟性能相关的,密集的网络请求,长链接,密集的CPU操作(比如大量的复杂计算)都会使耗电功率增加。此外,耗电量还会被很多其他因素影响,比如用户在不同光线下使用,iPhone会自动调整屏幕亮度,就会导致耗电量不同;网络状况(流畅的Wi-Fi还是信号不好的3G),由于耗电量的影响因素太多,统计出来并不能精准的反应你的APP的性能,一般的APP不必把耗电量当作一个优化指标,只要把可能影响耗电量的、可优化的部分尽量优化即可,比如网络请求和CPU操作。
测量耗电量的时候不能用模拟器,模拟器下得到的电量值是负数,也不能用真机连着电脑debug,因为这个过程本身就在给手机充电。正确的做法是在手机上设置:设置–>开发者–>logging–>energy
点击Start Recording
,一段时间以后再stop,再用手机连接到电脑的instrument上,使用import log记录进行分析。
Tips
- 做性能方面的检测工作时,一定要在真机上测试,而不是模拟器。模拟器的性能是Mac的,跟iPhone不可同日而语,测出来的数据不准也就没有了意义。
- 性能测试要用发布配置,也就是说要用release包,而不是调试模式。
- 最好在你支持的设备中性能最差的设备上测试。