页面间跳转的性能问题总结为以下三种情形:
1).A页面跳转到B页面,由于B页面需要加载大量的数据,所以导致页面跳转延迟。
2).A页面跳转到B页面,由于B页面需要加载大量UI元素,所以导致页面跳转延迟。
3).A页面跳转到B页面,由于A或B页面的GPU使用率过高,所以导致面页跳转时出现过场动画不流畅,缓慢等。
-渲染服务进程
虽然看到的效果跟Application的代码是一一对应的,但视图绘制渲染的工作并不是由Application完成的,而是由一个名为渲染服务的进程(BackBoard)来完成的,这个进程的工作便是你在屏幕上看到的一切内容。既然做实际绘制渲染工作的是渲染服务进程,那么渲染服务进程要进行绘制渲染的依据是什么呢?而Application跟渲染服务进程又是怎么交互的呢?
-UIView与CALayer
首先简单讲述一下UIView与CALayer的关系(不讲述两者的区别)。简单来说,UIView就是CALayer的管理器,CALayer的主要工作是为屏幕的绘制渲染提供所需的数据源,也就是说,你在屏幕上看到的内容,都是来源于CALayer。每一个UIView都有一个Backing Layer,UIView的UI属性跟CALayer的属性是一一对应的,设置UIView的UI属性实际上是设置CALayer对应的属性,即UIView的绘制渲染工作是由CALayer完成。UIView对象之间存在着一定的层级关系,那么所以UIView的Backing Layer也相应的存在着一定的层级关系,这个层级关系叫做图层树(模型树)。
-图层树,呈现树,渲染树
使用Core Animation的Application(iOS默认使用),除了图层树,还有呈现树和渲染树,每个图层对象集合都扮演着不同的角色。图层树中的图层对象负责存储在屏幕上显示的目标值,呈现树中的图层对象负责存储在屏幕上显示的瞬时值,而渲染树的图层对象是渲染服务进程用来绘制渲染所使用的。Application使用到的是图层树与呈现树,上图中的代码,使用的则是图层树中的图层对象。既然渲染服务进程使用的是渲染树,那么图层树中的图层对象所存储的目标值又是如何显示在屏幕上呢?
-UI更新过程在Application的主线程中设置图层树中的图层对象时,被设置的图层对象会被标记为待处理状态(在辅助线程设置图层对象,图层对象不会被标记),当Application的主线程即将进入休眠时,Core Animation会打包图层树中待处理的图层对象,并通过IPC发送到渲染服务进程,IPC是通过端口交互的,消息在两个端口间传递,而渲染服务进程的端口是不公开的(更多关于内核方面的资料可以阅读《OS X与iOS内核编程》),当打包的图层发送到渲染服务进程时,这些图层会被反序列化成渲染树,渲染服务进程便可以开始绘制渲染的工作-RunLoop更新UI的工作
Application的主线程为了保持存活状态,启动了运行循环(RunLoop),RunLoop是一个事件处理循环,使用RunLoop的目的是让你的线程在有工作的时候忙于工作,而没工作的时候处于休眠状态。下图为RunLoop调度的顺序。
从RunLoop调度的顺序得知,当没有未处理事件时,线程就会进入休眠状态。在RunLoop中注册了一个观察者,这个观察者用于监听线程即将进入休眠的状态,当线程即将进入休眠时,观察者会执行监听回调_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv(),这个函数实现了Core Animation打包图层树中待处理的图层对象,并通过IPC发送到渲染服务进程的工作
现在解决问题:第一种 一般人都能明白:在加载数据的时候解决线程的问题就行了!!!
那第二种:
在页面跳转时,除了加载数据,还需要加载UI元素,而加载UI元素的工作一般会在viewDidLoad中完成,如果需要加载的UI元素过多,同样会出现页面跳转延迟的情况。
我们用辅助线程加载数据解决了页面跳转延迟的情况,那么我们可以以同样的方式来加载UI元素。
虽然我们可以把生成UI元素的工作放到辅助线程中完成,且看到的效果相同,但这种处理方式的效率非常低,这种方式生成大量UI元素所需要的时间比直接在主线程中生成要多数倍,增加加载页面所需要的时间,这显然不是我们想要的结果,我们想要的是既可以在主线程生成UI,又可以不出现页面跳转延迟的情况
Core Animation会打包图层树中待处理的图层对象,除了打包图层对象,Core Animation还会打包基础动画对象,一并发送到渲染服务进程,渲染服务进程接收到图层对象和动画对象后,会根据动画对象来不断计算和绘制图层对象,形成屏幕上看到的动画效果,所以动画对象能否及时发送到渲染服务进程就显得非常重要,这关系到你App的用户体验。页面跳转时的过场动画的打包工作,跟viewDidLoad是在同一次RunLoop中,所以viewDidLoad的执行时间就显得很关键
我们发现viewDidAppear方法并没有被调度,即viewDidAppear跟前面几个方法并在不同一次RunLoop中,既然如此,我们可以便使用viewDidAppear来解决页面跳转延迟的情况
从主线程的执行堆栈可得知,viewDidAppear是在过场动画结束后被调用的,而过场动画的持续时间是0.35秒。
我们来算一下整个过程所需要的时间,假设生成页面需要0.5秒,那么优化前后所需要的时间都是0.85秒(经测试,其实时间有减少,只是少到可以忽略,时间减少的部份应该是GPU计算量的问题),虽然问题解决了,但效果并不理想,因为完成整个过程所需要的时间并没有减少,所以我们需要进一步优化。尝试过很多种方式,但似乎没有什么方式可以很好地减少生成UI元素所需要的时间,那么我们只能把优化的方向放在过场动画的持续时间上了
如图所示,把生成UI元素的任务从本次RunLoop中抽出,提交到下一次的RunLoop当中,因为本次RunLoop没有被阻塞,所以能及时把图层对象和动画对象发送到渲染服务进程,渲染服务进程便开始进行过场动画的绘制与渲染,与此同时,Application的主线程RunLoop进入下一次Loop,开始执行生成UI元素的任务,即,可以理解为渲染服务进程绘制渲染过场动画,和Application生成UI元素的任务同时进行,这样我们便把动画的时间也利用上,从而大大减小了整个过程所需的时间。
在Demo中,是使用GCD的方式来实现,也可以使用performSelector: withObject: afterDelay:方法来实现同样的效果,但不建议,因为这样会增加主线程RunLoop的执行时间