iOS

【OC梳理】性能检测及优化汇总

2018-10-19  本文已影响46人  忠橙_g

启动时间

启动时间可谓是用户对你的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通常用于检测常见的三种泄露情形:

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 保持界面流畅的技巧

检测方式


页面渲染时间

对于静态页面来讲,页面的渲染时间就是从viewDidLoad第一行到viewDidAppear最后一行代码的时间。但是大多数页面是需要网络请求回数据才能正常展示,因此优化的方向主要有两个:


刷新帧率

刷新帧率可以通过Instrument里的Core Animation查看,也可以使用CADisplayLink(已经有许多现成的封装控件,代码也很简单),它是一个以和屏幕刷新率相同的频率将内容画到屏幕上的定时器,最快能每秒调用60次,在正常情况下会在每次刷新结束都被调用,精确度相当高。如果是CPU或是GPU某个步骤耗时导致渲染错过了一次垂直信号,那这个方法就不会被调用了,之后统计的帧数也就随之降低了。

理想的FPS值为60左右,过低的话就用该进性优化了,根据WWDC的说法,当FPS 低于45的时候,用户就会察觉到到滑动有卡顿。

Core Animation(必须使用真机)

运行Instrument中的Core Animation,可以录制运行过程中的帧率:

左侧红框中的数字,代表着FPS值,理论上60最佳。
以下是右侧框中,Debug Options中选项的作用:

发生离屏渲染的可能有:

/* 圆角处理 */
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

模拟器中可以直接设置上面的某些选项:


网络请求时间,流量消耗

大部分的网络请求都是通过HTTP完成,使用成熟的第三方库诸如AFNetworking很容易搭建一个功能简易的网络模块。
对于网络模块,通用的优化方面大致如下:

第一类:关键核心的业务数据,期望能100%送达服务器。
第二类:重要内容请求,需要较高的请求成功率。
第三类:一般性内容请求,对成功率无要求。

之所以要将请求分为三类,是要在可靠性保障上做区分。理论上我们应该尽可能让所有的请求成功率达到最高,但客户端的流量,带宽,手机电量,服务器的压力等都是有限的资源,所以我们采取的策略是只对关键性的网络请求做高强度的可靠性保障。

第一类请求类似大家用微信时发送的消息,消息数据一旦从输入框发出,从用户来的角度感知这个消息数据是一定会到达对方的。如果网络环境差,网络模块会自动在后头悄悄重试,一段时间后仍无法成功就通过产品交互的方式告知用户发送失败了,即使失败,请求的数据(消息本身)一直存在客户端。
对于这类请求的处理方式第一步不是通过网络发送,而是持久化到Database当中。一旦入了DB,即使断网,断电,重启,请求数据依然还在,只需在App重启的时候还原请求数据,再次发送即可。

如果判断为第一类请求,第一步我们先将请求持久化到DB当中。
第二步发送请求,如果请求失败则将请求加入重试队列,成功则从重试队列中移除。重试队列背后也需要一套通用机制,比如多久重试一次,重试几次之后放弃。
遇到最恶劣的场景,请求发送失败之后,App被kill。我们需要在App重启之后从DB当中重新load所有失败的请求再次重试。

第二类请求的例子可以是我们App启动时用户看到的首页,首页的内容从服务器获取,如果第一次请求就失败体验较差,这种场景下我们应该允许请求有机会多试几次,增加一个retryCount即可。一般3次的重试基本可以排除网络抖动的情况。三次失败之后即可认为请求失败,通过产品交互告知用户。

第三类请求的重要性最低,比如进入Controller的UV采集打点。这类请求只需要做一次,即使失败也不会对产品体验产生什么负面影响。


UI阻塞次数,不可操作时长,主线程阻塞超过400毫秒次数

主线程阻塞超过400毫秒就会让用户感知到卡顿,跟用户交互的操作如渲染,管理触摸反应,回应输入等都是在主线程的,所以不要让主线程承担过多耗时操作,耗时操作放到子线程中进行。

Time Profiler

使用此工具,就可以揪出耗时的函数,调试界面介绍:

同样,可以右键打开定位到方法的代码:


耗电功率

耗电功率是个比较综合的指标,影响因素很多。跟性能相关的,密集的网络请求,长链接,密集的CPU操作(比如大量的复杂计算)都会使耗电功率增加。此外,耗电量还会被很多其他因素影响,比如用户在不同光线下使用,iPhone会自动调整屏幕亮度,就会导致耗电量不同;网络状况(流畅的Wi-Fi还是信号不好的3G),由于耗电量的影响因素太多,统计出来并不能精准的反应你的APP的性能,一般的APP不必把耗电量当作一个优化指标,只要把可能影响耗电量的、可优化的部分尽量优化即可,比如网络请求和CPU操作。
测量耗电量的时候不能用模拟器,模拟器下得到的电量值是负数,也不能用真机连着电脑debug,因为这个过程本身就在给手机充电。正确的做法是在手机上设置:设置–>开发者–>logging–>energy点击Start Recording,一段时间以后再stop,再用手机连接到电脑的instrument上,使用import log记录进行分析。


Tips


参考文章

iOS APP 性能检测
iOS App从点击到启动
深度优化iOS网络模块

上一篇下一篇

猜你喜欢

热点阅读