本篇主要解决的问题
- 优化你是从哪几方面着手?
- 在屏幕成像的过程中,CPU 和 GPU 分别负责处理哪些事情?针对界面的流畅度可以做什么优化?
- 如何减少启动时间?
- 安装包如何瘦身?
优化你是从哪几方面着手?
- 内存优化
- 卡顿优化
- 耗电优化
- 降低 CPU、GPU 功耗
- 少用定时器
- 优化 I/O 操作
- 尽量不要频繁写入小数据,最好批量一次性写入
- 读写大量重要数据时,考虑使用
dispatch_io
,其提供了基于 GCD 的异步操作文件 I/O 的 API。用dispatch_io
系统会优化磁盘访问 - 数据量比较大的,建议使用数据库(比如 SQLite、CoreDota)
- 网络优化
- 减少、压缩网络数据,比如 XML 换成 JSON,现在也有公司在 protobuf,比如上传的文件压缩之后再传
- 多次请求结果是相同的,尽量使用缓存,NSCache
- 尽量使用断点续传,避免重复下载
- 网络不可用的时候,不要尝试执行网络请求
- 设置合适的超时时间
- 定位优化
- 硬件检测优化
- 用户移动、摇晃、倾斜设备时,会产生动作(motion)事件,这些事件由加速计、陀螺仪、磁力计等硬件检测。在不需要检测的场合,应该及时关闭这些硬件。
- 启动时间优化
- 安装包大小优化
在屏幕成像的过程中,CPU 和 GPU 分别负责处理哪些事情?针对界面的流畅度可以做什么优化?
屏幕上的任何内容要想显示出来,都需要经过 的处理:
- CPU 计算好数后提交给 GPU
- GPU 对计算好的数据进行渲染,渲染成能显示的数据并后放入
- 器按照 信号逐行读取缓冲区中数据,经过数模转换后传递给显示器显示
双缓冲区机制
为解决效率问题,iOS 使用的是双缓冲区机制。GPU 会先渲染好一帧放入一个缓冲区,供视频控制器读取;GPU 的下一帧渲染好之后会放入另一个缓冲区,这时视频控制器的指针会指向这一个缓冲区。这样做的弊端是:如果视频控制器在读取的过程中(即屏幕只显示了一部分),GPU 完成了新一帧的绘制,那么就会造成画面撕裂的现象。为解决这个问题引入了 VSync
机制,当视屏控制器读取完一帧之后才会发出 VSync
信号,GPU 才会绘制新的一帧,完成后缓冲区才会更新。
按照 60FPS 的帧率,每隔 16ms 就会有一次 VSync 信号。
卡顿的原因
当 VSync
信号到来时,系统通过 CADisplayLink
等机制通知 App,App 开始在 CPU 中计算显示内容并提交给 GPU,GPU 完成渲染后提交给缓冲区。
如果在这个过程中由来了第二个 VSync
信号,也就是说 CPU
或者 GPU
的计算时间过长,在第二个垂直同步信号来之前还没有往缓冲区提交新内容。那么,屏幕会保留之前的显示内容,正在计算的这一帧,会等待下一次机会才能显示。
这就是丢帧。就是说,卡顿的原因就是 CPU 或者 GPU 的计算时间过长,导致当垂直同步
信号到来时还没有往双缓冲区提交新的内容,屏幕会保留之前的内容,新内容只能等下一个垂直同步信号才有机会
显示。
示意图如下:
了解了上面的过程,我们就知道卡顿优化主要是要针对 CPU 与 GPU 进行优化。
CPU (Central Processing Unit,中央处理器)功能及优化
- 对象的创建和销毁
- 尽量使用轻量级对象。比如在不需要响应触摸事件的时候使用
CALayer
代替UIView
- 使用
xib
会更消耗性能,在性能非常敏感的界面,可以放弃使用xib
- 尽量推迟对象的创建,把对象的创建分散到多个任务中去
- 尽量使用轻量级对象。比如在不需要响应触摸事件的时候使用
- 对象属性的调整
UIView
的属性调整远大于一般对象的属性调整,要尽量减少属性的的调整- 尽量避免调整视图的层次、添加和移除视图
- 布局计算
- UIView 属性调整已经很消耗性能了,如果再修改
frame
、bounds
、transform
等属性,那么 CPU 和 GPU 又会重新计算和渲染,更消耗性能了,所以这些属性要尽量减少调整次数。 AutoLayout
的带来的CPU
消耗会随着视图数量的增长呈指数级增长。UIImageView
的size
最好与图片的大小保持一致(避免更多计算)
- UIView 属性调整已经很消耗性能了,如果再修改
- 文本的计算和排版
- 如果界面中有大量文本,那么文本的宽高计算也会消耗大量资源。可以将宽高计算与绘制文本放到子线程中去处理。
- 图片的格式转换和解码
UIImage
创建图片的时候,图片并不会立即解码,而是在CALayer
提交到 GPU 的前一刻才进行解码,这一步是在主线程中。要想绕开这个机制,常见的做法是自己在子线程中进行解码,常见的第三方库都有这个功能。
- 图像的绘制(Core Graphics)
- 一些以 CG 开头的绘制图像的方法也可以放到子线程中去
GPU (Graphics Processing Unit,图形处理器)及优化
- 纹理的
- 尽量避免短时间内大量图片的显示,可以将多张图片合成一张图片
- GPU 能处理的最大纹理尺寸是
4096x4096
一旦超过这个值,就会占用 CPU 的资源进行处理 - 减少视图层级
- 尽量避免透明度小于 1 的视图
- 尽量避免、
- 触发离屏渲染的操作
- 主动触发:光化
layer.shouldRasterize
layer.shadow
、layer.mask
、layer.border
、layer 设置圆角
- 重写 方法时,视图只要设置背景颜色,也会触发离屏渲染
CAShapeLayer
的矢量图形显示- Core Graphics API (核心绘图)的绘制操作会导致 CPU 的离屏渲染。
- 主动触发:光化
- 触发离屏渲染的操作
卡顿检测
- 添加
Observer
到主线程RunLoop
中,通过监听RunLoop
状态切换的好时,以达到监控卡顿的目的。。
你在项目中是怎么优化内存的?
tableview 卡顿的原因可能又哪些?你平时是怎么优化的?
如何减少启动时间?
APP 的启动分为两种:
- 冷启动:从零启动
- 热启动:APP 已经在内存中,在后台活着,再次点击 APP 启动 APP
APP 的启动时间主要是针对冷启动来说的。启动时间主要由main()
之前和main()
之后两部分组成。
分析main()
之前的时间消耗
在 Edit scheme -> Run -> Arguments 中添加环境变量 DYLD_PRINT_STATISTICS
且设置值为 1
。(按下 cmd + shift + ,
快捷呼出 Edit scheme )
现在启动程序就可以看到打印出来的信息了。
如果设置 DYLD_PRINT_STATISTICS_DETAILS
为 1
,可以看到更详细的信息。
APP 的启动过程可以分为如图所示的三大阶段:dyld 阶段、runtime 阶段、main 阶段
启动 APP 时,dyld
做的事情有:(dyld ,dynamic link editor, Apple 的动态连接器,可以用来装载 Mach-O 文件,可执行文件、动态库等都属于 Mach-O 文件)
- 装载 APP 的可执行文件,同时会递归加载所有依赖的动态库
- 当
dyld
把可执行文件、动态库都装载完毕后,会通知runtime
进行下一步的处理。
启动APP时,runtime 所做的事情有:
- 调用
map_images
进行可执行文件内容的解析和处理 - 在
load_images
中调用call_load_methods
,调用所有Class
和Category
的+load
方法 - 进行各种
objc
结构的初始化(注册 objc 类、初始化类对象等等) - 调用
C++
静态初始化器和__attribute__((constructor))
修饰的函数
到此为止,可执行文件和动态库中所有的符号(Class, Protocol, Selector, IMP,...)都已经按格式成功加载到内存中,被 runtime
所管理。
main() 阶段:
main 函数的实现很简单,如下:
int main(int argc, char * argv[]) { @autoreleasepool { return UIApplicationMain(argc, argv, nil NSStringFromClass([AppDelegate class])); }}复制代码
当代码走到 UIApplicationMain
函数时,就会走到 Appdelegate
类里。然后就会调用 application:didFinishLaunchingWithOptions:
。
App启动过程中每一个步骤都会影响启动性能,但是有些部分所消耗的时间少之又少,另外有些部分根本无法避免,考虑到投入产出比,我们只列出我们可以优化的部分:
- dyld
- 减少动态库(定期清理不必要的动态库)
- 减少 Objc 类、分类的数量(定期清理不必要的类、分类)
- 减少 C++ 虚函数数量
- Swift 尽量使用 struct
- runtime
- 尽量不要用到 +load 方法
- 尽量不要用到
__attribute__((constructor))
的C函数 - 也尽量不要用到C++的静态对象
- main
- 在不影响用户体验的前提下,尽可能将一些操作延迟,不要全部都放在
finishLaunching
方法中 - 按需加载
- 在不影响用户体验的前提下,尽可能将一些操作延迟,不要全部都放在