美文网首页
Android性能之渲染(一)

Android性能之渲染(一)

作者: Hawkinswang | 来源:发表于2017-02-28 20:44 被阅读0次

    做过Android开发的人都知道,有时候我们给app加上一些绚丽的动画,运行app的时候却发现app出现明显的卡顿,这严重影响了用户的体验。接下来我要从渲染来讲解为什么会出现这种问题以及如何解决这种问题?

    首先,我们来谈谈渲染!


    渲染功能是应用程序最普遍的功能,Android系统每隔16ms重新绘制一次Activity,也就是说,你的应用程序必须在16ms内完成屏幕刷新的全部逻辑操作,这样才能达到每秒60帧。这个每秒帧数的参数实际上来源于手机硬件,定义了屏幕每秒刷新速度有多快,我们大多数手机屏幕刷新率大概在60赫兹,这就意味着你有60ms的时间去完成每帧的绘制逻辑操作。如果错过了,比如说你花费了24ms才完成计算,那些就会出现失帧的情况,Android系统尝试在屏幕上绘制新的一帧,但是这一帧还没准备好,所以画面就不会刷新,用户盯着同一张图看了32ms而不是16ms。失帧情况下丢失的任何动画,用户很容易察觉出卡顿感,哪怕仅仅出现一次失帧,用户都会发现动画不是很顺畅,如果出现多次失帧,用户就会开始抱怨卡顿。

    上面我们对每帧花费的事件有了清晰的了解,接下来我们来看看是什么原因导致了卡顿,以及如何去解决应用中的这些问题?

    Android系统的渲染,分为两个关键组件CPU和GPU。两者共同在屏幕上绘制图片,每个组件都有自身定义的特定流程,你必须遵守这些特定的操作规则才能达到效果。

    CPU方面,最常见的性能问题是不必要的布局和失效, 这些内容必须在视图层析结构中进行测量、清除并重新创建。引发这种问题通常有两个原因,一是重建显示列表的次数太多,而是花费太多的事件作废视图层次,并进行不必要的重绘,这两个原因在更新显示列表,或者其他缓存GPU资源时,导致CPU工作过度;第二个问题主要出在GPU方面,

    就是我们所说的透支,通常是在像素着色过程中,通过其他工具进行后期着色,浪费了GPU处理时间。

    下图中可以看出渲染的常见三个问题,不必要的布局layouts、失效invalidations、过度绘制(overdraw)

    想要开发一款性能优越的app,你必须了解底层是如何运行的,如果你不知道硬件是如何运行的,你就无法熟练使用它。Activity是如何绘制到屏幕上的?或者说,那些复杂的xml布局文件和标记语言是如何转换成用户能看懂的图像?

    实际上,这是由格栅化操作来完成的,格栅化将诸如字符串、按钮、路劲或者形状的一些高级对象拆分到不同的像素上在屏幕上进行显示。格栅化是一个非常耗时的操作,也就是说你的手机里有一块特殊硬件,目的就是加快格栅化的操作,图像处理单元,也就是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等都是一起打包到统一的纹理中,然后使用网格工具(如Nine Patches)上传到GPU,这就意味着,你每次需要绘制这些资源时,你不用做任何转换,因为它们已经存储在GPU中了,大大加快了这些视图类型的显示。然而随着UI对象的不断升级,渲染流程也变得越来越复杂,例如说,绘制图像,就是把图片上传到CPU存储器,然后传递到GPU中进行渲染,路径使用是完全不同一码事,你需要在CPU中创建一系列的多边形,甚至在GPU中创建掩蔽纹理来定义路径;还有,绘制字符也很复杂,首先我们需要在CPU中把字符绘制成图像,然后把图像上传到GPU进行渲染再返回到CPU,在屏幕上为字符串的每个字符绘制一个正方形。

    上面我们稍微了解了渲染的工作机制,接下来我们就是说一个困扰我们大部分程序的问题,GPU性能问题瓶颈——Overdraw(过度绘制)。

    如果你曾今装修过房子,应该知道,可能墙壁的装饰不符合我们的美观,我们就需要在以前的基础上在刷一遍漆,新刷的漆就覆盖了以前的,墙壁可能刷了好几次,让费了资源,也让费了时间,这个情形很像Android中的Overdraw问题。

    Overdraw,指在一帧的时间内像素被绘制了多少次,理论上一个像素每次只绘制一次是最优的。但是由于我们的布局一般都是重叠的,不管是可见的,还是不可见的,都可能绘制一次,这就导致了一些像素会被多次绘制。这是一个非常大的问题,因为我们的渲染的像素对我们最终显示在屏幕上没有任何用处,造成了GP性能的浪费。

    为了最大化优化你应用的性能,你就应该尽量避免Overdraw。

    那么我们如何来查看应用是否Overdraw呢?这很简单,只需要打开手机的设置——开发者选项——调试GPU过度绘制,如下图

    打开以后,你将会看到自己的应用上出现几种颜色变化,每种颜色代表的含义如下

    没有颜色: 意味着没有overdraw。像素只画了一次。

    蓝色: 意味着overdraw 1倍。像素绘制了两次。大片的蓝色还是可以接受的(若整个窗口是蓝色的,可以摆脱一层)。

    绿色: 意味着overdraw 2倍。像素绘制了三次。中等大小的绿色区域是可以接受的但你应该尝试优化、减少它们。

    浅红: 意味着overdraw 3倍。像素绘制了四次,小范围可以接受。

    暗红: 意味着overdraw 4倍。像素绘制了五次或者更多。这是错误的,要修复它们。

    清除Overdraw有两种主要的方式,首先,清除不必要的背景和图片:其次,你可以定义view隐藏的屏幕位置区域,也就是说,隐藏起来的部分不渲染,这样会减少CPU和GPU的开销。

    一、清除不必要的背景和图片

    1. 去掉window的默认背景

    当我们使用了Android自带的一些主题时,window会被默认添加一个纯色的背景,这个背景是被DecorView持有的。当我们的自定义布局时又添加了一张背景图或者设置背景色,那么DecorView的background此时对我们来说是无用的,但是它会产生一次Overdraw,带来绘制性能损耗。

    去掉window的背景可以在onCreate()中setContentView()之后调用

    getWindow().setBackgroundDrawable(null);

    或者在theme中添加

    android:windowbackground="null";

    2. xml中去掉不必要的背景

    根view设置了背景,如果子view的背景和根view一样,或者子view不需要设置背景,那些字view不能设置背景,完全可以使用根view的背景颜色,否者,就会造成背景多次绘制。

    3. 图片设置背景,已加载到图片则去掉背景

    如果我们需要给imageview设置背景,通常我们可能直接用Picasso加载图片到imageview,然后就调用imageview的setBackgroundColor方法,但是这样会图片和背景颜色会绘制两只,造成Overdraw。

    其实,我们完全可以在加载到图片的时候去掉背景,在没加载到图片的时候设置背景,这样就解决了Overdraw的问题,解决的代码如下:

    if (chat.getAuthor().getAvatarId() == 0) {

    Picasso.with(getContext()).load(android.R.color.transparent).into(chat_author_avatar);

    chat_author_avatar.setBackgroundColor(chat.getAuthor().getColor());

    } else {

    Picasso.with(getContext()).load(chat.getAuthor().getAvatarId()).into(chat_author_avatar);

    chat_author_avatar.setBackgroundColor(Color.TRANSPARENT);

    }

    二、自定义View的隐藏部分不渲染(clipRect和quickReject)

    Android会设法避免绘制那些在最终图片中不显示的UI组件,这种优化类型称作剪切,它对UI性能非常重要。如果你能确定某个对象完全被阻挡,你完全没有必要绘制它,事实上,这是最重要的性能优化方法之一,而且是由Android系统执行的。但是必行的是,这一技术,无法应对复杂的自定义控件,系统无法检测onDraw具体会执行什么操作,这些情况下,底层系统无法识别如何去绘制对象,系统很难将覆盖的view从渲染管道中清除,

    例如,这叠牌只有上面的牌可见,其他牌都被挡住了,这就意味着绘制那些重叠的像素就是浪费时间。为了解决这个问题,我们可以使用具有一些特别方法的Canvas类,去让Android系统识别被遮挡的不需要绘制的部分,最有用的方法是Canvas.clipRect,它可以帮助你识别给定view的图片边界,边界之外区域的任何绘制操作都会被忽略。如果你知道view的可见部分或者被遮挡的部分范围,你就可以使用Canvas.clipRect定义边界,可以避免遮挡区域的任何绘制操作。ClipRect API帮助系统识别出无需绘制的区域,对自定义view进行剪切时,这个方法也很有用处,比如说,如果你知道绘制对象在剪辑矩形之外,这个方法就非常好用。幸运的是,你不必要亲自搞清楚重叠逻辑,我们可以使用Canvas.quickReject方法,判断给定区域是否完全在剪辑矩形之外,这种情况下可以忽略全部绘制工作。

    关于过度绘制就讲到这里,是时候了解一下渲染管道中的CPU部分。

    为了在屏幕上绘制某个东西,Android通常将高级XML文件转换成GPU能够识别的对象,然后显示在屏幕上,这个操作是在DisplayList的帮助下完成的。DisplayList持有所有要交给GPU绘制到屏幕上的数据信息,包含GPU要绘制的全部对象的信息列表,还有执行绘制操作的OpenGL命令列表,在某个view第一次需要被渲染时,DisplayList会因此被创建,当这个view要显示到屏幕上时,我们将绘制指令提交给GPU来执行DisplayList。我们下次渲染这个view时,比如说位置发生了变化,我们仅仅需要执行DisplayList就够了,但是,若果你修改了view的某些可见组件的内容,那么之前的DisplayList就不能用了,这时我们要重新创建一个DisplayList,重新执行渲染指令并更新到屏幕上。

    任何时候view的绘制内容发生变化,都需要重新创建DisplayList,并重新执行指定更新到屏幕上,这个流程的表现性能取决于你的view的复杂程度和视觉变化的类型。比如说,某个文本框尺寸突然变成当前的两倍,在改变尺寸前,需要通过父view重新计算,并摆放其他子view的位置,在这种情况下,我们改变了其中一个view,后面就会有很多工作要做,这些类型的视觉变化需要渲染管道的额外工作。当你的尺寸发生变化时,出发了测量操作,会经过整个View Hierarchy询问各个View的新尺寸,你一旦改变View的大小就会触发上述工程;如果你是改变对象位置或者布局中某个View重新摆放了子view,都会触发布局操作,会触发整个Hierarchy重新计算对象在屏幕上的新位置。

    现在Android系统已经非常有效的处理记录并执行渲染管道,除非你要自定义View或者通知绘制很多View,其他情况下一般不会耗费太多时间,测量和布局性能也很好。但是,当你的View Hierarchy失控时,也更容易出现问题,执行这个功能的时间是和你的View Hierarchy中需要处理的节点数成正比,系统需要处理的View越多,处理时间就越长。造成这些浪费的原因是,View Hierarchy中包含太多的无用View,这些View根本不会显示在屏幕上,一旦触发测量操作和布局操作,只会拖累应用程序的性能。

    当然,Android的SDK中有一款叫做Hierarchy Viewer工具,可以帮助你查找并修复这个流氓View,删除不必要的View,减少布局层级(这里就不介绍Hierarchy Viewer的使用了)。

    关于CPU部分的优化,可以从以下几个方面来讲

    1. 使用Hierarachy Viewer来查找、删除无用的View和层级

    2. 在布局层级相同的情况下,能使用LinearLayout就不要使用RelativeLayout。因为LinearLayout效率更高,RelativeLayout的功能比较复杂,CPU渲染时间更长。

    3. include标签配合merge标签使用,可以重用布局、减少布局层数

    4、使用ViewStub。它是个非常轻量级的View,宽高都为0,因此本身不参与任何的布局和绘制过程。他的意义在于按需要加载所需的布局,需要的时候加载,不需要的时候就不加载进来,提高程序初始化的性能。

    总而言之,渲染问题主要包括三个方面:背景重叠、自定义view重叠(重叠部分不必要绘制)、view的层级过多,想要减少渲染时间,从这个三面去考虑就可以了。

    相关文章

      网友评论

          本文标题:Android性能之渲染(一)

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