iOS图形显示原理、界面保持流畅的技巧

写在前面:
这篇文章并非原创,是对iOS 保持界面流畅的技巧的学习总结。
讲述图像显示的原理;界面卡顿的原因;从CPU和GPU方面如何解决卡顿(让界面保持流畅)。

一.屏幕显示图像的原理

1. HSync、VSync

CRT电子枪从上到下一行行扫描,扫描完成后回到初始位置继续下一次扫描。每扫描完一行,显示器会发出一个水平同步信号HSync,让显示器的显示过程和视屏控制器进行同步。

绘制完一帧后,CRT电子枪回复到原位准备下一帧前,显示器会发出垂直同步信号VSync。显示器也按照VSync信号产生的频率进行刷新。

iOS图形显示原理、界面保持流畅的技巧_第1张图片
图像显示原理.png

2. 图像显示原理

iOS图形显示原理、界面保持流畅的技巧_第2张图片
图像显示原理

1)CPU计算好内容,提交给GPU进行渲染,2)GPU把渲染的结果放入帧缓冲区(FrameBuffer);
3)视频控制器收到VSync信号后逐行读取FrameBuffer的数据,数据转换后传递给显示器

2.1 CPU的工作

iOS图形显示原理、界面保持流畅的技巧_第3张图片
CPU的工作.png

2.1 GPU的工作

iOS图形显示原理、界面保持流畅的技巧_第4张图片
GPU的渲染管线.png

二.卡顿、掉帧产生的原因:

iOS图形显示原理、界面保持流畅的技巧_第5张图片
卡顿、掉帧

每秒会有60帧的画面更新,所以60分之一秒就要产生一个画面,即60ps是流畅画面;相当于16.7ms产生一个画面;

16.7ms内会场上一个VSync信号。

  • 图中第一段是正常显示的情况:CPU和GPU完成工作后,把渲染结果提交到帧缓冲区,等待VSync信号到来后显示到屏幕上;

  • 图中第二段开始出现卡顿、掉帧:在VSync信号到来之前,CPU或者GPU没有完成任务,这一帧就被丢弃,等待下一次机会再显示,但是屏幕保留着之前未显示完的画面,出现卡顿和掉帧。

总结:
出现卡顿和掉帧的原因:在VSync信号到来之前(16.7ms),CPU和GPU没有完成下一帧画面的合成,就会造成卡顿。

三.卡顿尝试的原因和解决方案

CPU方面:

iOS图形显示原理、界面保持流畅的技巧_第6张图片
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方面:

iOS图形显示原理、界面保持流畅的技巧_第7张图片
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.耗电优化

  1. 尽可能降低CPU、GPU功耗
  2. 少用定时器
  3. 优化I/O操作
  • 尽量不要频繁写入小数据,最好批量一次性写入
  • 读写大量重要数据时,考虑用dispatch_io,其提供了基于GCD的异步操作文件I/O的API。用dispatch_io系统会优化磁盘访问
  • 数据量比较大的,建议使用数据库(比如SQLite、CoreData)
  1. 网络优化
  • 减少、压缩网络数据
  • 如果多次请求的结果是相同的,尽量使用缓存
  • 使用断点续传,否则网络不稳定时可能多次传输相同的内容
  • 网络不可用时,不要尝试执行网络请求
  • 让用户可以取消长时间运行或者速度很慢的网络操作,设置合适的超时时间
  • 批量传输,比如,下载视频流时,不要传输很小的数据包,直接下载整个文件或者一大块一大块地下载。如果下载广告,一次性多下载一些,然后再慢慢展示。如果下载电子邮件,一次下载多封,不要一封一封地下载
  1. 定位优化
  • 如果只是需要快速确定用户位置,最好用CLLocationManager的requestLocation方法。定位完成后,会自动让定位硬件断电
    如果不是导航应用,尽量不要实时更新位置,定位完毕就关掉定位服务
  • 尽量降低定位精度,比如尽量不要使用精度最高的kCLLocationAccuracyBest
  • 需要后台定位时,尽量设置pausesLocationUpdatesAutomatically为YES,如果用户不太可能移动的时候系统会自动暂停位置更新
  • 尽量不要使用startMonitoringSignificantLocationChanges(监控位置改变的,精细的),优先考虑startMonitoringForRegion(监控区域改变的)
  1. 硬件检测优化
    用户移动、摇晃、倾斜设备时,会产生动作(motion)事件,这些事件由加速度计、陀螺仪、磁力计等硬件检测。在不需要检测的场合,应该及时关闭这些硬件

启动优化

1.APP的启动

  1. APP的启动可以分为2种
  • 冷启动(Cold Launch):从零开始启动APP
  • 热启动(Warm Launch):APP已经在内存中,在后台存活着,再次点击图标启动APP
  • APP启动时间的优化,主要是针对冷启动进行优化
  1. 检测启动时间:
  • 通过添加环境变量可以打印出APP的启动时间分析(Edit scheme -> Run -> Arguments)
  • DYLD_PRINT_STATISTICS设置为1
  • 如果需要更详细的信息,那就将DYLD_PRINT_STATISTICS_DETAILS设置为1(启动时间在400ms以内的都算可行)

2. APP冷启动三阶段

包含以下三个阶段:

  • dyld
  • runtime
  • main
iOS图形显示原理、界面保持流畅的技巧_第8张图片
冷启动三阶段

2.1. dyld阶段

1.什么是 dyld(dynamic link editor):Apple的动态链接器,可以用来装载Mach-O文件(可执行文件、动态库等)

  1. 启动APP时,dyld所做的事情有
  • 装载APP的可执行文件,同时会递归加载所有依赖的动态库
  • 当dyld把可执行文件、动态库都装载完毕后,会通知Runtime进行下一步的处理

2.2. runtime阶段

启动APP时,runtime所做的事情有:

  1. 调用map_images进行可执行文件内容的解析和处理
  2. 在load_images中调用call_load_methods,调用所有Class和Category的+load方法
  3. 进行各种objc结构的初始化(注册Objc类 、初始化类对象等等)
  4. 调用C++静态初始化器和attribute((constructor))修饰的函数

到此为止,可执行文件和动态库中所有的符号(Class,Protocol,Selector,IMP,…)都已经按格式成功加载到内存中,被runtime 所管理

2.3.main阶段

总结一下

  1. APP的启动由dyld主导,将可执行文件加载到内存,顺便加载所有依赖的动态库
  2. 并由runtime负责加载成objc定义的结构
  3. 所有初始化工作结束后,dyld就会调用main函数
  4. 接下来就是UIApplicationMain函数,AppDelegate的application:didFinishLaunchingWithOptions:方法

APP的启动优化

按照不同的阶段

  • dyld
  1. 减少动态库、合并一些动态库(定期清理不必要的动态库)
  2. 减少Objc类、分类的数量、减少Selector数量(定期清理不必要的类、分类)
  3. 减少C++虚函数数量
  4. Swift尽量使用struct
  • runtime
    用+initialize方法和dispatch_once取代所有的attribute((constructor))、C++静态构造器、ObjC的+load

  • main
    在不影响用户体验的前提下,尽可能将一些操作延迟,不要全部都放在finishLaunching方法中
    按需加载

安装包瘦身

  • 安装包(IPA)主要由可执行文件、资源组成
  1. 资源(图片、音频、视频等)
  • 采取无损压缩
  • 去除没有用到的资源: https://github.com/tinymind/LSUnusedResources
  1. 可执行文件瘦身
  • 编译器优化
    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插件检测出重复代码、未被调用的代码

  1. 生成LinkMap文件,可以查看可执行文件的具体组成
iOS图形显示原理、界面保持流畅的技巧_第9张图片
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里面对图片的加载和解码的使用


你可能感兴趣的:(iOS图形显示原理、界面保持流畅的技巧)