一文读懂iOS图像显示原理与优化

站在巨人的肩膀上,总结原理实现与优化及卡顿监测,一鼓作气,气脉通畅,还要啥自行车~huaixiao~php

图像图形渲染原理

图形渲染主要是利用GPU并行运算能力,实现图形渲染并显示在屏幕的每个像素上。渲染过程最经常使用的就是光栅化,即将数据转化为可见像素的过程。GPU及相关驱动实现了图形处理的OpenGLDirectX模型,其实OpenGL不是函数API而是一种标准,制定了相关函数API及其实现的功能,具体的函数库由第三方来实现,一般是由显卡制造商来提供。html

GPU渲染过程以下图所示:ios

主要包括:顶点着色器(包含了3D坐标系的转换,每一个顶点属性值设定)、形状(图元)装配(造成基本的图形)、几何着色器(构造新的顶点来造成其余形状,如上图的另外一个三角形)、光栅化(将形状映射到屏幕的相应的像素生成片断,片断包含了像素结构全部的数据)、片断着色器(丢弃超过视图之外的像素并着色)、测试与混合(判断像素位置如是否在其余像素的后面及透明度等决定是否丢弃及混合)。git

要想图形更加真实逼真须要更多的顶点及颜色属性,这样就增长了性能开销,为提高成产和执行效率,常常会使用纹理来表现细节。github

纹理是一个 2D 图片(甚至也有 1D 和 3D 的纹理),纹理通常能够直接做为图形渲染流水线的*第五阶段(即片断着色器)*的输入;objective-c

GPU内部包含了若干处理核来实现并发执行,其内部使用了二级缓存(L1L2 cache),其与CPU的架构模型包含以下两种形式:分离式及耦合式,以下图所示:编程

  • 分离式的结构缓存

    CPU 和 GPU 拥有各自的存储系统,二者经过 PCI-e 总线进行链接。这种结构的缺点在于 PCI-e 相对于二者具备低带宽和高延迟,数据的传输成了其中的性能瓶颈。目前使用很是普遍,如PC、智能手机等。安全

  • 耦合式的结构性能优化

    CPU 和 GPU 共享内存和缓存。AMD 的 APU 采用的就是这种结构,目前主要使用在游戏主机中,如 PS4。

屏幕图形显示结构以下:

CPU将图形数据经过总线BUS提交至GPUGPU通过渲染处理转化为一帧帧的数据并提交至帧缓冲区,视频控制器会经过垂直同步信号VSync逐帧读取帧缓冲区的数据并提交至屏幕控制器最终显示在屏幕上。为解决一个帧缓冲区效率问题(读取和写入都是一个没法有效的并发处理),采用双缓冲机制,在这种状况下,GPU 会预先渲染一帧放入一个缓冲区中,用于视频控制器的读取。当下一帧渲染完毕后,GPU 会直接把视频控制器的指针指向第二个缓冲器,以下图所示:

双缓冲机制虽然提高了效率但也引入了画面撕裂问题,即当视频控制器还未读取完成时,即屏幕内容刚显示一半时,GPU 将新的一帧内容提交到帧缓冲区并把两个缓冲区进行交换后,视频控制器就会把新的一帧数据的下半段显示到屏幕上,形成画面撕裂现象,以下图:

为了解决这个问题,GPU 一般有一个机制叫作垂直同步(简写也是 V-Sync),当开启垂直同步后,GPU 会等待显示器的 VSync 信号发出后,才进行新的一帧渲染和缓冲区更新。这样能解决画面撕裂现象,也增长了画面流畅度,但须要消费更多的计算资源,也会带来部分延迟。

iOS 设备会始终使用双缓存,并开启垂直同步。而安卓设备直到 4.1 版本,Google 才开始引入这种机制,目前安卓系统是三缓存+垂直同步。

卡顿

VSync 信号到来后,系统图形服务会经过 CADisplayLink 等机制通知 App,App 主线程开始在 CPU 中计算显示内容,好比视图的建立、布局计算、图片解码、文本绘制等。随后 CPU 会将计算好的内容提交到 GPU 去,由 GPU 进行变换、合成、渲染。随后 GPU 会把渲染结果提交到帧缓冲区去,等待下一次 VSync 信号到来时显示到屏幕上。因为垂直同步的机制,若是在一个 VSync 时间内,CPU 或者 GPU 没有完成内容提交,则那一帧就会被丢弃,等待下一次机会再显示,而这时显示屏会保留以前的内容不变。这就是界面卡顿的缘由。

图像显示

图形渲染技术栈

整个图形渲染技术栈:App 使用 Core GraphicsCore AnimationCore Image 等框架来绘制可视化内容,这些软件框架相互之间也有着依赖关系。这些框架都须要经过 OpenGL 来调用 GPU 进行绘制,最终将内容显示到屏幕之上,结构以下图所示:

框架介绍:

  • UIKit

    UIKit 自身并不具有在屏幕成像的能力,其主要负责对用户操做事件的响应UIView 继承自 UIResponder),事件响应的传递大致是通过逐层的 视图树 遍历实现的。

  • Core Animation

    Core Animation 是一个复合引擎,其职责是 尽量快地组合屏幕上不一样的可视内容,这些可视内容可被分解成独立的图层(即 CALayer),这些图层会被存储在一个叫作图层树的体系之中。从本质上而言,CALayer 是用户所能在屏幕上看见的一切的基础。

  • Core Graphics

    Core Graphics 基于 Quartz 高级绘图引擎,主要用于运行时绘制图像。开发者可使用此框架来处理基于路径的绘图,转换,颜色管理,离屏渲染,图案,渐变和阴影,图像数据管理,图像建立和图像遮罩以及 PDF 文档建立,显示和分析。

  • Core Image

    Core ImageCore Graphics 偏偏相反,Core Graphics 用于在运行时建立图像,而 Core Image 是用来处理运行前建立的图像 的。Core Image 框架拥有一系列现成的图像过滤器,能对已存在的图像进行高效的处理。

  • OpenGL(ES)

    OpenGL ES(OpenGL for Embedded Systems,简称 GLES),是 OpenGL 的子集。

  • Metal

    Metal 相似于 OpenGL ES,也是一套第三方标准,具体实现由苹果实现。大多数开发者都没有直接使用过 Metal,但其实全部开发者都在间接地使用 MetalCore AnimationCore ImageSceneKitSpriteKit 等等渲染框架都是构建于 Metal 之上的。当在真机上调试 OpenGL 程序时,控制台会打印出启用 Metal 的日志。根据这一点能够猜想,Apple 已经实现了一套机制将 OpenGL 命令无缝桥接到 Metal 上,由 Metal 担任真正于硬件交互的工做

UIView与CALayer关系

UIKit中的每个视图控件其内部都有一个关联的CALayer,即backing layer;因为这种一一对应的关系,视图采用视图树形式呈现,与之对应的图层也是采用图层树形式。

视图的职责是建立并管理图层,以确保当子视图在层级关系中 添加或被移除 时,其关联的图层在图层树中也有相同的操做,即保证视图树和图层树在结构上的一致性。

苹果采用这种结构的目的是保证iOS/Mac平台底层CALayer通用,避免重复代码且职责分离,毕竟采用多点触摸形式与基于鼠标键盘的交互有着本质的区别;

CALayer

CALayer基本等同于纹理,本质上是一张图片,所以 CALayer 也包含一个 contents 属性指向一块缓存区,称为 backing store,能够存放位图(Bitmap)。iOS 中将该缓存区保存的图片称为 寄宿图

位图(英语:Bitmap,台湾称为点阵图),又称栅格图(Raster graphics),是使用像素阵列(Pixel-array/Dot-matrix点阵)来表示的图像。位图也可指:一种数据结构,表明了有限域中的稠集(dense set),每个元素至少出现一次,没有其余的数据和元素相关联。在索引,数据压缩等方面有普遍应用,位图的像素都分配有特定的位置和颜色值。

图形渲染流水线支持从顶点开始进行绘制(在流水线中,顶点会被处理生成纹理),也支持直接使用纹理(图片)进行渲染。相应地,在实际开发中,绘制界面也有两种方式:一种是 手动绘制(custom drawing);另外一种是 使用图片(contents image)

Contents Image 是指经过 CALayercontents 属性来配置图片,典型的是经过CGImage来指定其内容。Custom Drawing 是指使用 Core Graphics 直接绘制寄宿图。实际开发中,通常经过继承 UIView 并实现 -drawRect:方法来自定义绘制。

虽然 -drawRect: 是一个 UIView 方法,但事实上都是底层的 CALayer 完成了重绘工做并保存了产生的图片。下图所示为 -drawRect: 绘制定义寄宿图的基本原理。

  • UIView都有一个CALayer属性

  • CALayer存在弱引用delegate属性,实现了<CALayerDelegate>协议,由UIView来代理实现协议方法;

  • 当须要重绘时,CALayer首先调用-displayLayer方法,此时代理能够直接设置contents属性;

    须要重绘指:好比改变了 Frame、更新了 UIView/CALayer 的层次时,或者手动调用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay方法;

  • 若是代理没有实现 -displayLayer: 方法,CALayer 则会尝试调用 -drawLayer:inContext: 方法。在调用该方法前,CALayer 会建立一个空的寄宿图(尺寸由 boundscontentScale 决定)和一个 Core Graphics 的绘制上下文CGContextRef,为绘制寄宿图作准备,做为 ctx 参数传入。

  • -drawLayer:inContext内部会调用-drawRect,细节代码以下:

    - (void)drawLayer:(CALayer*)layer inContext:(CGContextRef)context {
        UIGraphicsPushContext(context);
    
        CGRect bounds;
        bounds = CGContextGetClipBoundingBox(context);
        [self drawRect:bounds];
    
        UIGraphicsPopContext();
    }
    复制代码

    具体的函数调用栈以下:

  • 最后,由 Core Graphics 绘制生成的寄宿图会存入 backing store

Core Animation Pipeline

了解完CALayer本质及流程后,详细介绍下Core Animation Pipeline工做原理,以下图:

其中iOS中应用并不负责渲染而是由专门的渲染进程负责,即Render Server

在 iOS 5 之前这个进程叫 SpringBoard,在 iOS 6 以后叫 BackBoard或者backboardd;

越狱查看系统进程,确实存在此进程,以下图:

主要处理流程以下:

  • 首先,由 App 处理事件(Handle Events),如:用户的点击操做,在此过程当中 app 可能须要更新视图树,相应地,图层树 也会被更新;

  • 其次,App 经过 CPU 完成对显示内容的计算,如:视图的建立、布局计算、图片解码、文本绘制等。在完成对显示内容的计算以后,App 对图层进行打包,并在下一次 RunLoop 时将其发送至 Render Server,即完成了一次 Commit Transaction 操做。

    具体commit transcation能够细分为以下步骤:

    • Layout,主要进行视图构建,包括:LayoutSubviews 方法的重载,addSubview: 方法填充子视图等;
    • Display,主要进行视图绘制,这里仅仅是设置最要成像的图元数据。重载视图的 drawRect: 方法能够自定义 UIView 的显示,其原理是在 drawRect: 方法内部绘制寄宿图,该过程使用 CPU 和内存;
    • Prepare,属于附加步骤,通常处理图像的解码和转换等操做;
    • Commit,主要将图层打包,并将它们经过IPC发送至 Render Server。该过程会递归执行,由于图层和视图都是以树形结构存在。
  • Render Server执行OpenGLCore Graphics相关操做,如根据layer的各类属性(若是是动画属性,则会计算动画layer的属性的中间值)并用OpenGL准备渲染;

  • GPU经过Frame Buffer、视频控制器等相关组件对图层进行渲染到屏幕;

为了知足屏幕60FPS刷新率,RunLoop每次操做的时间间隔不该超过16.67ms,且上述步骤须要并行执行。

渲染与RunLoop

iOS 的显示系统是由 VSync 信号驱动的,VSync 信号由硬件时钟生成,每秒钟发出 60 次(这个值取决设备硬件,好比 iPhone 真机上一般是 59.97)。iOS 图形服务接收到 VSync 信号后,会经过 IPC 通知到 App 内。App 的 Runloop 在启动后会注册对应的 CFRunLoopSource 经过 mach_port 接收传过来的时钟信号通知,随后 Source 的回调会驱动整个 App 的动画与显示。

备注:实际观察App启动后未注册相关的VSync相关的Source,所以上述应用应该是Render Server渲染进程注册Source监听VSync信号来驱动图层的渲染,进而提交至GPU。

Core AnimationRunLoop 中注册了一个 Observer,监听了 BeforeWaitingExit 事件。这个 Observer 的优先级是 2000000,低于常见的其余 Observer。当一个触摸事件到来时,RunLoop 被唤醒,App 中的代码会执行一些操做,好比建立和调整视图层级、设置 UIView 的 frame、修改 CALayer 的透明度、为视图添加一个动画;这些操做最终都会被 CALayer 捕获,并经过 CATransaction 提交到一个中间状态去(CATransaction 的文档略有提到这些内容,但并不完整)。当上面全部操做结束后,RunLoop 即将进入休眠(或者退出)时,关注该事件的 Observer 都会获得通知。这时 Core Animation 注册的那个 Observer 就会在回调中,把全部的中间状态合并提交到 GPU 去显示;若是此处有动画,Core Animation 会经过 CADisplayLink 等机制屡次触发相关流程。

渲染性能优化

为了保证渲染性能,主要是保证CPUGPU不会阻碍上述渲染流程进而引起“掉帧”现象,所以须要分别针对CPUGPU影响渲染过程进行分析、评估及优化。

CPU资源消耗缘由及解决方案

对象建立

对象建立会分配内存、调整属性、甚至还有读取文件(如建立UIViewController读取xib文件)等操做,比较消耗CPU资源。所以,尽可能使用轻量的对象替代重量的对象,如CALayerUIView不须要响应触摸事件;若是对象不涉及UI操做,则尽可能放到后台线程执行;性能敏感的视图对象,尽可能使用代码建立而不是Storyboard来建立;若是对象能够复用,可使用缓存池来复用。

对象调整

对象调整也常常是消耗CPU资源的地方,如CALayer属性修改、视图层次调整、添加和移除视图等;

CALayer内部并无属性方法,其内部是经过runtime动态接收方法resoleInstanceMethod方法为对象临时添加一个方法,并把对应属性值保存到内部的一个Dictionary字典里,同时还会通知delegate、建立动画等。UIView的关于显示相关的属性(好比frame/bounds/transform)等其实是CALayer属性映射来的。

对象销毁

虽然对象销毁销毁资源很少,但累积起来也不容忽视。一般当容器类持有大量对象时,其销毁时的资源消耗就很是明显,所以,可见用于后台线程去释放的对象挪动后台线程去。技巧代码以下:

//将对象捕获到block中,而后扔到后台队列中随便发个消息以免编译器警告;
NSArray *tmp = self.array;
self.array = nil;
dispatch_async(queue, ^{
    [tmp class];
});
复制代码
布局计算

视图布局计算是应用最为常见的销毁CPU资源的地方,其最终实现都会经过UIView.frame/bounds/center等属性的调整上,所以,避免CPU资源消耗尽可能提早计算好布局,在须要时一次性调整好对应属性,而不要屡次、频繁的计算和调整这些属性。

Autolayout

Auotlayout是苹果提倡的技术,可大部分状况下能很好地提高开发效率,可是其对于复杂视图来讲尝尝会带来严重的性能问题,具体可参阅pilky.me/36/,所以对于性能要求高的视图尽可能使用代码实现视图。

文本计算

若是页面包含大量文本,文本宽高计算会占用很大一部分资源,而且不可避免。能够经过UILabel内部的实现方式:[NSAttributedString boundingRectWithSize:options:context]富文本AttributedString来计算文本宽高,用[NSAttributeString drawWithRect:options:context:]来绘制文本,并放在后台线程执行避免阻塞主线程;或者使用CoreText基于c的跨平台API来绘制文本。

Core Text 是为一些必须处理底层字体处理和文字布局的开发者准备,如无必要,你应该使用 TextKit(Text Programming Guide for iOS)、CocoaText(Cocoa Text Architecture Guide)等框架开发你的 App 或 Mac 应用。Core Text 是以上两种文本框架的底层实现,所以它们的速度和效率是共享的。除此以外,以上两种文本框架提供了富文本编辑及页面布局引擎。若是你的 App 只使用 Core Text,则须要为其提供其余的基础实现。Core Text 编程指南

文本渲染

屏幕上能看到的全部文本内容控件,包括UIWebView,在底层都是经过CoreText排版、绘制为Bitmap显示。常见的文本控件,如UILabelUITextView等,其排版和绘制都是在主线程进行,当显示大量文本时,CPU的压力会很是大。解决方案只有一个,就是自定义文本控件,并用TextKit或最底层的CoreText对文本异步绘制

图片解码

当使用UIImageCGImageSource的那几个方法建立图片时,图片数据并不会当即解码。只有图片设置到UIImageView或者CALayer.contents中去,而且CALayer被提交到GPU前,CGImage中的数据才会获得解码,且须要在主线程执行。

解决方法:后台线程先把图片绘制到CGBitmapContext中,而后从Bitmap直接建立图片。目前常见的网络图片库都自带这个功能。

图像绘制

图像的绘制一般是指用CGxx开头的方法将图像绘制到画布中,而后从画布建立图片并显示这样的一个过程。这个最多见的就是[UIView drawRect:]方法,因为CoreCraphic方法一般都是线程安全的,因此图像的绘制能够很容易放到后台线程进行,示例以下:

- (void)display {
	dispatch_async(backgroudQueu, ^{
		CGContextRef ctx = CGBitmapContextCreate(...);
		//draw in context ....
		CGImageRef img = CGBitmapContextCreateImage(ctx);
		CFRelease(ctx);
		dispatch_async(mainQueue, ^{
			layer.contents = img;
		});
	});
}
复制代码

GPU资源消耗缘由及解决方案

相对于CPU来讲,GPU主要就是:接收提交的纹理顶点描述(三角形)应用变换混合渲染,而后输出到屏幕上。一般你所能看到的内容,主要也就是纹理(图片)和形状(三角模拟的矢量图形)两类。

纹理的渲染

全部的Bitmap,包括图片、文本、栅格化的内容,最终都要从内存提交到显存,绑定为GPU纹理。不管是提交到显存的过程,仍是GPU调制和渲染纹理的过程,都要消耗很多GPU资源。当在较短期内显示大量图片时(如UITableView存在很是多的图片而且快速滑动时),CPU占用率很低,GPU占用很是高,所以会致使界面掉帧卡顿。有效避免此状况的方法就是尽可能减小在短期内大量图片的显示,尽量将多张图片合并为一张进行显示。

视图的混合

多存在多视图且多层次重叠显示时,GPU会首先将其混合在一块儿。若是视图结构很复杂,混合的过程也会消耗不少的GPU资源。为了减轻GPU的消耗,应尽可能减小视图数量级层次,并在不透明的视图里标明opaque属性以免无用的Alpha通道合成。

图形的生成

CALayerborder、圆角、阴影、遮罩(mask),CASharpLayer的矢量图形显示,一般会触发离屏渲染(offscreen rendering),而离屏渲染一般发生在GPU中。当一个列表视图中存在大量圆角的CALayer且款式滑动时,会消耗大量的GPU资源,进而引起界面卡顿。为避免此种状况,能够尝试开始CALayer.shouldRasterize属性,这会吧离屏渲染的操做转嫁到CPU上;最好是尽可能避免使用圆角、阴影、遮罩等属性。

GPU屏幕渲染存在两种方式:当前屏幕渲染(On-Screen Rendering)离屏渲染(Off-Screen Rendering),其中当前屏幕渲染就是正常的GPU渲染流程,GPU将渲染完成的帧放到帧缓冲区,而后显示到屏幕;而离屏渲染会额外建立一个离屏渲染缓冲区(如保存后续复用的数据),后续仍会提交至帧缓冲区进而显示到屏幕。

离屏渲染须要建立新的缓冲区,渲染过程当中会涉及从当前屏幕切换到离屏环境屡次上下文环境切换,等到离屏渲染完成后还须要将渲染结果切换到当前屏幕环境,所以付出的代价较高。

AsyncDisplayKit

AsyncDisplayKit(简写ASDK)是Facebook开源的一个用于保持iOS界面流畅的开源库,其基本原理以下:

将不须要主线程执行的消耗性能的经过异步执行方式执行,如文本宽高和视图布局计算,文本渲染、图片界面和图形绘制,对象建立、属性调制和销毁;但UIKitCore Animation相关操做必须在主线程执行,对于不能后台执行的就优化性能。

UIView CALayer封装

在原有UIViewCALayer基础上,封装了ASDisplayNode类(简写ASNode),包装了常见的视图属性(如frame/bounds/aplphs/transform/backgroudColor/superNode/subNodes等),创建ASNodeCALayer的对应关系,当CALayer属性改变或者动画产生时,会经过delegate通知的UIVIew进而通知ASNode。因为UIviewCALayer不是线程安全的,而且只能在主线程建立、访问和销毁,但ASNode是线程安全的,能够在后台线程建立和修改。ASNode还提供了layer backed属性,当不须要触摸事件时,就省去了UIView的中间层功能。同时还提供了大量优化后的子类封装,如Button/Control/Cell/Image/ImageView/Text/TableView/CollectView等。

图层预合成

对于多层级CALayer状况,GPU须要图层合成,但对于多层级图层中不须要动画和位置调整的状况,就会致使不必的GPU性能消耗,所以ASDK为此实现了一个pr-composing的技术,将多层级图层合并渲染成一张图片,有效下降了GPU的消耗。

异步并发操做

上文提到的能够后台线程的任务经过GCD异步并发执行,有效利用iPhone处理器多核的特色。

RunLoop任务分发

ASDK 在此处模拟了 Core Animation 的这个机制:全部针对 ASNode 的修改和提交,总有些任务是必需放入主线程执行的。当出现这种任务时,ASNode 会把任务用 ASAsyncTransaction(Group) 封装并提交到一个全局的容器去。ASDK 也在 RunLoop 中注册了一个 Observer,监视的事件和 CA 同样,但优先级比 CA 要低。当 RunLoop 进入休眠前、CA 处理完事件后,ASDK 就会执行该 loop 内提交的全部任务。经过这种机制,ASDK 能够在合适的机会把异步、并发的操做同步到主线程去,而且能得到不错的性能。

卡顿检测

instrument工具

主要工具使用以下:

Time Profiler,用来检测CPU的使用状况。它能够告诉咱们程序中的哪一个方法正在消耗大量的CPU时间。使用大量的CPU并不必定是个问题 - 你可能指望动画路径对CPU很是依赖,由于动画每每是iOS设备中最苛刻的任务。 可是若是你有性能问题,查看CPU时间对于判断性能是否是和CPU相关,以及定位到函数都颇有帮助。

Core Animation,用来监测Core Animation性能。它给咱们提供了周期性的FPS。

以下图使用Core Animation工具查看FPS(Frames Per Second)每秒帧渲染数;

基于RunLoop检测

主要有两种方案:

  • FPS监控

    原理就是添加CADisplayLink对象至runloop中统计每秒回调次数,经过次数/时间来获取屏幕刷新率FPS,具体实现以下:

// 建立CADisplayLink,并添加到当前run loop的NSRunLoopCommonModes
_link = [CADisplayLink displayLinkWithTarget:self selector:@selector(tick:)];
[_link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];

- (void)tick:(CADisplayLink *)link {
    if (_lastTime == 0) {
        _lastTime = link.timestamp;
        return;
    }
    
    _count++;
    NSTimeInterval delta = link.timestamp - _lastTime;
  	// 统计每秒的回调次数_count
    if (delta < 1) return;
    _lastTime = link.timestamp;
  	// FPS=次数/时间间隔
    float fps = _count / delta;
    _count = 0;    
    NSLog(@"current FPS: %d", (int)round(fps));
}
复制代码

CADisplayLink 是一个和屏幕刷新率一致的定时器(但实际实现原理更复杂,和 NSTimer 并不同,其内部实际是操做了一个 Source)。若是在两次屏幕刷新之间执行了一个长任务,那其中就会有一帧被跳过去(和 NSTimer 类似),形成界面卡顿的感受。在快速滑动TableView时,即便一帧的卡顿也会让用户有所察觉。经过对比CADisplayLink添加至runloop先后modes变化,发现其实现是向runloop中添加Source1回调为IODispatchCalloutFromCFMessage

UI绘制并不必定FPS为满60帧,如动画片FPS为24,所以,经过FPs方案监测卡顿是存在问题的。

FPS 是一秒显示的帧数,也就是一秒内画面变化数量。若是按照动画片来讲,动画片的 FPS 就是 24,是达不到 60 满帧的。也就是说,对于动画片来讲,24 帧时虽然没有 60 帧时流畅,但也已是连贯的了,因此并不能说 24 帧时就算是卡住了。

  • 主线程卡顿监控

    经过子线程监测主线程的runloop,判断kCFRunLoopBeforeSourceskCFRunLoopAfterWaiting两个状态之间的耗时是否达到必定阈值,若监测到卡顿则记录此时的函数调用信息,具体代码以下:

    static void runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)
    {
        MyClass *object = (__bridge MyClass*)info;
        
        // 记录状态值
        object->activity = activity;
        
        // 发送信号
        dispatch_semaphore_t semaphore = moniotr->semaphore;
        dispatch_semaphore_signal(semaphore);
    }
    
    - (void)registerObserver
    {
        CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL};
        CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
                                                                kCFRunLoopAllActivities,
                                                                YES,
                                                                0,
                                                                &runLoopObserverCallBack,
                                                                &context);
        CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
        
        // 建立信号
        semaphore = dispatch_semaphore_create(0);
        
        // 在子线程监控时长
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            while (YES)
            {
                // 假定连续5次超时50ms认为卡顿(固然也包含了单次超时250ms)
                long st = dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, 50*NSEC_PER_MSEC));
                if (st != 0)
                {
                    if (activity==kCFRunLoopBeforeSources || 
                        activity==kCFRunLoopAfterWaiting)
                    {
                        if (++timeoutCount < 5)
                            continue;
                        //使用第三方crash收集库PLCrashReporter,其不只会收集crash信息也能够用于实施获取各线程的调用堆栈
                      	PLCrashReporterConfig *config = [[PLCrashReporterConfig alloc]
                                                       initWithSignalHandlerType:PLCrashReporterSignalHandlerTypeBSD                                  
                                                       symbolicationStrategy:PLCrashReporterSymbolicationStrategyAll];
                        PLCrashReporter *crashReporter = [[PLCrashReporter alloc] initWithConfiguration:config];
                        
                        NSData *data = [crashReporter generateLiveReport];
                        PLCrashReport *reporter = [[PLCrashReport alloc] initWithData:data error:NULL];
                        NSString *report = [PLCrashReportTextFormatter stringValueForCrashReport:reporter
                                                                                  withTextFormat:PLCrashReportTextFormatiOS];
    
                        NSLog(@"好像有点儿卡哦");
                    }
                }
                timeoutCount = 0;
            }
        });
    }
    复制代码

    为啥须要监测kCFRunLoopBeforeSourceskCFRunLoopAfterWaiting间的耗时,主要由于二者之间处理了APP内部事件处理的Source0时间,如触摸事件、CFSocketRef,还有中间监听kCFRunLoopBeforeWaiting状态Core Animation提交全部的图层中间状态至GPU,大部分致使卡顿的场景都在这二者之间;

    而主线程RunLoop闲置时处在kCFRunLoopBeforeSourceskCFRunLoopAfterWaiting之间的kCFRunLoopBeforeWaiting状态,所以致使错误的判断为卡顿,所以优化解决此问题出现了子线程ping方案。具体的原理以下:建立一个子线程经过信号量去ping主线程,由于ping的时候主线程确定是在kCFRunLoopBeforeSourceskCFRunLoopAfterWaiting之间。每次检测时设置标记位为YES,而后派发任务到主线程中将标记位设置为NO。接着子线程沉睡超时阙值时长,判断标志位是否成功设置成NO,若是没有说明主线程发生了卡顿,ANREye中就是使用子线程Ping的方式监测卡顿的,具体代码以下:

    @interface PingThread : NSThread
    ......
    @end
    
    @implementation PingThread
    
    - (void)main {
        [self pingMainThread];
    }
    
    - (void)pingMainThread {
        while (!self.cancelled) {
            @autoreleasepool {
              __block BOOL timeOut = YES;
                dispatch_async(dispatch_get_main_queue(), ^{
                  	timeOut = NO;
                  	dispatch_semaphore_signal(_semaphore);
                });
                [NSThread sleepForTimeInterval: lsl_time_out_interval];
                if (timeOut) {
                    NSArray *callSymbols = [StackBacktrace backtraceMainThread];
                  	...
                }
                dispatch_wait(_semaphore, DISPATCH_TIME_FOREVER);
            }
        }
    }
    @end
    复制代码

Reference

iOS-Core-Animation-Advanced-Techniques

计算机那些事(8)——图形图像渲染原理

iOS 图像渲染原理

iOS 保持界面流畅的技巧

关于drawRect

深刻理解 iOS Rendering Process

iOS 视图、动画渲染机制探究

iOS Core Animation: Advanced Techniques中文译本

iOS离屏渲染

iOS 的离屏渲染

离屏渲染优化详解:实例示范+性能测试

iOS 核心动画高级及技巧

iOS性能优化 - 工具Instruments之Time Profiler

CADisplayLink

backboardd

质量监控-卡顿检测

iOS开发--APP性能检测方案汇总(一)

相关文章
相关标签/搜索