这是一个关于星空的自定义动画Sample,源码请戳 https://github.com/wangfuda/nebula
Gif图:
本文将重点讲解在本例自定义动画编程中,如何结合 Android Studio 的 Memory Monitor,GPU monitor 按步骤做内存优化,GPU渲染优化。
关于动画实现部分,源码已提交至github,请手动阅读理解,注释很详尽。
内存占用优化
内存占用优化 步骤一:移动图片资源至大分辨率目录下,比如xxxhdpi.
先来彪一张直接撸完代码无任何优化的情况下,内存的占用图:
内存占用246M,不能忍。
问:为什么这么大?
答:因为资源图都是高清1K分辨率的图。
问:为什么这么大?
答:。。。
那么我们算一下这246M内存占用是怎么来的吧。
先来一条图片内存占用计算公式,公式溯源请自行去看源码:BitmapFactory.Java & BitmapFactory.cpp
scaledWidth = int( Width * targetDensity / density + 0.5)
scaledHeight = int( Height * targetDensity / density + 0.5)
memory = scaledWidth * scaledHeight * 4
其中参数定义如下:
Width:图片宽
Height:图片高
targetDensity:加载图片的目标手机的 density,这个值的来源是 DisplayMetrics 的 densityDpi,如果是小米note那么这个数值就是480,详见下图关于targetDensity的参数细节。
density:decodingBitmap 的 density,这个值跟这张图片的放置的目录有关(比如 hdpi 是240,xxhdpi 是480)
每像素字节数:ARGB8888格式的图片,每像素占用 4 Byte,而 RGB565则是 2 Byte。
对于一张1080x1920的图来说,放置在hdpi目录,并在小米note手机上(分辨率1080x1920,targetDensity为480),而且均默认以ARGB8888格式加载。
内存占用计算公式:
scaledWidth = int( 1080* 480/ 240+ 0.5) =2160
scaledHeight = int( 1920* 480/ 240+ 0.5)=3840
memory = scaledWidth * scaledHeight * 4=216038404 = 33177600 = 33.17M
一张背景图就占用33M,这个分辨率的图,我们res下一共有7张,还有其他几张小图。这回可以回答为什么占用245M的内存了。
那么内存占用的优化方案也就有了,我们尽量把图片资源放到大分辨率目录下,比如xxxhdpi(当然还有个前提,你的图片分辨率也确实符合大分辨率,否则会出现在大分辨率设备上,显示不全的问题)。
我们来看看把图片资源移动到xxxhdpi目录下后,内存占用情况:
把图片资源从hdpi移动到xxx-hdpi,从246M降低到56M,减少了190M,Bingo!
内存占用优化 步骤 二:压缩png图片大小(包体大小会减小,但与内存占用情况无关)
初始单张图片大小都接近2M,经过tinypng优化后,压缩率达到70-80%,非常完美,包体大小减小了,不过,经过我们步骤一的科学计算,这个优化并不会影响图片在内存中的占用。
内存占用优化 步骤 三:动画完成且不再循环展示的部分,相关bitmap释放
public void releaseBitmap {
...
bitmap.recycle();
...
}
来看下bitmap释放后的memory monitor图:
内存占用降低到36M,减少了20M
内存占用优化 步骤 四:无用对象释放,非透明背景图片采用RGB_565颜色格式,并且将图片的inSampleSize设置为2
public void releaseValueAnimator {
...
valueAnimator = null;
...
}
public void initBitmap {
...
localOptions.inSampleSize = 2;
localOptions.inPreferredConfig = Bitmap.Config.RGB_565;
...
}
来看下变更图片颜色格式及采样率后的memory monitor图:
内存占用降低到28M,减少了8M(后来monitor截图只对背景图片做inPreferredConfig调整,内存占用变为30M,相比全部设置为2,增加了2M)
GPU渲染优化
接下来我们要专注于GPU渲染优化了。通过前面几张图,也能看到GPU monitor的状态,非常不乐观,完全达不到帧率刷新的要求:即每帧渲染不超过16ms,每秒可以渲染60帧。
先看GPU Monitor的各项指标含义:
Misc Time:表示在主线程执行了太多的任务,导致UI渲染跟不上vSync的信号而出现掉帧的情况;出现该线条的时候,可以在Log中看到这样的日志: Skipped xxx frames! The application may be doing too much work on its main thread
Swap Buffers:表示处理任务的时间,也可以说是CPU等待GPU完成任务的时间,线条越高,表示GPU做的事情越多;
Command Issue:表示执行任务的时间,这部分主要是Android进行2D渲染显示列表的时间,为了将内容绘制到屏幕上,Android需要使用Open GL ES的API接口来绘制显示列表,红色线条越高表示需要绘制的视图更多;
Sync:表示的是准备当前界面上有待绘制的图片所耗费的时间,为了减少该段区域的执行时间,我们可以减少屏幕上的图片数量或者是缩小图片的大小;
Draw:表示测量和绘制视图列表所需要的时间,蓝色线条越高表示每一帧需要更新很多视图,或者View的onDraw方法中做了耗时操作;
Measure/Layout:表示布局的onMeasure与onLayout所花费的时间,一旦时间过长,就需要仔细检查自己的布局是不是存在严重的性能问题;
Animation:表示计算执行动画所需要花费的时间,包含的动画有ObjectAnimator,ViewPropertyAnimator,Transition等等。一旦这里的执行时间过长,就需要检查是不是使用了非官方的动画工具或者是检查动画执行的过程中是不是触发了读写操作等等;
Input Handling:表示系统处理输入事件所耗费的时间,粗略等于对事件处理方法所执行的时间。一旦执行时间过长,意味着在处理用户的输入事件的地方执行了复杂的操作;
Vsync Delay:见Misc Time
GPU渲染优化 步骤一:优化内存占用
可以回过头去看看内存优化过程演进中的Monitor图,随着内存占用的降低,GPU渲染的性能改善也是随之渐进的,所以GPU渲染性能优化,首选就是内存优化
GPU渲染优化 步骤二:能在初始化中做的事,坚决不在onDraw中搞。
在sample代码中重构了onDraw中的画笔的属性设置,绘制区域的创建等代码,这些代码都重构到初始化中。而在onDraw中仅做参数值的动态调整。
Tips.本例Sample最初始未经过任何优化的代码及最终优化版本代码均在github上可以查看到commit记录,这里不再写详细代码对比,请移动github阅读源码。
我们看下重构前后的对比图monitor_5_bitmap_RGB_565_inSampleSize2.png
Vs monitor_6_gpu_optimize_object_and_paint_create.png
,对比发现,GPU渲染耗时明显降低。
我们暂时先对比看GPU Monitor的 0s ~ 9s 部分的性能改善,目前GPU优化主要在这里体现,因为后半部分各位爷看到了,GPU渲染耗时飙升,掉帧严重,那部分的优化在后续步骤会提到。
重构前 GPU Monitor:
重构后 GPU Monitor:
GPU渲染优化 步骤三:能用硬件加速,就别关闭它。
我们都看到了,在GPU Monitor中显示,9s后的GPU渲染,每帧耗时突然飙升,每帧渲染都是超60ms,
Draw上升至172ms,Vsync上升至148ms.
而且在logcat中也看到日志:
07-09 11:17:57.135 15980-15980/com.osan.nebula I/Choreographer: Skipped 31 frames! The application may be doing too much work on its main thread.
每帧超时,掉帧严重。到底是什么原因呢?经过反复的排查,终于找到原因,我们来聊聊这个飙升的来龙去脉。
由于在后半部分的动画中,绘制小星星的光晕效果时,使用的画笔设置了模糊属性,为了给小星星加个光晕效果。
paintCircleStar.setMaskFilter(new BlurMaskFilter(10, BlurMaskFilter.Blur.SOLID));
然后google官方文档都说了,硬件加速不支持的UI特效API之一就有它。也就是你要用硬件加速,这个模糊效果就失效。所以我一门心思的为了给小星星加光晕... 光晕...晕...,最后我选择了关闭硬件加速
private void drawState7(Canvas canvas) {
...
canvas.save();
setLayerType(LAYER_TYPE_SOFTWARE, null);//关闭硬件加速
canvas.translate(halfWidth, halfHeight);
canvas.scale(scale, scale);
canvas.rotate(30f * mValue7);
...
}
呵呵,为了小星星,闯祸了。因为本例动画中,各种对画布的旋转,缩放,变换,透明度动态变化,在非硬件加速情况下,不停的重新绘制,是GPU渲染耗时飙升的唯一原因。
我们来看看开启硬件加速的情况下,GPU Monitor的指标监控:
恢复硬件加速后,渲染耗时立刻恢复到绿线以下,即每帧渲染不超过16ms,达到渲染标准。而且,CPU消耗也明显下降。
那么为什么硬件加速有如此神奇之功效?
使用硬件加速在对一些view的属性改变上有更高的效率,因为不需要view的invalidate和redrawn。而我们动画中正式大量使用了对属性的改变。属性如:
透明度:alpha
移动:x, y, translationX, translationY
缩放:scaleX, scaleY
旋转:rotation, rotationX, rotationY
坐标:pivotX, pivotY
注:
1)使用硬件加速,对于渲染性能的提示是显著的,API>14后,硬件加速是默认开启的。
2)硬件加速还不支持所有的2D绘图命令,开启后可能会影响自定义View和绘图操作。
GPU渲染优化 步骤四:优化算数运算,并尽量从ondraw中移除算数运算
涉及计算任务,能不在ondraw中执行的,就坚决移走,即使只是一个a*b或a/b.
因为我们继续重构了onDraw方法,将可能优化的运算代码均做了优化,能放到初始化做的就移到初始化,能提炼共用的运算公式就共用,能不重复算的就绝对不算第二遍。
我们来看看优化算数运算后,GPU Monitor的指标监控:
相比步骤三中的GPU Monitor指标有了进一步降低,虽然降低幅度很小,但是还是对GPU渲染性能提升有效果的,而且观察发现,优化后,CPU和GPU的指标看起来更平稳。
最后附上一张coding过程中的草图以及nebula自定义动画的截图