美文网首页图像显示与优化离屏渲染
iOS图形显示原理、界面保持流畅的技巧

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

作者: 蔚尼 | 来源:发表于2018-06-21 16:54 被阅读51次

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

    一.屏幕显示图像的原理

    1. HSync、VSync

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

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

    图像显示原理.png

    2. 图像显示原理

    图像显示原理

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

    2.1 CPU的工作

    CPU的工作.png

    2.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.耗电优化

    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
    冷启动三阶段

    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. 资源(图片、音频、视频等)
    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文件,可以查看可执行文件的具体组成
    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图形显示原理、界面保持流畅的技巧

        本文链接:https://www.haomeiwen.com/subject/pxeqyftx.html