iOS图形显示原理、界面保持流畅的技巧
写在前面:
这篇文章并非原创,是对iOS 保持界面流畅的技巧的学习总结。
讲述图像显示的原理;界面卡顿的原因;从CPU和GPU方面如何解决卡顿(让界面保持流畅)。
一.屏幕显示图像的原理
1. HSync、VSync
CRT电子枪从上到下一行行扫描,扫描完成后回到初始位置继续下一次扫描。每扫描完一行,显示器会发出一个水平同步信号HSync,让显示器的显示过程和视屏控制器进行同步。
绘制完一帧后,CRT电子枪回复到原位准备下一帧前,显示器会发出垂直同步信号VSync。显示器也按照VSync信号产生的频率进行刷新。
图像显示原理.png2. 图像显示原理
图像显示原理1)CPU计算好内容,提交给GPU进行渲染,2)GPU把渲染的结果放入帧缓冲区(FrameBuffer);
3)视频控制器收到VSync信号后逐行读取FrameBuffer的数据,数据转换后传递给显示器
2.1 CPU的工作
CPU的工作.png2.1 GPU的工作
GPU的渲染管线.png二.卡顿、掉帧产生的原因:
卡顿、掉帧每秒会有60帧的画面更新,所以60分之一秒就要产生一个画面,即60ps是流畅画面;相当于16.7ms产生一个画面;
16.7ms内会场上一个VSync信号。
-
图中第一段是正常显示的情况:CPU和GPU完成工作后,把渲染结果提交到帧缓冲区,等待VSync信号到来后显示到屏幕上;
-
图中第二段开始出现卡顿、掉帧:在VSync信号到来之前,CPU或者GPU没有完成任务,这一帧就被丢弃,等待下一次机会再显示,但是屏幕保留着之前未显示完的画面,出现卡顿和掉帧。
总结:
出现卡顿和掉帧的原因:在VSync信号到来之前(16.7ms),CPU和GPU没有完成下一帧画面的合成,就会造成卡顿。
三.卡顿尝试的原因和解决方案
CPU方面:
CPU优化方案1.对象创建、调整、销毁
1.1对象创建
-
1)对象创建会分配内存、调整属性,比较小号CPU资源。所以用
轻量级对象
代替重量级对象
。
eg:
对于不需要响应事件的控件,用CALayer显示;
对象不涉及UI操作,放到后台线程创建;
性能敏感的界面,storyborad的资源消耗>代码创建; -
2)推迟对象创建的时间,对象放到多个任务中
eg:懒加载 -
3)对象复用
如果对象可以复用,并且复用的代价比释放、创建新对象要小,那么这类对象应当尽量放到一个缓存池里复用。
1.2对象调整
- 避免视图层次调整、添加、移除;
视图层次调整时,UIView和CALayer之间会调用很多方法和通知;
CAlayer内部没有属性,当调整UIView 的关于显示相关的属性(比如 frame/bounds/transform)的时候,resolveInstanceMethod临时创建一个方法,把修改的属性值放到字典里面,创建动画等,非常消耗属性。
所以修改UIView的frame/bounds/transform属性消耗资源大于一般的属性;
1.3对象销毁
- 容器有大量对象,销毁时CPU耗时明显;可以把对象释放放到后台操作。
eg:把对象捕获到 block 中,然后扔到后台队列去随便发送个消息以避免编译器警告,就可以让对象在后台线程销毁了。
NSArray *tmp = self.array;
self.array = nil;
dispatch_async(queue, ^{
[tmp class];
});
2.预排版(布局计算、文本计算)
2.1布局计算
把布局放到后台计算,并进行缓存;
例如tableview的cell高度,可以在获取到数据之后,在后台线程里面进行计算好,避免cell里面和height获取的时候再次计算;
2.2文本计算
-
对UILabel,在后台线程里面:
[NSAttributedString boundingRectWithSize:options:context:] 计算文本宽高;
[NSAttributedString drawWithRect:options:context:] 绘制文本 -
或者CoreText 绘制文本:
先生成 CoreText 排版对象,然后自己计算
一个界面中包含大量文本(比如微博微信朋友圈等),文本的宽高计算会占用很大一部分资源,并且不可避免。
3.预渲染(文本等异步绘制、图片编解码、图像的绘制等)
3.1文本渲染
- 自定义文本控件,用 TextKit 或最底层的 CoreText 对文本异步绘制。
(CoreText 对象创建好后,能直接获取文本的宽高等信息,避免了多次计算(调整 UILabel 大小时算一遍、UILabel 绘制时内部再算一遍);CoreText 对象占用内存较少)
常见的文本控件 (UILabel、UITextView 等),其排版和绘制都是在主线程进行的,当显示大量文本时,CPU 的压力会非常大。
3.2图片的解码
- 在后台线程先把图片绘制到 CGBitmapContext 中,然后从 Bitmap 直接创建图片。(常见的网络图片库都自带这个功能。)
因为用 UIImage 或 CGImageSource 的那几个方法创建图片时,图片数据并不会立刻解码。图片设置到 UIImageView 或者 CALayer.contents 中去,并且 CALayer 被提交到 GPU 前,CGImage 中的数据才会得到解码。这一步是发生在主线程的,并且不可避免。如果想要绕开这个机制,只能按照上面的图片解码方法。
3.3图片的绘制
Quartz 2D绘制路径、文字 、Quartz 2D绘制路径实例里面,在[UIView drawRect:] 用CG开头的方法进行绘制,就是最简单的绘制方法。
GPU方面:
GPU方面纹理渲染
较短时间显示大量图片时(比如 TableView 存在非常多的图片并且快速滑动时),CPU 占用率很低,GPU 占用非常高,界面仍然会掉帧。
-
减少在短时间内大量图片的显示,尽可能将多张图片合成为一张进行显示;
-
GPU 的最大纹理尺寸是 4096×4096,尽量不要让图片和视图的大小超过这个值
视图混合
当多个视图(或者说 CALayer)重叠在一起显示时,GPU 会首先把他们混合到一起,混合的过程也会消耗很多 GPU 资源。所以需要试图混合。
- 尽量减少视图数量和层次
- 不透明的视图里标明 opaque 属性以避免无用的 Alpha 通道合成
- 把多个视图预先渲染为一张图片来显示。(第三方ASDK可以实现)
图形的生成
CALayer 的 border、圆角、阴影、遮罩(mask)---》产生离屏渲染(GPU方面的);
列表有大量圆角的时候,快速滚动列表,GPU资源基本占满,CPU资源消耗少;
- 制作圆角图片作为背景;
- 需要显示的图形在后台线程绘制为图片
YYKit的微博demo里面是把头像下载后在后台线程渲染为圆型后,放到ImageCache缓存中。
补充:
上述实践:微博Demo性能优化技巧
使用第三方# AsyncDisplayKit
,可把控件房贷线程创建和修改、图层预合成、异步并发操作、runloop任务分发、滑动列表预加载;
下面对上面进行总结:
卡顿优化 - CPU
-
尽量用轻量级的对象,比如用不到事件处理的地方,可以考虑使用CALayer取代UIView
-
不要频繁地调用UIView的相关属性,比如frame、bounds、transform等属性,尽量减少不必要的修改
-
尽量提前计算好布局,在有需要时一次性调整对应的属性,不要多次修改属性
-
Autolayout会比直接设置frame消耗更多的CPU资源
-
图片的size最好刚好跟UIImageView的size保持一致(不然CPU会对图片大小进行跳转)
-
控制一下线程的最大并发数量
-
尽量把耗时的操作放到子线程
文本处理(尺寸计算、绘制)
图片处理(解码、绘制)(把图片放到上下文,然后再进行显示)
卡顿优化 - GPU
-
尽量避免短时间内大量图片的显示,尽可能将多张图片合成一张进行显示
-
GPU能处理的最大纹理尺寸是4096x4096,一旦超过这个尺寸,就会占用CPU资源进行处理,所以纹理尽量不要超过这个尺寸
-
尽量减少视图数量和层次
-
减少透明的视图(alpha<1),不透明的就设置opaque为YES
-
尽量避免出现离屏渲染
面试题
你在项目中是怎么优化内存的?
优化你是从哪几方面着手?
列表卡顿的原因可能有哪些?你平时是怎么优化的?
遇到tableView卡顿嘛?会造成卡顿的原因大致有哪些?
卡顿检测
-
平时所说的“卡顿”主要是因为在主线程执行了比较耗时的操作
-
可以添加Observer到主线程RunLoop中,通过监听RunLoop状态切换的耗时,以达到监控卡顿的目的
可以使用第三方LXDAppFluecyMonitor-master辅助
耗电优化
1. 耗电主要来源
- CPU处理,Processing
- 网络,Networking
- 定位,Location
- 图像,Graphics
2.耗电优化
- 尽可能降低CPU、GPU功耗
- 少用定时器
- 优化I/O操作
- 尽量不要频繁写入小数据,最好批量一次性写入
- 读写大量重要数据时,考虑用dispatch_io,其提供了基于GCD的异步操作文件I/O的API。用dispatch_io系统会优化磁盘访问
- 数据量比较大的,建议使用数据库(比如SQLite、CoreData)
- 网络优化
- 减少、压缩网络数据
- 如果多次请求的结果是相同的,尽量使用缓存
- 使用断点续传,否则网络不稳定时可能多次传输相同的内容
- 网络不可用时,不要尝试执行网络请求
- 让用户可以取消长时间运行或者速度很慢的网络操作,设置合适的超时时间
- 批量传输,比如,下载视频流时,不要传输很小的数据包,直接下载整个文件或者一大块一大块地下载。如果下载广告,一次性多下载一些,然后再慢慢展示。如果下载电子邮件,一次下载多封,不要一封一封地下载
- 定位优化
- 如果只是需要快速确定用户位置,最好用CLLocationManager的requestLocation方法。定位完成后,会自动让定位硬件断电
如果不是导航应用,尽量不要实时更新位置,定位完毕就关掉定位服务 - 尽量降低定位精度,比如尽量不要使用精度最高的kCLLocationAccuracyBest
- 需要后台定位时,尽量设置pausesLocationUpdatesAutomatically为YES,如果用户不太可能移动的时候系统会自动暂停位置更新
- 尽量不要使用startMonitoringSignificantLocationChanges(监控位置改变的,精细的),优先考虑startMonitoringForRegion(监控区域改变的)
- 硬件检测优化
用户移动、摇晃、倾斜设备时,会产生动作(motion)事件,这些事件由加速度计、陀螺仪、磁力计等硬件检测。在不需要检测的场合,应该及时关闭这些硬件
启动优化
1.APP的启动
- APP的启动可以分为2种
- 冷启动(Cold Launch):从零开始启动APP
- 热启动(Warm Launch):APP已经在内存中,在后台存活着,再次点击图标启动APP
- APP启动时间的优化,主要是针对冷启动进行优化
- 检测启动时间:
- 通过添加环境变量可以打印出APP的启动时间分析(Edit scheme -> Run -> Arguments)
- DYLD_PRINT_STATISTICS设置为1
- 如果需要更详细的信息,那就将DYLD_PRINT_STATISTICS_DETAILS设置为1(启动时间在400ms以内的都算可行)
2. APP冷启动三阶段
包含以下三个阶段:
- dyld
- runtime
- main
2.1. dyld阶段
1.什么是 dyld(dynamic link editor):Apple的动态链接器,可以用来装载Mach-O文件(可执行文件、动态库等)
- 启动APP时,dyld所做的事情有
- 装载APP的可执行文件,同时会递归加载所有依赖的动态库
- 当dyld把可执行文件、动态库都装载完毕后,会通知Runtime进行下一步的处理
2.2. runtime阶段
启动APP时,runtime所做的事情有:
- 调用map_images进行可执行文件内容的解析和处理
- 在load_images中调用call_load_methods,调用所有Class和Category的+load方法
- 进行各种objc结构的初始化(注册Objc类 、初始化类对象等等)
- 调用C++静态初始化器和attribute((constructor))修饰的函数
到此为止,可执行文件和动态库中所有的符号(Class,Protocol,Selector,IMP,…)都已经按格式成功加载到内存中,被runtime 所管理
2.3.main阶段
总结一下
- APP的启动由dyld主导,将可执行文件加载到内存,顺便加载所有依赖的动态库
- 并由runtime负责加载成objc定义的结构
- 所有初始化工作结束后,dyld就会调用main函数
- 接下来就是UIApplicationMain函数,AppDelegate的application:didFinishLaunchingWithOptions:方法
APP的启动优化
按照不同的阶段
- dyld
- 减少动态库、合并一些动态库(定期清理不必要的动态库)
- 减少Objc类、分类的数量、减少Selector数量(定期清理不必要的类、分类)
- 减少C++虚函数数量
- Swift尽量使用struct
-
runtime
用+initialize方法和dispatch_once取代所有的attribute((constructor))、C++静态构造器、ObjC的+load -
main
在不影响用户体验的前提下,尽可能将一些操作延迟,不要全部都放在finishLaunching方法中
按需加载
安装包瘦身
- 安装包(IPA)主要由可执行文件、资源组成
- 资源(图片、音频、视频等)
- 采取无损压缩
- 去除没有用到的资源: https://github.com/tinymind/LSUnusedResources
- 可执行文件瘦身
-
编译器优化
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(https://www.jetbrains.com/objc/)检测未使用的代码:菜单栏 -> Code -> Inspect Code
-
编写LLVM插件检测出重复代码、未被调用的代码
- 生成LinkMap文件,可以查看可执行文件的具体组成
可借助第三方工具解析LinkMap文件: https://github.com/huanxsd/LinkMap
还需要学习的地方:
1.简单的 FPS 指示器:FPSLabel
2.查看自己app的fps
3.使用CADisplayLink显示FPS
1.如何异步绘制?
这篇博客异步绘制通过VVeboTableViewDemo分享了如何进行一步绘制;
并讲述以下部分:
提前计算并缓存好高度;
滑动时按需加载,在大量图片展示时提高滑动速度。
2.如何使用CoreText 绘制文本:(图文混排的绘制)
4.如何使用[NSAttributedString drawWithRect:options:context:] 绘制文本;
1.在后台线程操作对图片削圆,然后放到ImageCache缓存中/YYKit把头像渲染为圆形后放到缓存,如何使用这个功能?
2.YYKit的学习:YYLayout、YYDispatchQueuePool
、YYKit里面对图片的加载和解码的使用