Android性能优化(一)

Posted by Csming on 2018-04-15

原文地址:https://www.csdn.net/article/2015-01-20/2823621-android-performance-patterns/1

最近阅读了胡凯的Android性能优化典范,做了一些记录;还有一部分在开发的时候遇到的问题。

绘制问题

画面渲染

  • Android系统每隔16ms发出 VSYNC 信号,触发对UI进行渲染。如果每次都能够渲染成功,则能够达到60fps,即达到人肉眼能够接受的流畅画面。为了达到60fps,需要程序的大多数操作要在16ms内完成。

  • 如果你的某个操作时间超过16ms,那么在系统发出VSYNC信号的时候无法进行正常渲染,这样就会发生丢帧现象。用户会在后面看到同一帧画面

60fps:16ms刷新一次,能够达到60fps。60fps能够达到流畅的原因,是因为人眼与大脑之间的协作无法感知超过60fps的画面更新

这点,在我最近的毕设项目里,利用Hander向自己不停发送sendEmptyMessageDelayed请求的时候,就深有体会。当我的delay时长越接近16ms时,seekbar的处理就越流畅;而delay时长越长,画面就越不流畅。
Android系统的触发渲染也一样。
代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
static class VideoProgressHandler extends Handler {
// WeakReference to the outer class's instance.
private WeakReference<NeroVideoView> mVideoView;
private WeakReference<SeekBar> mSbProgress;

public VideoProgressHandler(NeroVideoView videoView, SeekBar seekbar) {
mVideoView = new WeakReference<>(videoView);
mSbProgress = new WeakReference<>(seekbar);
}

@Override
public void handleMessage(Message msg) {
NeroVideoView videoView = mVideoView.get();
SeekBar sbProgress = mSbProgress.get();
switch (msg.what) {
case UPDATE_PROGRESS: {
//当前时间
if (mVideoView != null) {
int currentPosition = videoView.getCurrentPosition();
sbProgress.setProgress(currentPosition);
sendEmptyMessageDelayed(UPDATE_PROGRESS, 40);
}
break;
}
default: {
break;
}
}
}
}

话说回来: 用户容易在UI执行动画或者滑动ListView的时候感到卡顿;这是因为这里的操作相对复杂。有可能是由于布局太过复杂,无法再16ms内完成渲染。有可能是因为你的UI上有层叠太多的绘制单元,还有可能是因为动画执行的次数过多。

**工具:**可以使用HierarchyViewer来查看布局是否太过复杂;也可以通过开发者选项里的Show GPU Overdraw来观察。

过度绘制

  • 过度绘制指的是:屏幕上某个像素在同一帧内被绘制了多次。(一层一层又一层)而覆盖在下面的层绘制了,但却被新绘制的层覆盖了。这样浪费了大量的CPU以及GPU资源。(看不到还绘制,当然浪费资源了对不对)

Overdraw有时候是因为你的UI布局存在大量重叠的部分,还有的时候是因为非必须的重叠背景。例如某个Activity有一个背景,然后里面的Layout又有自己的背景,同时子View又分别有自己的背景。仅仅是通过移除非必须的背景图片,这就能够减少大量的红色Overdraw区域,增加蓝色区域的占比。这一措施能够显著提升程序性能。

  • 为了提升性能,我们需要尽可能减少过度绘制的发生。

在出现复杂布局的情况下,就会很容易发生过度绘制的现象。
个人认为:ConstraintLayout是一个处理复杂布局非常棒的layout;但是仅在复杂布局下才能发挥其最大的性能,因为ConstraintLayout的绘制效率比LinearLayout、FrameLayout、RelativeLayout低好多,原因可以自行体会。
结论: 复杂布局使用ConstraintLayout,简单布局不使用。

  • **开发者选项的Show GPU Overdraw 能够帮助我们观察UI上的过度绘制情况。**开启这个功能,会把我们的手机屏幕上的元素标记为蓝色、绿色、淡红、深红;我们的目标就是尽量减少红色的发生。

Android的渲染原理

1.Resterization(栅格化):将绘制内容拆分为不同像素上显示。GPU就使用来加快栅格化操作的。

2.CPU负责把UI组件(绘制内容)计算成Polygons(多边形),Texture(纹理),然后交给GPU进行栅格化渲染

每次将数据从CPU转移到GPU是很费事的。
OpenGL ES能够把那些需要渲染的纹理放在GPU Memory中,比较省事;

所以,在处理比较麻烦的动画和交互的时候,也有人用OpenGL ES来处理绘制

在Android里面那些由主题所提供的资源,例如Bitmaps,Drawables都是一起打包到统一的Texture纹理当中,然后再传递到GPU里面,这意味着每次你需要使用这些资源的时候,都是直接从纹理里面进行获取渲染的。当然随着UI组件的越来越丰富,有了更多演变的形态。例如显示图片的时候,需要先经过CPU的计算加载到内存中,然后传递给GPU进行渲染。文字的显示更加复杂,需要先经过CPU换算成纹理,然后再交给GPU进行渲染,回到CPU绘制单个字符的时候,再重新引用经过GPU渲染的内容。动画则是一个更加复杂的操作流程。

Android处理UI组件更新

Android需要将XML布局文件转为GPU能够识别并绘制的对象。
Displaylist用来处理这个事项。Displaylist持有所有将要递交给GPU绘制到屏幕上的数据信息。

过程:
当某个View第一次需要被渲染的时候,它的DisplayList被创建,当这个View将要显示到屏幕上时,会执行GPU的绘制指令来进行渲染。
后续,如果执行移动这个View的位置等操作而需要再次绘制这个View时,我们就仅需要再执行一次绘制指令就够了。
而,如果修改了View中的某个可见组件(绘制内容发生改变),那么之前的DisplayList就不能使用了,需要重新创建一个DisplayList兵重新执行渲染指令更新到屏幕上。

创建DisplayList,渲染DisplayList,更新到屏幕上等一系列操作挺耗时的,如果布局太过复杂,会导致严重的性能问题。

可以使用Monitor GPU Rendering来查看渲染的表现性能,也可以通过Show GPU view updates来查看视图更新操作。还可以使用HierarchyViewer来查看视图。

使布局尽量扁平化,移除非必须的UI组件能够减少Measure,Layout的计算时间。

ClipRect,QuickReject

针对自定义View:

我们可以通过canvas.clipRect()来帮助系统识别那些可见的区域。这个方法可以指定一块矩形区域,只有在这个区域内才会被绘制,其他的区域会被忽视。
可以使用canvas.quickreject()来判断是否没和某个矩形相交,从而跳过那些非矩形区域内的绘制操作。

性能调试工具

  • **1.Profile GPU Rendering(GPU呈现模式分析)**打开On Screen As Bars(在屏幕上显示为条形图)进行调试。

选择之后就能够在手机画面上看到GPU绘制图形信息。分别关于StatusBar,NavBar,激活的程序Activity区域的GPU Rending信息。
随着界面的刷新,界面上会滚动显示垂直的柱状图来表示每帧画面所需要渲染的时间,柱状图越高表示花费的渲染时间越长。

中间横着的横线,代表16ms。如果我们确保每一帧花费的时间都小于16ms,就能避免出现卡顿现象。

每一条柱状线都包含三部分,蓝色代表测量绘制Display List的时间,红色代表OpenGL渲染Display List所需要的时间,黄色代表CPU等待GPU处理的时间。

内存问题

简单说说GC

Android系统有自动管理内存的机制。系统会根据内存中不同的内存数据类型分别执行不同的GC操作。内存块分为Young Generation(新生代)、Old Generation(老生代)、Permanent Generation(持久代)。

了解过JVM的话,一定知道这三者的区别;最近分配的对象会存放在Young Generation区域,当这个对象在这个区域停留的时间达到一定程度,它会被移动到Old Generation,最后到Permanent Generation区域。每隔内存区域都有固定大小,此后不断有新的对象被分配到此区域,当这些对象总的大小达到这些区域的阈值的时候,会出发GC。

**Android是没有标记整理的:**这意味着在GC的时候,Android不会讲内存中的资源整理为连续的内存单元。所以说,大量创建对象后,部分对象如果被回收了,今后创建的对象只能存储在较小块的零碎空白区域,导致下一次触发GC的时间就会缩短。

新生代的对象通常会被快速创建并且很快被销毁回收,新生代的GC操作速度也比老生代快。

在执行GC操作的时候,线程的所有操作都将被暂停,等待GC结束后,才能继续。通常GC操作不会占用太长时间,但是大量的GC就会占用帧间隔的时间了,那么在用户就可能感到明显的卡顿。

频繁执行GC可能是由于:1.内存抖动,大量对象呗创建后有马上被释放。2.瞬间产生大量对象占用新生代区域,达到阈值后出发GC。

内存泄露

不再使用的对象无法被GC识别,这样就导致这个对象一直留在内存当中,占用了宝贵的内存空间。使得每级Generation的内存区域可用空间变小,GC就会更容易被触发,从而引起性能问题

关于内存泄漏:https://blog.csdn.net/anxpp/article/details/51325838

工具

  • Memory Monitor: 查看整个app所占用的内存,以及发生GC的时刻,短时间内发生大量的GC操作是一个危险的信号。
  • Allocation Tracker: 使用此工具来追踪内存的分配,前面有提到过。
  • Heap Tool: 查看当前内存快照,便于对比分析哪些对象有可能是泄漏了的,请参考前面的Case。

电池问题

电池问题不太想展开写……因为电池问题通常都是通过一些定时操作来进行处理的。比较不那么重要。

Purdue University研究了最受欢迎的一些应用的电量消耗,平均只有30%左右的电量是被程序最核心的方法例如绘制图片,摆放布局等等所使用掉的,剩下的70%左右的电量是被上报数据,检查位置信息,定时检索后台广告信息所使用掉的。如何平衡这两者的电量消耗,就显得非常重要了。

  • 应该减少唤醒屏幕的次数和持续时间;
  • 某些非必须马上执行的操作,例如上传歌曲,图片处理等,可以等到设备处于充电状态或者电量充足的时候才进行
  • 触发网络请求的操作,每次都会保持无线信号持续一段时间,我们可以把零散的网络请求打包进行一次操作,避免过多的无线信号引起的电量消耗。

建议一些非必须执行的操作,可以配合Android的JobScheduler进行处理


好困了……要去睡了