在前一篇博客中提到内存抖动和耗时复杂的计算会导致UI卡顿。
那为什么内存抖动会导致UI卡顿呢?
其实在 性能优化一之内存与垃圾回收器 这篇文章中已经有所提及。
这里来详细说明一下:
渲染功能是应用程序最普遍的功能,开发任何应用程序都是这样,一方面,设计师要求为用户展现可用性最高的超然体验,另一方面,那些华丽的图片和动画,并不是在所有的设备上都能流畅地运行。我们来了解一下什么是渲染性能。
首先,我们要知道Android系统每隔16ms就重新绘制一次Activity,也就是说,我们的应用必须在16ms内完成屏幕刷新的全部逻辑操作,这样才能达到每秒60帧,然而这个每秒帧数的参数由手机硬件所决定,现在大多数手机屏幕刷新率是60赫兹(赫兹是国际单位制中频率的单位,它是每秒中的周期性变动重复次数的计量),也就是说我们有16ms(1000ms/60次=16.66ms)的时间去完成每帧的绘制逻辑操作,如果错过了,比如说我们花费34ms才完成计算,那么就会出现我们称之为丢帧的情况。
、
安卓系统尝试在屏幕上绘制新的一帧,但是这一帧还没准备好,所以画面就不会刷新。如果用户盯着同一张图看了32ms而不是16ms,用户会很容易察觉出卡顿感,哪怕仅仅出现一次掉帧,用户都会发现动画不是很顺畅,如果出现多次掉帧,用户就会开始抱怨卡顿,如果此时用户正在和系统进行交互操作,例如滑动列表或者输入数据,那么卡顿感就会更加明显,用户会毫不留情地对我们的应用进行吐槽。
那UI内存抖动跟绘制界面又有什么关系呢?
原因在于内存抖动的发生导致内存中短时间内生成很多对象又在短时间内马上释放,而app分配的堆内存是有限的,这样就会导致app堆内存接近溢出值时,强制GC启动,你分配的内存越多越频繁,GC的启动次数也就越多越频繁,都知道执行GC操作的时候,任何线程的任何操作都会需要暂停,等待GC操作完成之后,其他操作才能够继续运行(所以垃圾回收运行的次数越少,对性能的影响就越少),这样子就会导致UI线程暂时停止,让GC先去回收内存,但是当绘制与gc时间超过了16ms就会引起上面所说的掉帧现象。
接下来介绍耗时计算对UI绘制的影响。
首先我们先去了解下渲染管线:
Android系统的渲染管线分为两个关键组件:CPU和GPU,它们共同工作,在屏幕上绘制图片,每个组件都有自身定义的特定流程。我们必须遵守这些特定的操作规则才能达到效果。
在CPU方面,最常见的性能问题是不必要的布局和失效,这些内容必须在视图层次结构中进行测量、清除并重新创建,引发这种问题通常有两个原因:一是重建显示列表的次数太多,二是花费太多时间作废视图层次并进行不必要的重绘,这两个原因在更新显示列表或者其他缓存GPU资源时导致CPU工作过度。
在GPU方面,最常见的问题是我们所说的过度绘制(overdraw),通常是在像素着色过程中,通过其他工具进行后期着色时浪费了GPU处理时间。
接下来我们将讲解更多关于失效布局和重绘的内容。
CPU和 GPU:
想要开发一款性能优越的应用,我们必须了解底层是如何运行的。有一个主要问题就是,Activity是如何绘制到屏幕上的?那些复杂的XML布局文件和标记语言,是如何转化成用户能看懂的图像的?
实际上,这是由格栅化操作来完成的, 格栅化 就是将例如字符串、按钮、路径或者形状的一些高级对象,拆分到不同的像素上在屏幕上进行显示,格栅化是一个非常费时的操作。我们所有人的手机里面都有一块特殊硬件,它就是图像处理器(GPU 显卡的处理器 ),目的就是加快格栅化的操作,GPU在上个世纪90年代被引入用来帮助加快格栅化操作。
GPU使用一些指定的基础指令集,主要是多边形和纹理,也就是图片,CPU在屏幕上绘制图像前会向GPU输入这些指令,这一过程通常使用的API就是Android的OpenGL ES,这就是说,在屏幕上绘制UI对象时无论是按钮、路径或者复选框,都需要在CPU中首先转换为多边形或者纹理,然后再传递给GPU进行格栅化。
我们要知道,一个UI对象转换为一系列多边形和纹理的过程肯定相当耗时,从CPU上传处理数据到GPU同样也很耗时。所以很明显,我们需要尽量减少对象转换的次数,以及上传数据的次数,幸亏,OpenGL ES API允许数据上传到GPU后可以对数据进行保存,当我们下次绘制一个按钮时,只需要在GPU存储器里引用它,然后告诉OpenGL如何绘制就可以了,一条经验之谈: 渲染性能的优化就是尽可能地上传数据到 GPU ,然后尽可能长地在不修改的情况下保存数据,因为每次上传资源到 GPU 时,我们都会浪费宝贵的处理时间 ,Android系统的Honeycomb版本发布之后,整个UI渲染系统就在GPU中运行,之后各个版本都在渲染系统性能方面有更多改进。
Android系统在降低、重新利用GPU资源方面做了很多工作,这方面完全不用担心,举例说,任何我们的主题所提供的资源,例如Bitmaps、Drawables等都是一起打包到统一的纹理当中,然后使用网格工具上传到GPU,例如Nine Patches等,这样每次我需要绘制这些资源时,我们就不用做任何转换,他们已经存储在GPU中了,大大加快了这些视图类型的显示。然而随着UI对象的不断升级,渲染流程也变得越来越复杂,例如说绘制图像,就是把图片上传到CPU存储器,然后传递到GPU中进行渲染。路径使用时完全另外一码事,我们需要在CPU中创建一系列的多边形,甚至在GPU中创建掩蔽纹理来定义路径。绘制字符就更加复杂一些,首先我们需要在CPU中把字符绘制制成图像,然后把图像上传到GPU进行渲染再返回到CPU,在屏幕上为字符串的每个字符绘制一个正方形。
现在Android系统已经解决了大多数性能问题,除非我们还有更高要求,我们基本不会发现与GPU相关的问题,然后还有一个GPU性能问题瓶颈,这个问题困扰着每个程序员,这就是过度绘制(下篇博文讲解)。
所以耗时计算之所以会导致UI卡顿,原因在于耗时操作占用了CPU的很多时间,影响了CPU在转换为多边形或纹理的效率,进而影响GPU的渲染时间,导致界面不能在16ms之内绘制完成,造成UI卡顿。
那我们针对这些代码如何去优化它,使他不再耗时呢?
有两种解决办法:
1、 重写代码
介绍我最喜欢的两个性能技术,批处理(batching)和缓存(caching)。前面我们已经说过一些函数和运算,需要非常大的资源开销,这也会影响计算性能。例如,在执行之前把数据载入特定的内存区域,或者,在搜索之前对数值集进行排序,在执行多次之后,而且次数确实是个很大的数字,资源开销将会严重影响应用程序的性能。批处理是可以帮助解决这种性能问题,它消除每个运算的独立执行开销,好像是所有人都开一辆车,而不是每个都开一辆,从而节省汽油。这种情况最常见于在执行运算之前,你需要准备数据。例如,在查找集合中的值时,最有效的方法是进行排序,然后进行二分法搜索等等。有一点必须弄清楚,这并不是最有效的方法。这只是举一个例子而已,最简单的方法是写一个函数,提供一个集合和一个值,对集合进行排序,然后查看值是否存在于集合之中。对于某些性能要求来说,这样做是可以的。但是,如果有10000个值,而且总共需要数百万组数据,排序所花费的时间,将会增加很多倍,答案很明确。对这组数据一次性完成排序,然后查找所有10000个值,并不是明智的方法。这时就需要用到批处理,我们找到重复的运算,找到之后,进行批处理。
缓存是与批处理相似的概念,这也是目前为止,你能理解的最重要的性能技术。这项技术全面地推动现代计算机科学的发展,以计算机为例,内存的作用是用来存储信息,让CPU能够更快的访问数据,其速度远快于访问硬盘数据。
或者以网络为例,世界各地存在大型服务器仓库,它们被称为数据中心,它们的作用是存储并缓冲被频繁访问的内容。这样,你的计算机就不必每次都访问远在12000英里之外的服务器。你在埃及的朋友可能在这个服务器上发布了一张图片,当然,如果你在埃及,这样的缓冲服务器可能就没有什么意义,但是你已经明白其中的道理。以代码为例,最常见的缓存优化通常涉及多次计算,但是如果始终相同的数据,例如,在循环计算中,你计算一个4x4数列的导数,结算始终是相同的,每次重新计算循环迭代,实际是在浪费计算机资源。相反,在循环流程的外部存储导数的结果,并让你的内部循环语句引用缓存结果,可以极大地提升效率。我之所以喜爱缓存和批处理,是因为他们能够改善所有你能够想到的性能问题,包括在上篇博客提到的解决Febonacci数列的方法就是利用了这两个非常有效的技术。如果你想成为一名性能专家,你最好能够熟练掌握这两项强大的技术。
2、 优化代码
为了确保应用程序的高性能,每项功能都应该尽可能高效地运行。但是这些功能的执行时间以及它们在代码中所处的位置也很重要,当你首次启动一个Android应用程序时,主线程就已经创建了,主线程非常重要,因为它负责运行你的代码,并在合适的视图位置发送事件和执行绘图功能。这些前面我们已经讲过,基本上来说,主线程是应用程序所在的线程,有时候,主线程也称为UI线程。例如,如果你触摸屏幕上的按钮,UI线程将会发送一个触摸事件给视图,视图将按钮状态设定为已按下状态,然后向事件队列发送一个有效请求,然后UI线程处理此请求,并通知按钮将其本身绘制为已按下状态。如果你有任何触摸事件的处理代码块,将会在线程中执行,这些触摸处理所用的时间越长,线程的执行时间就会越长,在绘图功能执行完之前,视图将会更新显示状态,让用户能够看到其状态,这里需要记住的是,输入处理代码与渲染和更新代码,共享这个线程的处理周期时间。
这意味着,在触摸事件处理,网络访问或数据库查询等计算周期时间,UI不会更新绘图,在简单的情况下,渲染周期可能会延误16毫秒左右,而让用户感到延迟。但是,如果你暂停UI线程渲染超过5秒,用户将会看到“应用程序未响应”对话框,并询问用户是否会想要关闭你的应用程序,这样可能导致用户停止使用。那你如何解决这个问题,你要找出不需要在主线程上执行的功能,也就是说,不需要等它们完成之后,才能执行绘图。你应该将这个功能转移到一个单独的独立线程,这个线程不会阻止UI线程。例如,如果你按一下提交按钮,以完成一个订单,然后编写和发送确认邮件,这可以在单独的线程上完成。Android有系列很好用的API,能够简化这些工作。