卡顿优化
在了解卡顿优化相关的前头,首先要了解 CPU 和 GPU。
CPU(Central Processing Unit,中央处理器)
对象的创建和销毁、对象属性的调整、布局计算、文本的计算和排版、图片的格式转换和解码、图像的绘制(Core Graphics)都是通过 CPU 来做的。
GPU(Graphics Processing Unit,图形处理器)
纹理的渲染、
所要显示的信息一般是通过 CPU 计算或者解码,经过 CPU 的数据交给 GPU 渲染,渲染的工作在帧缓存的地方完成,然后从帧缓存读取数据到视频控制器上,最终显示在屏幕上。
在 iOS 中有双缓存机制,有前帧缓存、后帧缓存,这样渲染的效率很高。
屏幕成像原理
我们所看到的动态的屏幕的成像其实和视频一样也是一帧一帧组成的。为了把显示器的显示过程和系统的视频控制器进行同步,显示器(或者其他硬件)会用硬件时钟产生一系列的定时信号。当电子枪换到新的一行,准备进行扫描时,显示器会发出一个水平同步信号(Horizonal Synchronization),简称 HSync;而当一帧画面绘制完成后,电子枪回复到原位,准备画下一帧前,显示器会发出一个垂直同步信号(Vertical Synchronization),简称 VSync。显示器通常以固定频率进行刷新,这个刷新率就是 VSync 信号产生的频率。
卡顿现象
卡顿成因
前面我们知道,完成显示信息的过程是:CPU 计算数据 -> GPU 进行渲染 -> 屏幕发出 VSync 信号 -> 成像,假如屏幕已经发出了 VSync 但 GPU 还没有渲染完成,则只能将上一次的数据显示出来,以致于当前计算的帧数据丢失,这样就产生了卡顿,当前的帧数据计算好后只能等待下一个周期去渲染。
解决办法
解决卡顿现象的主要思路就是:尽可能减少 CPU 和 GPU 资源的消耗。
按照 60fps 的刷帧率,每隔 16ms 就会有一次 VSync 信号产生。那么针对 CPU 和 GPU 有以下优化方案:
CPU
- 尽量用轻量级的对象 如:不用处理事件的 UI 控件可以考虑使用 CALayer;
-
不要频繁地调用
UIView
的相关属性 如:frame、bounds、transform 等; - 尽量提前计算好布局,在有需要的时候一次性调整对应属性,不要多次修改;
Autolayout
会比直接设置 frame 消耗更多的 CPU 资源;- 图片的 size 和 UIImageView 的 size 保持一致;
- 控制线程的最大并发数量;
- 耗时操作放入子线程;如文本的尺寸计算、绘制,图片的解码、绘制等;
GPU
- 尽量避免短时间内大量图片显示;
- GPU 能处理的最大纹理尺寸是 4096 * 4096,超过这个尺寸就会占用 CPU 资源,所以纹理不能超过这个尺寸;
- 尽量减少透视图的数量和层次;
- 减少透明的视图(alpha < 1),不透明的就设置 opaque 为 YES;
- 尽量避免离屏渲染;
离屏渲染
在 OpenGL 中,GPU 有两种渲染方式:
On-Screen Rendering:当前屏幕渲染,在当前用于显示的屏幕缓冲区进行渲染操作;
Off-Screen Rendering:离屏渲染,在当前屏幕缓冲区外开辟新的缓冲区进行渲染操作;
离屏渲染消耗性能的原因:
离屏渲染的整个过程,需要多次切换上下文环境,先是从当前屏幕(On-Screen)切换到离屏(Off-Screen),渲染结束后,将离屏缓冲区的渲染结果显示到屏幕上,上下文环境从离屏切换到当前屏幕,这个过程会造成性能的消耗。
哪些操作会触发离屏渲染?
- 光栅化,
layer.shouldRasterize = YES
- 遮罩,
layer.mask
- 圆角,同时设置
layer.masksToBounds = YES
,layer.cornerRadius > 0
- 可以用 CoreGraphics 绘制裁剪圆角
- 阴影
- 如果设置了
layer.shadowPath
不会产生离屏渲染
- 如果设置了
卡顿检测
这里的卡顿检测主要是针对在主线程执行了耗时的操作所造成的,这样可以通过 RunLoop 来检测卡顿:添加 Observer 到主线程 RunLoop 中,通过监听 RunLoop 状态的切换的耗时,达到监控卡顿的目的。
耗电优化
耗电的主要来源为:
- CPU 处理;
- 网络请求;
- 定位;
- 图像渲染;
优化思路
- 尽可能降低 CPU、GPU 功耗;
- 少用定时器;
- 优化 I/O 操作;
- 尽量不要频繁写入小数据,最好一次性批量写入;
- 读写大量重要数据时,可以用
dispatch_io
,它提供了基于 GCD 的异步操作文件的 API,使用该 API 会优化磁盘访问; - 数据量大时,用数据库管理数据;
- 网络优化;
- 减少、压缩网络数据(JSON 比 XML 文件性能更高);
- 若多次网络请求结果相同,尽量使用缓存;
- 使用断点续传,否则网络不稳定时可能多次传输相同的内容;
- 网络不可用时,不进行网络请求;
- 让用户可以取消长时间运行或者速度很慢的网络操作,设置合适的超时时间;
- 批量传输,如下载视频,不要传输很小的数据包,直接下载整个文件或者大块下载,然后慢慢展示;
- 定位优化;
- 如果只是需要快速确定用户位置,用
CLLocationManager
的requestLocation
方法定位,定位完成后,定位硬件会自动断电; - 若不是导航应用,尽量不要实时更新位置,并为完毕就关掉定位服务;
- 尽量降低定位精度,如不要使用精度最高的
KCLLocationAccuracyBest
; - 需要后台定位时,尽量设置
pausesLocationUpdatesAutomatically
为 YES,若用户不怎么移动的时候,系统会自暂停位置更新;
- 如果只是需要快速确定用户位置,用
启动优化
App 的启动分为两种:冷启动(Cold Launch) 和热启动(Warm Launch)。
前者表示从零开始启动 App,后者表示 App 已经存在内存中,在后台依然活着,再次点击图标启动 App。
App 启动的优化主要是针对冷启动的优化,通过添加环境变量可以打印出 App 的启动时间分析:Edit Scheme -> Run -> Arguments -> Environment Variables 添加 DYLD_PRINT_STATISTICS
设置为 1。
运行程序则会打印:
这里打印的是在执行 main
函数之前的耗时信息,若想打印更详细的信息则添加环境变量为:
DYLD_PRINT_STATISTICS_DETAILS
设置为 1。
App 冷启动
冷启动可分为三个阶段:dyld 阶段、Runtime 阶段、main 阶段。
第一个阶段就是处理程序的镜像的阶段,第二个阶段是加载本程序的类、分类信息等等的 Runtime 阶段,最后是调用 main 函数阶段。
dyld
dyld(Dynamic Link Editor),Apple 的动态链接器,可以用来装载 Mach-O 文件(可执行文件、动态库等)。
启动 App 时,dyld 会装载 App 的可执行文件,同时会递归加载所有依赖的动态库,当 dyld 把可执行文件、动态库都装载完毕后,会通知 Runtime 进行做下一步的处理。
Runtime
启动 App 时,调用 map_images
进行可执行文件的内容解析和处理,再 load_images
中调用 call_load_methods
调用所有 Class 和 Category 的 load
方法,然后进行 objc 结构的初始化(注册类、初始化类对象等)。然后调用 C++ 静态初始化器和 __attribute_((constructor))
修饰的函数,到此为止,可执行文件的和动态库中所有的符号(类、协议、方法等)都已经按照格式加载到内存中,被 Runtime 管理。
main
在 Runtime 阶段完成后,dyld 会调用 main 函数,接下来是 UIApplication 函数,AppDelegate 的 application: didFinishLaunchingWithOptions:
函数。
启动优化思路
针对不同的阶段,有不同的优化思路:
dyld
- 减少动态库、合并动态库,定期清理不必要的动态库;
- 减少类、分类的数量,减少 Selector 的数量,定期清理不必要的类、分类;
- 减少 C++ 虚函数数量;
- Swift 开发尽量使用 struct;
虚函数和 Java 中的抽象函数有点类似,但区别是,基类定义的虚函数,子类可以实现也可以不实现,而抽象函数子类一定要实现。
Runtime
- 用
inilialize
方法和dispatch_once
取代所有的__attribute_((constructor))
、C++ 静态构造器、以及 Objective-C 中的load
方法;
main
- 将一些耗时操作延迟执行,不要全部都放在
finishLaunching
方法中;
安装包瘦身
安装包(ipa)主要由可执行文件和资源文件组成,若不管理妥善则会造成安装包体积越来越大,所以针对资源优化我们可以将资源采取无损压缩,去除没用的资源。
对于可执行文件的瘦身,我们可以:
-
从编译器层面优化
- Strip Linked Product、Make Strings Read-Only、Symbols Hidden by Default 设置为 YES;
- 去掉异常支持,Enable C++ Exceptions、Enable Objective-C Exceptions 设置为 NO,Other C Flags 添加
-fno-exceptions
; - 利用 AppCode,检测未使用代码检测:菜单栏 -> Code -> Inspect Code;
- 编写 LLVM 插件检测重复代码、未调用代码;
- 通过生成 LinkMap 文件检测;
LinkMap
Build Setting -> LD_MAP_FILE_PATH: 设置文件路径 ,Build Setting -> LD_GENERSTE_MAP_FILE -> YES
运行程序可看到:
打开可看见各种信息:
我们可根据这个信息针对某个类进行优化。
写的真的很不错