页面间跳转的性能优化(一)

页面间跳转的性能优化(一)

来源:Delpan

链接:http://www.jianshu.com/p/77847c0027c9

前言

现在App的页面越来越复杂,页面初始化的工作越来越多,加载页面所需的时间也随之增长,如果页面加载的时间过长,这将会影响App的流畅度及用户体验,我们需要解决这一问题。观察过一些日常使用的App,页面间跳转的性能问题总结为以下三种情形:

1).A页面跳转到B页面,由于B页面需要加载大量的数据,所以导致页面跳转延迟。

2).A页面跳转到B页面,由于B页面需要加载大量UI元素,所以导致页面跳转延迟。

3).A页面跳转到B页面,由于A或B页面的GPU使用率过高,所以导致面页跳转时出现过场动画不流畅,缓慢等。

情形一比较容易解决,利用辅助线程加数据即可;由于图层树的更新(即UI页面的更新)需要在主线程上完成,所以情形二的性能优化让很多开发人员头痛;虽然网上有很多视图性能优化的技术文,但据了解,其实大部份团队都不会去做视图的性能优化,情形三也是最普遍存在。本文将会讲述这三种情形的性能优化,但并不会讲述页面间跳转的过渡动画,及页面间跳转的原理,这部份在网上已经有大量技术文讲述。关于情形三所涉及的像素混合,像素对齐,离屏渲染等知识点将不进行讲述,本文会讲述一种偷懒的方式来优化情形三。

点击下载Demo,或https://github.com/IOSDelpan/SmoothTransitionDemo。

目录

基础知识

-渲染服务进程

-UIView与CALayer

-图层树,呈现树,渲染树

-UI更新过程

-RunLoop更新UI的工作

情形一

情形二

基础知识

想在屏幕上显示一个视图,我们只需要简单地实现以下代码,并运行Application到模拟器或真机即可。

-渲染服务进程

虽然看到的效果跟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发送到渲染服务进程的工作。本文不会提及深入的RunLoop原理,深入部份会在RunLoop篇讲述。

情形一

绝大多数的App页面都是用来展示各式各样的数据,如果跳转页面的同时,在主线程加载大量的数据,便会出现以下情况。

如Gif图所示,屏幕卡顿了一会才出现页面跳转的过场动画,即出现了页面跳转延迟的情况。从基础知识的UI更新过程,RunLoop更新UI的工作中得知,Application的UI更新在于主线程即将进入休眠时,RunLoop观察者的回调函数_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv(),只要该函数执行完,我们就可以在屏幕上看到UI更新的结果。既然知道这是由于在主线程加载大量数据所致,那么我们来解决这一情形,首先需要知道是那个函数占用了CPU,使用Instruments的Time Profiler测试一下。

从测试的结果可以看到,是setUpData这个方法占用了主线程,而setUpData方法是在viewDidLoad里被调用的,那么viewDidLoad又是在何时被调用的呢?

从主线程活动的状态以及执行堆栈可以看出,viewDidLoad是在_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()里被调用的,大致过程如下图。

知道了问题函数和主线程的执行堆栈,那么解决这一问题就变得很简单。只需要把加载数据的setUpData方法放到辅助线程中执行并返回结果到主线程显示即可。

当我们使用多线程去加载数据时,由于主线程没有被阻塞,所以没有出现页面跳转延迟的情况,具体代码请看Demo。

情形二

在页面跳转时,除了加载数据,还需要加载UI元素,而加载UI元素的工作一般会在viewDidLoad中完成,如果需要加载的UI元素过多,同样会出现页面跳转延迟的情况。

如Gif图所示,出现了页面跳转延迟的情况,这是由于在viewDidLoad中生成大量的UI元素所致。在情形一中,我们用辅助线程加载数据解决了页面跳转延迟的情况,那么我们可以以同样的方式来加载UI元素。

虽然我们可以把生成UI元素的工作放到辅助线程中完成,且看到的效果相同,但这种处理方式的效率非常低,这种方式生成大量UI元素所需要的时间比直接在主线程中生成要多数倍,增加加载页面所需要的时间,这显然不是我们想要的结果,我们想要的是既可以在主线程生成UI,又可以不出现页面跳转延迟的情况。

我们知道当Application的主线程即将进入休眠时,Core Animation会打包图层树中待处理的图层对象,除了打包图层对象,Core Animation还会打包基础动画对象,一并发送到渲染服务进程,渲染服务进程接收到图层对象和动画对象后,会根据动画对象来不断计算和绘制图层对象,形成屏幕上看到的动画效果,所以动画对象能否及时发送到渲染服务进程就显得非常重要,这关系到你App的用户体验。页面跳转时的过场动画的打包工作,跟viewDidLoad是在同一次RunLoop中,所以viewDidLoad的执行时间就显得很关键。除了viewDidLoad以外,在UIViewController的生命周期里还有另外几个方法,我们来看一下这几个方法的被调度的情况。

从打印信息中得知,viewWillAppear,viewWillLayoutSubviews,viewDidLayoutSubviews是紧跟viewDidLoad之后执行的,所以这几个方法的执行时间同样很重要,但我们发现viewDidAppear方法并没有被调度,即viewDidAppear跟前面几个方法并在不同一次RunLoop中,既然如此,我们可以便使用viewDidAppear来解决页面跳转延迟的情况。

Gif图显示的效果和根据基础知识猜想的结果一样,解决了页面跳转延迟的情况,那么viewDidAppear何时被调用?

从主线程的执行堆栈可得知,viewDidAppear是在过场动画结束后被调用的,而过场动画的持续时间是0.35秒。

我们来算一下整个过程所需要的时间,假设生成页面需要0.5秒,那么优化前后所需要的时间都是0.85秒(经测试,其实时间有减少,只是少到可以忽略,时间减少的部份应该是GPU计算量的问题),虽然问题解决了,但效果并不理想,因为完成整个过程所需要的时间并没有减少,所以我们需要进一步优化。尝试过很多种方式,但似乎没有什么方式可以很好地减少生成UI元素所需要的时间,那么我们只能把优化的方向放在过场动画的持续时间上了。

从Gif图显示的效果可以看到,完成整个过程所需要的时间明显减少了,实现原理请看下图。

如图所示,把生成UI元素的任务从本次RunLoop中抽出,提交到下一次的RunLoop当中,因为本次RunLoop没有被阻塞,所以能及时把图层对象和动画对象发送到渲染服务进程,渲染服务进程便开始进行过场动画的绘制与渲染,与此同时,Application的主线程RunLoop进入下一次Loop,开始执行生成UI元素的任务,即,可以理解为渲染服务进程绘制渲染过场动画,和Application生成UI元素的任务同时进行,这样我们便把动画的时间也利用上,从而大大减小了整个过程所需的时间。

在Demo中,是使用GCD的方式来实现,也可以使用performSelector: withObject: afterDelay:方法来实现同样的效果,但不建议,因为这样会增加主线程RunLoop的执行时间。

我们还可以把这个耗时的任务分解成若干个小的任务来实现。

如Gif图所示,没有出现页面跳转延迟的情况。使用定器时把任务分解,可以得到同样的结果,若是加上一些动画,效果会更棒。在Demo中,用到的定时器是CADisplayLink,用NSTimer可以达得到样的效果,关于CADisplayLink,建议能不用就不用,因为它会使目标线程长期处于活跃状态。

情形三将会在页面间跳转的性能优化(二)中讲述。如果文中有讲错的地方,还望指出。

Tips:虽然黑科技很强大,但也很危险,在你没有足够了解它的情况下,不能轻易去使用,更不能滥用。本文的讲述旨在如何利用基础知识来解决日常开发中遇到的问题,并不是硬式化地讲解使用方式。

阅读 281712 投诉

精选留言

写留言

5

wh

放在 view did appear 每次调用那个页面都会执行一次吧 比如 返回这个页面 也会调用一次

2天前

3

DENG

这个好,mark

3天前

3

003

涨知识

3天前

以上留言由公众号筛选后显示

了解留言功能详情

 

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268

推荐阅读更多精彩内容

  • 前言 现在App的页面越来越复杂,页面初始化的工作越来越多,加载页面所需的时间也随之增长,如果页面加载的时间...
    Delpan阅读 22,440评论 93 464
  • 续言 在页面间跳转的性能优化(一)中介绍了一些基础知识,讲述了情形一与情形二的优化方式及原理,但有许多人对情...
    Delpan阅读 7,980评论 32 95
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,565评论 25 707
  • 寒降南山,霜满前庭。东风恶、尤胜刀兵。 三春桃李,一夜霜清。见天残云,人残影,树残形。 匹年穷岁,长情笃信。笑书生...
    十万字的梦想阅读 327评论 2 6
  • 1、复制字符串到剪切板 2、打电话 创建一个成员变量UIWebView来加载URL,拨完后能自动回到原应用 3、发...
    univer2012阅读 160评论 0 0