在学习了 Androd 的相关基础知识后,自己也动手写了几个项目。IM、 O2O、 Live 等都写过,而且大部分都是自己实现的。可是完成这么多项目之后,并没有感觉自己成为了大神,但是我们和那些大神不都是在做项目么???我们离大神的差距在哪里呢???
当我自己做完了一个 O2O 仿淘宝的项目过后,界面和功能都高仿淘宝,感觉自己可以进淘宝了,没什么毛病。可是一安装使用过后,才发现这 App 要不是自己写的,活不到下一回合啊。卡死了,数据加载太慢了,这还是从自己搭的服务器上获取的少量数据,要是淘宝的数据一放上来估计活不过下一秒。用着用着还老是闪退、崩溃,伤心ing。伤心过后生活还必须正常进行下去,必须解决啊,这里就献上一方良药。那就是性能优化。
性能优化
性能优化对于我们来说很直接的一个点就是解决卡的现象。一个 APP 加载一个页面在网络良好的情况下还要 几秒钟,滑动一下半天才反应过来。相信谁都不想用这个 APP。
卡是怎么原因造成的呢?要理解卡顿的真正原因要先知道 Android 的渲染机制。
小知识扩展:
相信大家小时候都应该玩过或者见过一个游戏,那就是一本小书上面画着很多幅画面,这些画面是将一个动作拆分为一个个渐变的画面,然后你可以快速的翻动它,这样就感觉能看到一个动态的画面。这里一幅画面就是一帧。在短时间内(几秒)在翻动多幅画面(几十帧),就会让你看到一个动态的画面。但是如果你翻得比较慢,你也就没有这种感觉了。
- Fps ( Frames Per Second): 帧 / 秒。(这里 Fps 越高越好,还是越低越好~!)
- 帧:一幅画面。
视觉暂留,由于人类眼睛的特殊生理结构,如果所看画面之帧率高于每秒约 10-12 帧的时候,就会认为是连贯的,超过 10-12 帧就感觉不出画面是有间隔的,帧数就是在 1 秒钟时间里传输的图片的量,也可以理解为图形处理器每秒钟能够刷新几次。超过大概 85 赫兹(帧)的图像,像是画面每更新一次只会发光几百分之一秒的阴极射线管及等离子显示屏,此时已经到达大脑处理图像的极限,人眼并无法分辨与更高更新率的差异。
- 12 fps:由于人类眼睛的特殊生理结构,如果所看画面之帧率高于每秒约10-12帧的时候,就会认为是连贯的
- 24 fps:有声电影的拍摄及播放帧率均为每秒24帧,对一般人而言已算可接受
- 30 fps:早期的高动态电子游戏,帧率少于每秒30帧的话就会显得不连贯,这是因为没有动态模糊使流畅度降低
- 60 fps:在实际体验中,60帧相对于30帧有着更好的体验
- 85 fps:一般而言,大脑处理视频的极限
有人说是 24 帧无法识别流畅,其实不是眼睛的问题了,有声电影的拍摄及播放帧率均为每秒 24 帧,对一般人而言已算可接受,但对早期的高动态电子游戏,尤其是射击游戏或竞速游戏来说,帧率少于每秒 30 帧的话,游戏就会显得不连贯,这是因为电脑会准确地显示瞬时的画面(像是一台快门速度无限大的相机),没有动态模糊使流畅度降低。
第一个原因就是由两者图像生成原理不同造成的。
电影虽然只有24FPS,但是每一帧都包含了一段时间的信息,而游戏则只包含那一瞬间的信息。一个电影在一段时间内曝光,画面的每一帧,都包含有一段时间的信息,这段时间的长度由快门时间决定,最长不能超过1/24秒,所以视频中每一帧包含信息量较大。而游戏的第一帧包含第0秒的信息,第二帧包含了第1/24秒的信息,只有这一个瞬间的信息,这中间的信息完全丢失了,所以看起来会卡。
用图来解释一下,比如有一个圆从左上角移动到右下角,第一帧是这样的:
如果是电影,第二帧可能是类似下图这样的(图画得不好但是就是这个意思):
如果是游戏的话,第二帧就应该是这样的图:
看出区别来了吗?这是因为电影和游戏的画面生成方式的本质不同造成的,电影的画面是拍摄的实际场景,在快门时间内胶片/传感器持续曝光,这一段时间里人物场景的变化都会被拍到胶片/传感器上,每隔一段时间换下一张胶片再曝光一段时间。而游戏的画面则是由显卡生成的,显卡通过计算生成一帧画面,生成完毕后再计算下一帧,这样每一帧都是清晰的,不会有模糊,像我上面图中的那个圆,不管他的移动速度是快是慢,显卡只计算两帧画面,中间的移动轨迹一概不会显示,我们看到物体就好像老版西游记里面孙悟空施一个法术“就”的一声飞过去了。
60 Fps 和 16 ms
市面上绝大多数Android设备的屏幕刷新频率是 60 HZ。现在 Android 以 60 Fps 来作为 App 性能的衡量标准,因为这是人眼和大脑之间的协作感知到的流畅画面更新体验,当然,再高点也可以。也就是一秒内要绘制 60 帧,即 1000ms / 60Fps = 16ms 。因此 Android 系统要求每一帧都要在 16 ms 内绘制完成。这意味着每一帧你只有 16 ms(1秒 / 60帧率)的时间来处理所有的任务。
对我们来说任务主要是绘制界面,平滑的完成一帧意味着任何特殊的帧需要执行所有的渲染代码(包括 framework 发送给 GPU 和 CPU 绘制到缓冲区的命令)都要在 16ms 内完成,保持流畅的体验。
如果你的应用没有在 16ms 内完成这一帧的绘制,假设你花了 24ms 来绘制这一帧,那么就会出现掉帧,也就是我们前面所说的卡的现象的情况。
这里掉帧是说这一帧延迟绘制呢,还是说直接不绘制了呢~!
系统准备将新的一帧绘制到屏幕上,但是这一帧并没有准备好,所有就不会有绘制操作,画面也就不会刷新。反馈到用户身上,就是用户盯着同一张图看了 32ms 而不是 16ms ,然后突然看到的是下下张图,也就是说掉帧发生了。
掉帧 drop frames
先来了解一个概念:
Vertical Synchronization :垂直同步。简称为:VSYNC。
又没有竖直刷新呢~!
定义:显卡的输出帧数和屏幕的垂直刷新频率同步。
Android 系统每隔 16ms 就会发出一个 VSYNC 信号,通知 GPU 来渲染界面了。但是这个时候 GPU 上个工作还没有做完,没空理你,好吧,这一帧的画面就没有绘制,丢失了,掉了。下一次这个信号就又要等 16ms 了。在这 16ms 中,你还是只能看到上次渲染的画面。于是你就感觉到卡住了。
引起掉帧的原因非常多,比如:
-
布局嵌套层次太多,花了非常多时间重新绘制界面中的大部分东西,这样非常浪费 GPU 时间。
-
过度绘制严重,在绘制用户看不到的对象上花费了太多的时间。
-
有一大堆动画重复了一遍又一遍,消耗 CUP、GPU 资源。
频繁的触发垃圾回收
虚拟机在执行 GC 垃圾回收操作时,所有线程(包括 UI 线程)都需要暂停,当 GC 垃圾回收完成之后所有线程才能工作。如果大量 GC 操作导致渲染时间操作 16ms , 就会导致丢帧卡顿的问题。
注意:Android4.4 引进了新的 ART 虚拟机来取代 Dalvik 虚拟机。它们的机制大有不同,简单而言:
- Dalvik 虚拟机的 GC 是非常耗资源的,并且在正常的情况下一个硬件性能不错的Android设备也会很容易耗费掉 10 - 20 ms 的时间;
- ART 虚拟机的GC会动态提升垃圾回收的效率,在 ART 中的中断,通常在 2 - 3 ms 间。 比 Dalvik 虚拟机有很大的性能提升;
ART 虚拟机相对于 Dalvik 虚拟机来说的垃圾回收来说有一个很大的性能提升,但 2 - 3 ms 的回收时间对于超过16ms帧率的界限也是足够的。因此,尽管垃圾回收在 Android 5.0 之后不再是耗资源的行为,但也是始终需要尽可能避免的,特别是在执行动画的情况下,可能会导致一些让用户明显感觉的丢帧。
既然知道了造成差体验的原因,那下面就来讲讲有那些解决方法。
To 检测和解决
想要解决这些没有优化的地方,最笨的方法就是去看代码,一行一行找,查看哪里没有写好,逻辑没有优化等等。还可以使用很多工具来帮助我们来查找到应用的一些做的不好的地方。
调试工具
1. OverDraw
- OverDraw: 过渡绘制。
生活一点的说法: 任何事物都有个度。你干好事过渡了也会变成坏事,做坏事也就不用说了。通俗来讲,绘制界面可以类比成一个涂鸦客涂鸦墙壁,涂鸦是一件工作量很大的事情,墙面的每个点在涂鸦过程中可能被涂了各种各样的颜色,但最终呈现的颜色却只可能是 1 种。这意味着我们花大力气涂鸦过程中那些非最终呈现的颜色对路人是不可见的,是一种对时间、精力和资源的浪费,存在很大的改善空间。绘制界面同理,花了太多的时间去绘制那些堆叠在下面的、用户看不到的东西,这样是在浪费CPU周期和渲染时间!
专业解释:某些组件在屏幕上的一个像素点的绘制次数超过一次。
** Android 会在屏幕上显示不同深浅的颜色来表示过度绘制:
没颜色:没有过度绘制,即一个像素点绘制了 1 次,显示应用本来的颜色;
蓝色:1倍过度绘制,即一个像素点绘制了 2 次;
绿色:2倍过度绘制,即一个像素点绘制了 3 次;
浅红色:3倍过度绘制,即一个像素点绘制了 4 次;
深红色:4倍过度绘制及以上,即一个像素点绘制了 5 次及以上;
设备的硬件性能是有限的,当过度绘制导致应用需要消耗更多资源(超过了可用资源)的时候性能就会降低,表现为卡顿、不流畅、ANR 等。为了最大限度地提高应用的性能和体验,就需要尽可能地减少过度绘制,即更多的蓝色色块而不是红色色块。
实际测试,常用以下两点来作为过度绘制的测试指标,将过度绘制控制在一个约定好的合理范围内:
应用所有界面以及分支界面均不存在超过4X过度绘制(深红色区域);
应用所有界面以及分支界面下,3X过度绘制总面积(浅红色区域)不超过屏幕可视区域的1/4;
过渡绘制的根源
过度绘制很大程度上来自于视图相互重叠的问题,其次还有不必要的背景重叠。
优雅源码
有能力且有兴趣看源码的童鞋,过度绘制的源码位置在: /frameworks/base/libs/hwui/OpenGLRenderer.cpp ,有兴趣的可以去研究查看。
if (Properties::debugOverdraw && getTargetFbo() == 0) {
const Rect* clip = &mTilingClip;
mRenderState.scissor().setEnabled(true);
mRenderState.scissor().set(clip->left,
mState.firstSnapshot()->getViewportHeight() - clip->bottom,
clip->right - clip->left,
clip->bottom - clip->top);
// 1x overdraw
mRenderState.stencil().enableDebugTest(2);
drawColor(mCaches.getOverdrawColor(1), SkXfermode::kSrcOver_Mode);
// 2x overdraw
mRenderState.stencil().enableDebugTest(3);
drawColor(mCaches.getOverdrawColor(2), SkXfermode::kSrcOver_Mode);
// 3x overdraw
mRenderState.stencil().enableDebugTest(4);
drawColor(mCaches.getOverdrawColor(3), SkXfermode::kSrcOver_Mode);
// 4x overdraw and higher
mRenderState.stencil().enableDebugTest(4, true);
drawColor(mCaches.getOverdrawColor(4), SkXfermode::kSrcOver_Mode);
mRenderState.stencil().disable();
}
}
- 追踪过渡绘制
通过在 Android 设备的设置 APP 的开发者选项(手机开发者选项怎么打开知道么,在手机设置里,关于手机这一项,进去后点(我的小米)版本号一栏,点击 7 此次。 Android 模拟器点击 Build Number。)里打开 “ 调试 GPU 过度绘制 ” ,来查看应用所有界面及分支界面下的过度绘制情况,方便进行优化。
2. Lint
Android Lint 是一个代码扫描工具,能够帮助识别代码结构存在的问题,主要包括:
- 布局性能(可以解决无用布局,嵌套太多,布局太多)
- 未使用的资源
- 不一致的数组大小
- 国际化问题(硬编码)
- 图标的问题(重复的图标,错误的大小)
- 可用性问题(如不指定的文本字段的输入型)
- manifest 文件的错误
使用步骤:
3. 多使用 include、merge、Stub 等标签
include:用来代码布局复用
使用格式:
<!-- include 复用布局 -->
<include
android:layout_height="wrap_content"
android:layout_width="match_parent"
layout="@layout/layout_title"/>
merge:用来减少布局层级,和 include 搭配使用
<include layout="@layout/layout_merge"/>
<?xml version="1.0" encoding="utf-8"?>
<!-- reduce layout hierarchy -->
<!-- use with include -->
<merge xmlns:android="http://schemas.android.com/apk/res/android">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/app_name"
android:textSize="30sp"
/>
</merge>
ViewStub:按需加载,ViewStub 继承自 View,它非常轻量级且宽/高都是0,因此它本身不参与任何的布局和绘制过程。
在实际开发中,有很多布局文件在正常情况下不会显示,比如网络异常的界面,在网络正常时就没有必要加载。
ViewStub 的示例:
<ViewStub
android:id="@+id/stub_import"
android:inflatedId="@+id/panel_import"
android:layout="@layout/layout_network_error"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
其中 stub_import 是 ViewStub 的 id, panel_import 是 layout_network_error 这个布局的 id。那么它是如何做到按需加载的呢?在需要加载 ViewStub 中的布局时,可以按照如下两种方式进行:
View importPanel = ((ViewStub) findViewById(R.id.stub_import)).inflate();
或者
((ViewStub)findViewById(R.id.stub_import)).setVisibility(View.VISIBLE);
当 ViewStub 通过 setVisibility 或者 inflate 方法加载后,ViewStub 就会被它内部的布局替换掉,这个时候 ViewStub 就不再是整个布局结构中的一部分了。另外,目前 ViewStub 还不支持 <merge> 标签。
4. TraceView
TraceView 是 Android 平台特有的数据采集和分析工具,它可以查看每一个方法运行的时间,常用来定位两类性能问题:
- 方法调用一次需要耗费很长时间导致卡顿
- 方法调用一次不长,但被频繁调用导致累计时长卡顿
使用步骤:
ProfilePanel 各列作用说明:
5. NinePatch
点9,.9,NinePatch:一种特殊格式的图片。
NinePatchDrawable 绘画的是一个可以伸缩的位图图像,Android会自动调整大小来容纳显示的内容。一个例子就是NinePatch为背景,使用标准的Android按钮,按钮必须伸缩来容纳长度变化的字符
NinePatchDrawable是一个标准的PNG图像,它包括额外的1个像素的边界,你必须保存它后缀为.9.png,并且保持到工程的res/drawable目录中。如果你是从APK解压后得到的*.9.png文件,注意它是已将周围的空白像素去掉了的,在使用时必须再加上。
左边跟顶部的线来定义哪些图像的像素允许在伸缩时被复制。
底部与右边的线用来定义一个相对位置内的图像,视图的内容就放入其中。
6. ANR
ANR:Application Not Response(应用无响应)的简称。当 UI 线程在 5 秒之后无法响应用户的触摸操作,就会发生 ANR 现象。ANR 导致应用卡死,必须解决。
ANR 的种类:
- 在 UI 线程中执行了耗时操作,此时用户在该耗时操作尚未执行完毕时,触摸了屏幕,导致 UI 线程在 5 秒内无法响应用户操作。
- 广播接收者在 onReceive() 方法中执行了一段耗时操作,此时用户在该耗时操作尚未完成时,触摸了屏幕,导致 UI 线程 10 秒内无法响应用户操作。
- 在 Service 中 on 打头的方法中执行了耗时操作(因为 Service 也是运行在主线程中的)
捕获处理 ANR:
- 在 LogCat 中可以看到 ANR 的信息
- 在 Android Device Monitor 的 FileExplore 中找到 data/data 文件夹下有一个 traces.txt 文件,该文件中描述了 ANR 的详细信息。
如何避免 ANR:
- 不在 UI 线程中执行耗时操作,如:
- 执行 IO 操作
- 执行数据库读写
- 执行网络操作
- 自定义 View 中的 onDraw() 方法中执行耗时操作
- 其它任何导致 UI 线程阻塞的操作。如长时间的循环、递归等。