APP优化之Android性能优化
1. 什么是卡顿?卡顿的衡量标准
什么是卡顿?
卡顿是人的一种视觉感受,比如我们滑动界面时,如果滑动不流程我们就会有卡顿的感觉,这种感觉我们需要有一个量化指标,在编程时如果开发的程序超过了这个指标我们认为其是卡顿的。
卡顿的衡量标准
FPS(帧率):每秒显示帧数(Frames per Second)。表示图形处理器每秒钟能够更新的次数。高的帧率可以得到更流畅、更逼真的动画。一般来说12fps类似手动快速翻动书籍的帧率,明显可以感知到不够顺滑。30fps是可以接受(游戏不可以接收,要更高),但是无法顺畅表现绚丽的画面内容。60fps可以明显提升交互感和逼真感,一般来说超过75fps就不容易察觉到有明显的流畅度提升了,VR设备需要高于75fps,才可能消除眩晕的感觉。
开发app的性能目标就是保持60fps,这意味着每一帧你只有16ms≈1000/60的时间来处理所有的任务。Android系统每隔16ms发出VSYNC信号,触发对UI进行渲染,如果每次16ms渲染一帧都成功,这样就能够达到流畅的画面所需要的60fps。
如果此时用户在看动画的执行或者滚动屏幕(如RecyclerView),就会感觉到界面不流畅了(卡了一下)。丢帧导致卡顿产生。
流畅的情况下:
出现了丢帧现象(卡顿)
严重丢帧(卡死了)
给我们一种感觉,如果帧率越低,卡顿就越严重,那么是不是就可以使用帧率来衡量卡顿那?
答案:不是,用户看小说,看字看了好几分钟都没有操作更新画面,那难道是卡顿吗!当然不是,所以帧率越低,卡顿就越严重是错误的
如何衡量卡顿
FPS的高低不能准确的反映应用的流程度。如下图所示,只有有更新的时候才刷新界面。
当界面没有变动的时候,手机不需要对界面进行更新,所以此时的FPS会很低,如果1秒钟内都没有变动那么FPS=0。所以我们需要利用其他方式来衡量应用的流程度,比如可以利用丢帧数来衡量。
单位时间内丢帧数可以反映出应用是否流程。不丢帧是终极目标,但每秒丢帧在6-7帧左右可以接受,如果丢10帧以上就需要优化了。
丢帧情况(单位时间内均匀分布) | 卡顿情况 |
---|---|
0 - 10帧 | 流畅 |
10 - 20帧 | 较卡 |
20 - 40帧 | 卡顿 |
40 - 60帧 | 卡死 |
对于我们开发人员来说,会使用一些工具找出卡顿比较集中的地方,找出原因,消除或减弱卡顿。(测试团队会有专门的工具去测试丢帧的情况)
衡量的公式:在规定时间预计显示的图片数量 - 实际效果中的图片数据 = 差值就是你的丢帧数
补充:
app卡顿,实际是失帧,而失帧由两点造成:
- 一是绘制的图形过于复杂(层级嵌套过深等等),过度绘制
- 一是主线程存在耗时的函数,这些函数因为没有到ANR的地步,所有想要查找,就要用特殊的工具TraceView去查看,找到有问题的类,函数,改正,就可以解决这个问题
2. 卡顿的原因及分析工具的使用
核心:分析在16ms中我们的应用做了什么工作,那些工作阻止我们在16ms时更新界面。
通常情况下,在16ms中我们有那些工作需要处理。
以XML布局被绘制出来为例进行说明。
处理过程:
- CPU负责把UI组件计算成多边形和纹理(Profile GPU Rendering:分析颜色为绿色)
- OpenGL负责绘制图像(Profile GPU Rendering:分析颜色为红色)
- GPU栅格化需要显示内容并渲染到屏幕上(Profile GPU Rendering:分析颜色为橙色)
而实际开发中我们还加入交互、业务处理等工作,这些工作都需要在16ms中处理完成。此时我们就需要有一个工具,直观的帮助我们找出哪些工作占用了多少时间。
Profile GPU Rendering
通过手机开发者选项中提供的Profile GPU Rendering(GPU呈现模式分析)功能,我们可以清楚的看到处理流程中各部分的耗时。手机端工具(开发助手GPU渲染图)。建议大家在Android6.0及以上手机测试,在GPU计算过程所对应呈现的颜色更多,有利于我们对卡顿更详细的分析
打开Profile GPU Rendering操作截图如下:
大家可以拿着真机配置一下。看看有什么变化。
条形图说明
-
水平方向的一根绿线代表16ms,条状在绿线下面,代表符合60fps的标准.
-
每条都代表一帧画面所有工作内容
-
每条中不同的颜色代表不同的工作内容
Android6.0及以上的手机颜色对应关系如下:
原因分析谷歌官网:https://developer.android.google.cn/studio/profile/inspect-gpu-rendering.html#profile_rendering
3. 通用UI优化流程
第一步:UI层优化
-
UI问题比较容易查找
-
一旦出现问题影响范围广(xml、mesure、layout、draw、Display List、栅格化……)
工具:设备过渡绘制查看功能、Hierarchy Viewer等
常见问题:过渡绘制(难在自定义控件)、布局复杂、层级过深……
过渡绘制
在屏幕一个像素上绘制多次(超过1次)。
如:文本框,如果设置了背景颜色(黑色),那么显示的文字(红色)就需要在背景之上再次绘制。
打开手机开发者中的过渡绘制区域即可查看。蓝色标识这个区域绘制了两次。(手机上查看过度绘制)
说明:
-
如果大面积都是蓝色,属于正常情况。
-
重点关注大面积绿色及以后的,表示看能否优化。
设备中的该选项只能直观的让我们感受到应用的界面是否存在过渡绘制,如果存在,我们需要利用Hierarchy Viewer查找布局中不合理的地方。
第二步:自定义控件绘制优化
原理:两个控件重叠时,一个控件显示,一个控件被挡住,那么只绘制显示出来的部分,被挡住就不用再进行绘制
Clip Rect 与 Quick Reject
Clip Rect:识别可见区域
Quick Reject:控件所在的矩形区域是否有交集
在Canvas中有上述两个方法,帮助我们进行判断,避免出现过渡绘制。
我们可以通过canvas.clipRect()
来帮助系统识别那些可见的区域,在这个区域之外的我们不在进行绘制。如侧拉菜单,当菜单显示的时候被菜单遮挡的部分是不用进行绘制的,一旦绘制就会出现过渡绘制现象。系统的控件会控制过渡绘制,但我们自己的控件就需要自行管理了。所以在使用侧拉菜单时就需要优先考虑系统提供的了。(如果系统没有提供的,我们自己编写时也需要注意,避免出现过渡绘制。)
自定义控件过渡绘制案例
案例效果:
- 在MainActivity中使用自定义控件
<com.sn.overdraw.MyView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/transparent"/>
- 图片资源数组
//图片资源
private final int[] ids = new int[]{R.drawable.img1, R.drawable.img2, R.drawable.img3, R.drawable.img4,R.drawable.img5, R.drawable.img6};
private final Bitmap[] imgs = new Bitmap[6];
- 初始化时加载图片资源,同时对一会需要使用到的画笔做初始化
private Paint paint;
//初始化,在构造方法里调用
private void init() {
//初始化数据,把图片数据通过BitmapFactory,转换为BitmapFactory,装入Bitmap数组中
for (int i = 0; i < 6; i++) {
imgs[i] = BitmapFactory.decodeResource(getResources(), ids[i]);
}
//创建画笔对象
paint = new Paint();
//打开抗锯齿
paint.setAntiAlias(true);
}
- 先将图片摆放好,绘制出现过度绘制的问题
//A.相邻两张牌错开20像素,这种写法没有优化,过度绘制很严重,可以打开工具看一下
for (int i = 0; i < 6; i++) {
//参数 1.图片bitmap 2.每次绘制比上一次水平间隔的距离,int型 3.固定0,每次绘制比上一次垂直间隔的距离,int型 4.画笔对象
canvas.drawBitmap(imgs[i],i*100,0,paint);
}
原因比较简单,对于“大王”这张牌来说,我们不需要绘制完整的图片,如果都绘制了就会出现上面的情况。
处理思路:
找出牌需要绘制的区域,让canvas在绘制这张牌时仅仅按区域绘制一部分即可。对于“大王”这张牌来说我们仅仅绘制如下内容。
重点来了,我们该如何划定这个区域?
在Canvas中clipRect方法可以帮助我们划定一个区域,进行绘制。
方法参数说明:
clipRect(int left, int top, int right, int bottom)
canvas.clipRect(0, 0, 20, imgs[i].getHeight());
设置完成后,我们来绘制大王这张牌。
canvas.drawBitmap(imgs[0],0,0,paint);
再增加循环,快速绘制所有的牌。
for (int i = 0; i <imgs.length; i++) {
canvas.clipRect(i * 20, 0, (i + 1) * 20, imgs[i].getHeight());
canvas.drawBitmap(imgs[i],i*20,0,paint);
}
大家会发现绘制完成的结果不是我们想要的。
我们需要借助save和restore来完成裁剪的操作。
save:用来保存Canvas的状态。save之后,可以调用Canvas的平移、放缩、旋转、错切、裁剪等操作。
restore:用来恢复Canvas之前保存的状态。防止save后对Canvas执行的操作对后续的绘制有影响。
save和restore要配对使用(restore可以比save少,但不能多),如果restore调用次数比save多,会引发Error。save和restore之间,往往夹杂的是对Canvas的特殊操作。
代码修改如下:
for (int i = 0; i <imgs.length; i++) {
canvas.save();
canvas.clipRect(i * 20, 0, (i + 1) * 20, imgs[i].getHeight());
canvas.drawBitmap(imgs[i],i*20,0,paint);
canvas.restore();
}
剩下最后一个工作,把最上面的牌绘制完整。
//C.优化了过度绘制,只绘制显示的部分,不显示被遮挡的部分不看
//提示:要让裁剪循环有效,必不可少的两个方法canvas.save();canvas.restore();两个方法配对出现
for (int i = 0; i < imgs.length; i++) {
//裁剪前调用此方法
canvas.save();
//前5张牌都是被遮挡住的
if(i<imgs.length-1) {
canvas.clipRect(i * 20, 0, (i + 1) * 20, imgs[i].getHeight());
}
//最后一张牌是完整画下来的
else if(i==5){
canvas.clipRect(i * 20, 0, i * 20+imgs[i].getWidth(),imgs[i].getHeight());
}
canvas.drawBitmap(imgs[i],i*20,0,paint);
//裁剪后调用此方法
canvas.restore();
}
4. Hierarchy Viewer 的使用
Hierarchy Viewer可以很直接的呈现布局的层次关系,视图组件的各种属性。 我们可以通过红,黄,绿三种不同的颜色来区分布局的Measure,Layout,Executive的相对性能表现如何。
打开工具
选择需要查看的内容:查看各个节点Measure,Layout,Executive
三个小圆点, 依次表示Measure, Layout, Draw, 可以理解为对应View的onMeasure, onLayout, onDraw三个方法.
绿色, 表示该View的此项性能比该View Tree中超过50%的View都要快.
黄色, 表示该View的此项性能比该View Tree中超过50%的View都要慢.
红色, 表示该View的此项性能是View Tree中最慢的.
一般来说:
-
Measure红点, 可能是布局中嵌套RelativeLayout, 或是嵌套LinearLayout都使用了weight属性.
-
Layout红点, 可能是布局层级太深.
-
Draw红点, 可能是自定义View的绘制有问题, 复杂计算等.
5. UI的通用优化
常规做法
-
没有用的父布局——没有背景绘制或没有大小限制的父布局,不会对界面效果产生任何影响。特别是
<include/>
进来的布局,很容易产生没有用的父布局问题。可以通过<merge/>
标签替代<include/>
。 -
在布局层次一样的情况下,建议使用LinearLayout代替RelativeLayout。
-
使用LinearLayout导致的层次变深,可以使用RelativeLayout进行替换。同样的界面我们可以使用不同的方式去实现,选择一个层级最少的方案。
-
不常用的UI被设置成了GONE,尝试使用
<ViewStub/>
代替。 -
去掉多余的背景颜色,减少过渡绘制,对于有多层背景色的布局来说,留最上面的一层即可。谨慎使用alpha透明度的设置,如果后渲染的元素有设置alpha值,那么这个元素就会和屏幕上已经渲染好的元素做blend处理,这样会导致不少性能问题,特别是出现在列表的Item中。
-
对于使用Selector当背景的布局,可以将normal状态的color设置为透明。
-
我们不能因为提高性能而忽略了界面需要达到的效果(平衡Design设计与Performance性能)。
-
onDraw绘制方法,不要再此方法里创建对象,消耗内存,因为这个方法会被调用多次,导致对象多次创建
扩展:不要在频繁调用的方法去创建对象
第二步:代码问题查找
工具:Lint
-
常见问题:我们重点关注Performance和Xml中的一些建议
-
在绘制时实例化对象(onDraw)
-
手机不能进入休眠状态(Wake lock,比如我的VR在onpouse中,没有暂停)
-
资源忘记回收(比如LBS云检索里面,没有对定位,搜索等进行停止销毁操作)
-
Handler使用不当导致内存泄漏
-
没有使用SparseArray代替HashMap
-
未被使用的资源(要apk瘦身)
-
布局中无用的参数(常出现在我们在LinearLayout与RelativeLayout进行替换,有些子控件用到了这些根布局特有的属下,替换后这些属性没有用了,又没有删除,也会消耗CPU的资源)
-
可优化布局(如:ImageView与TextView的组合是否可以使用TextView独立完成)
-
效率低下的weight
-
无用的命名空间等
6. Lint工具
Android lint:是一个代码扫描工具,能够帮助我们识别代码结构存在的问题,主要包括
-
布局性能(以前是 layoutopt工具,可以解决无用布局、嵌套太多、布局太多)
-
未使用到资源
-
不一致的数组大小
-
国际化问题(硬编码)
-
图标的问题(重复的图标,错误的大小)
-
可用性问题(如不指定的文本字段的输入型)
-
manifest文件的错误
总结:Lint工具用来查找代码的问题(注意:工具不是万能的,还是有些代码问题是他不能查出来)
提示:Lint工具提供了解决问题的方法,点击让他自动解决就可以
Android Studio中开启Lint工具,选中需要分析的Module,点击工具栏中Analyze中的Inspect Code选项。
重点关注Performance(性能)和XML(布局的问题)
问题处理
1. 案例中性能问题处理
其他的一些性能问题
建议使用concate方法进行连接字符串,会比append的方式性能好。
2. 案例中xml提到的内容如下
无效的命名空间
无效的布局参数:比如在线性布局中的控件使用到了相对布局中的属性,运行时需要处理,影响代码的执行效率
3. 案例中关于定义声明变量的警告
分析包括中会包括错误和警告,会给出具体的描述、类别、位置。上图是一个错误的描述,下图为上面的I18N国际化选项点击打开给出的没有进行国际化的字符串的位置。
7. Traceview解决程序耗时问题
Traceview作用:主线程耗时大的函数、滑动过程中的CPU工作问题,工具可以提供每个函数的耗时和调用次数,我们重点关注两种类型的函数
-
主线程里占用CUP时间很长的函数,特别关注IO读取操作(文件IO、网络IO、数据库操作等),一句话,主线程不能做耗时操作
-
主线程调用次数多的函数,虽然每一次占用时长不大,但是累计起来就是耗时操作了
使用Traceview找出卡住主线程的地方
通过Android Studio打开里面的Android Device Monitor,切换到DDMS窗口,点击左边栏上面想要跟踪的进程,再点击上面的Start Method Profiling的按钮,如下图所示:
启动跟踪之后,再操控app,做一些你想要跟踪的事件,例如滑动RecyclerView(ListView),点击某些视图进入另外一个页面等等。操作完之后,回到Android Device Monitor,再次点击相同的按钮停止跟踪。此时工具会为刚才的操作生成TraceView的详细视图。
可以看到哪个方法的耗时占用比例
重点关注 Incl Cpu Time、 Call+RecurCalls/Total、 Real Time/Call
通过降序排序,可以分别找到这两列中数值比较大的内容。
提示:开发中消息小心递归方法
指标说明:
Incl(Inclusive) Cpu Time
方法本身和其调用的所有子方法占用CPU时间.
Excl(Exclusive) Cpu Time
方法本身占用CPU时间.
Incl Real Time
方法(包含子方法)开始到结束用时.
Excl Real Time
方法本身开始到结束用时.
Call + Recursion Calls/Total
方法被调用次数 + 方法被递归调用次数.
Cpu Time/Call
方法调用一次占用CPU时间. 方法实际执行时间(不包括io等待时间)
Real Time/Call
方法调用一次实际执行时间. 方法开始结束时间差(包括等待时间)
小案例:我们可以在RecycleView中的ViewHolder或listView中的getView方法中做点手脚,比如睡几毫秒(8ms),通过监控滚动,我们是否可以定位到问题代码。
Traceview通过代码进行查找:
解决问题:应用启动非常快,我们想手动的点击Traceview工具监控程序,是来不及的,所以使用代码的形式,来完成
- 在onCreate或Application中在要监测的代码头和尾,打上trace,就是加上下面的两行代码
提示:手动操作监控代码,在这两行代码内的代码,运行会被监控(监控结构会生成文件存储SDCard上)
//A.如果没有办法手动操作监控,可以使用代码监控重点代码
//A.监控的结果会生成文件存储SDCard上,要到处,用工具进行查看
Debug.startMethodTracing("ycfDemo");// 文件的名称
....................
//A.如果没有办法手动操作监控,可以使用代码监控重点代码
Debug.stopMethodTracing();
2.运行程序, 会在sdcard上生成一个"ycfDemo.trace"的文件.
注意:需要给程序加上写存储的权限
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
注意:Android6.0以后的模拟器需要为应用打开读写权限
3.把生成文件导出,然后在用工具打开即可
好的做法
-
不要阻塞UI线程,占用CUP较多的工作尽可能放在子线程中执行。
-
需要结合使用场景选择不同的线程处理方案
AsyncTask: 为UI线程与工作线程之间进行快速的切换提供一种简单便捷的机制。适用于当下立即需要启动,但是异步执行的生命周期短暂的使用场景。
HandlerThread: 为某些回调方法或者等待某些任务的执行设置一个专属的线程,并提供线程任务的调度机制。比较大了,自己维护一整套消息分发处理
ThreadPool: 把任务分解成不同的单元,分发到各个不同的线程上,进行同时并发处理。
IntentService: 适合于执行由UI触发的后台Service任务,并可以把后台任务执行的情况通过一定的机制反馈给UI。
-
如果大量操作数据库数据时建议使用批处理操作。如:批量操作数据
8. 性能优化综合案例优化
应用启动(AppStartup)性能优化——使用NoHttp获取应用列表
这里之所以用NOhttp,是以他为典型,因为很多第三方框架,都要在Application中进行初始化,这个初始化的过程,会对我们程序带来多大的影响.
一点带面.
案例编写:
1.使用的资源:图片,布局,Libs,代码
2、需要关联的库
compile 'com.yolanda.nohttp:nohttp:1.1.0'
compile 'com.orhanobut:logger:1.15'
compile 'com.android.support:design:24.2.1'
3、初始化NoHttp
//初始化时耗时操作
Logger.setTag("NoHttp");
Logger.setDebug(true);
NoHttp.initialize(this, new NoHttp.Config()
.setConnectTimeout(30 * 1000)
.setReadTimeout(30 * 1000)
);
问题表现:通常从用户点击到应用完全展示完首页,需要用户等待一段时间。我们如何缩短时间并提高用户体验。
一种解决思路:在这个地方加载一个广告,一方面有收入,一方面这里确实需要等待
优化思路:
-
布局优化
-
代码优化
-
业务逻辑优化
分析:应用在启动的过程中我们的代码能够影响启动速度的地方如下
- Application的onCreate
- 首屏Activity的渲染
步骤:
-
利用Traceview工具观察启动过程方法耗时情况,重点关注onCreate方法(自定义Application和首页Activity)。问题:Traceview工具如何在应用启动时监控数据?
-
分析自定义Application耗时操作,判断onCreate方法中的内容(如:第三方的工具是否可以不占用主线程进行初始化)。
-
查看界面是否存在过渡绘制。
-
利用Hierarchy Viewer工具查看界面需要优化的点。
-
启动过程中的白屏优化。
第一步:观察耗时情况
解决问题:应用启动非常快,我们想手动的点击Traceview工具监控程序,是来不及的,所以使用代码的形式,来完成,具体参照"Traceview解决程序耗时问题"笔记
第二步:把耗时操作放到IntentService中
/**
* A.将MyApplication中onCreate方法内容耗时的初始化工作移动到该类中
* 此类的父类就是Service,Service是运行在主线程的后台进程
* 提示:注意这个类要在清单文件下进行注册,步骤和Service注册一样
*/
public class InitService extends IntentService {
// 问题:由于将NoHttp的初始化工作移动到了子线程,当主线程使用NoHttp发现没有初始化完成,报异常了。
// 提示:实际这和同步异步问题原理一样,东西还没处理完,就拿着东西去用,肯定会出问题
// 方案一:使用boolean值进行初始化工作的标记,如果完成boolean为true,可以在使用该工具的地方每隔一个时间段判断一下。
// 方案二:当初始化工作完成后,发出一个通知,如果有观察者,则进行后续工作的处理(开发中常用这个模式,可以用RxAndroid完成)
//A.构造方法,实际就是给开启的子线程起一个名称
public InitService() {
super("init");
}
//B.标记是否初始化完成,false没有完成
public static boolean isInit=false;
//A.此方法的代码运行在子线程中,可以做耗时的操作
@Override
protected void onHandleIntent(@Nullable Intent intent) {
//初始化时耗时操作
Logger.setTag("NoHttp");
Logger.setDebug(true);
NoHttp.initialize(this, new NoHttp.Config()
.setConnectTimeout(30 * 1000)
.setReadTimeout(30 * 1000)
);
//B.初始完成,把标记改为true
isInit=true;
}
/**
* A.启动service,在MyApplication调用了此方法
* @param myApplication
*/
public static void start(MyApplication myApplication) {
Intent intent = new Intent(myApplication, InitService.class);
myApplication.startService(intent);
}
}
修改完成后,会引发一个问题,及在首页访问网络时,由于NoHttp的初始化还没有完成会报出如下异常:
如果我们在首页就需要立即访问网络,就需要对初始化进行监控,可以简单的使用一个boolean值,进行判断,当初始化完成后boolean值修改为true。我们在MainActivity中可以使用Handler间隔一段时间就检查一下boolean即可。
NOhttp初始化放到IntentSerice里面是提高性能的一种方式
第三步:过渡绘制
进入首页后,应用的启动速度限制就集中在首页的界面渲染上了。因此我们开始对界面进行优化处理。实际这里是层级嵌套过深导致的,我们把LinerLayout改为RelativeLayout即可.
提示:借用Hierarchy Viewer(层级查看器)来看层级嵌套过深的问题
9. 消除手机启动的白屏现象
两种处理方案:
方案一:设置成透明的界面,制造延时启动效果
<item name="android:windowIsTranslucent">true</item>
<item name="android:windowNoTitle">true</item>
方案二:设置一个背景图
<!--第二种方式:设置一个背景图,有问题背景图一直存在-->
<item name="android:windowBackground">@drawable/splash</item>
<item name="android:windowNoTitle">true</item>
<!--注意:当界面加载完成后需要将背景改成白色。-->
问题的解决:把下面的代码放在应用入口的Activity中
//C.Activity生命周期中,界面加载成功后的回调
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
//C.修改windows背景,把使用第二种方式所带来的问题,解决
getWindow().setBackgroundDrawable(new ColorDrawable(Color.WHITE))
}