写在前面的话
一般情况下,只有当我们发现“严重”的性能问题时,我们才会开始着手进行性能优化。此时,虽然可以针对性的解决严重的性能问题,但在继续优化过程中,面对无数细小的“不良”代码,却又力不从心。所以,为了得到的微小的性能改善,庞大的工作量和复杂的历史逻辑却让人望而却步。不得不承认,无数细小的不良代码所累加的性能问题是不可忽视的。面对这样的问题,最佳的解决办法就是总结优化过程中发现的不良代码,从编码之初就保留对性能的“敬畏心”和sense,才能根本构建良好的性能优化良性循环。
传统的编码规范仅只是增强代码的可读性和可维护性,本文的目的是整理提供常见的引起性能问题的代码习惯及对应规范,从编码源头硬性的对将会影响程序性能的操作进行规范,杜绝使用一定会引起性能问题的代码,以及给出更优的建议代码。总之,每个开发者都应该在开发过程中保持性能sense,尽可能高性能的完成产品功能需求。
1. 布局优化1.1 布局层次扁平化
- 控制布局层次,减少Activity上的布局层次,activty层的布局每多一层,页面上每个view的渲染都深一层,影响非常大!所以,请先检查所在页面的activity的页面层次是否做法“减无可减”。
- 对于Adapter控件,如RecyclerView,item的布局层数也要严格控制,一般情况不要超过2层,card3.0由于使用了粒度更细的复用view,为了确保通用性,一布局层次比card2.0平均多了1层,card3.0的渲染性能较2.0略差。
- 合理使用、,保持布局扁平化,简单化,按需加载。
1.2 保证布局效率
- 合理选择控件容器,如果可以达到相同的布局效果,尽量使用LinearLayout & FrameLayout 代替RelativeLayout。
-
避免overDraw,将Acitivity 中的Window 的背景图设置为空,android的默认背景不为空,将Activity的背景放到Activity的Theme中设置,同时避免fragment和activity背景重复设置。
设置Activity背景
getWindow().setBackgroundDrawable(
null
);
//Theme设置属性
src_image
- 去掉不必要的view背景,如:card3.0针对白色背景的block,父层级row直接使用了透明背景。
- 避免"overDesign",有道是“由俭入奢易,由奢入俭难",简单易用才是本质,过度设计可能反而会给用户带来不好的体验。 举个例子,card3.0有“对普通文本支持左右image,即包装到一个LinearLayout”的设计,该设计会让block的文本区域布局层次多一层,为了10%的需求,牺牲90%的block同样性能折损。所以,针对这个情况,我们针对常用block舍弃了该设计,优化后,页面上下滑动更为流畅。
-
标签,高效占位符,按需加载布局。ViewStub是一个轻量级的View,占用资源非常小的控件。我们可以为ViewStub指定一个布局,在Inflate布局的时候,ViewStub只会被初始化,然后当ViewStub被设置为可见的时候,或是调用了ViewStub.inflate()的时候,ViewStub所向的布局就会被Inflate和实例化,然后ViewStub的布局属性都会传给它所指向的布局。
ViewStub
加载布局时,可以使用下面其中一种方法:
((ViewStub) findViewById(R.id.test_stub)).setVisibility(View.VISIBLE);
View importPanel = ((ViewStub) findViewById(R.id.test_stub)).inflate();
- 显而易见,使用代码实现布局代替xml文件,可以在一定程度上减少inflate的时间,缩短在一个doFrame周期页面渲染耗时,这也是我们在优化过程中的一个重要手段!
1.3 自定义控件原则
- 避免在自定义布局的onDraw里创建对象;
- 避免在复杂的布局上使用动画,如:底tab的动效影响底tab帧耗时10%左右!
首先,如果你还在使用ListView,替换RecyclerView可能是更好的选择。比如,之前的版本基线由于短视频播放器上的部分实现问题,短视频所在页面使用ListView,切换RecyclerView之后,页面渲染性能大幅度提高。
2.1 共享缓存池RecycledViewPool
RecyclerView view1 =
new
RecyclerView(context);
LinearLayoutManager layout =
new
LinearLayoutManager(context);
layout.setRecycleChildrenOnDetach(
true
);
view1.setLayoutManager(layout);
RecycledViewPool pool = view1.getRecycledViewPool();
//...
RecyclerView view2 =
new
RecyclerView(context);
//... (set layout manager)
view2.setRecycledViewPool(pool);
//...
RecyclerView view3 =
new
RecyclerView(context);
//...(set layout manager)
view3.setRecycledViewPool(pool);
合理使用recyclerView页面采用共享缓存池RecycledViewPool,对象池可以节省创建viewHolder的开销,更能避免GC,使用中要注意一下几点:
- RecycledViewPool是依据Item的viewType来索引viewHolder的,所以你必须确保共享的RecyclerView的Adapter是同一个,或viewType是不会冲突的;
-
RecycledViewPool可以自主控制需要缓存的ViewHolder数量,并不是缓存越多越好,如下设置:
共享缓存池
mPool.setMaxRecycledViews(itemViewType, number);
- 在合适的时机,RecycledViewPool会自我清除掉所持有的ViewHolder对象引用,不用担心池子会“侧漏”。
2.2 科学合理的notify
合理采用局部刷新,替代notifyDataSetChanged,当只需要针对某个位置刷新数据时,可以选用一下方法进行局部刷新
-
RecyclerView局部刷新
//改变列表某个位置的item view
notifyItemChanged(
int
position)
//插入列表某个位置
notifyItemInserted(
int
position)
//删除列表某个位置
notifyItemRemoved(
int
position)
notifyItemMoved(
int
fromPosition,
int
toPosition)
notifyItemRangeChanged(
int
positionStart,
int
itemCount)
notifyItemRangeInserted(
int
positionStart,
int
itemCount)
notifyItemRangeRemoved(
int
positionStart,
int
itemCount)
2.3 优化加载图片时机
图片加载对于RecyclerView渲染是尤为关键的一步,对列表滑动的流畅性有很大的影响。所以,在列表滑动过程中暂停对图片的加载,当滑动停止时恢复加载,该优化可以大幅度提高用户的流畅体验值。需要注意的是,这样也会导致用户对于滑动过程中图片未正常加载产生疑惑,需要评估对用户体验的影响,也可专门针对低端机做此优化
-
滑动暂停加载图片
mRecyclerView.addOnScrollListener(
new
RecyclerView.OnScrollListener() {
@Override
public
void
onScrollStateChanged(RecyclerView recyclerView,
int
newState) {
if
(newState == RecyclerView.SCROLL_STATE_IDLE) {
ImageLoader.setPauseWork(
false
);
//恢复加载
}
else
{
ImageLoader.setPauseWork(
true
);
//暂停加载
}
}
});
2.4 其他tips
-
设置所有的 Item 的高度固定大小,这样可以减少item测量次数,尤其是对于 GridLayoutManager。
mRecyclerView.setHasFixedSize(
true
);
- 在onCreateViewHolder 和 onBindViewHolder 对时间敏感的方法中,尽量避免繁琐的操作和循环创建对象。
3.1 什么是UI线程
Android应用的UI线程的概念及其重要性是每个Android开发者都应该理解的。当一个应用启动,系统会为应用创建一个名为“main”的主线程。这个主线程(也就是UI主线程)主要负责向UI组件分发事件(包括绘制事件),它也是你的应用和Android的UI组件交互的线程,因此它非常重要。例如,如果你点击了屏幕上的一个按钮,UI线程会把点击事件交给view处理,view接到事件后会设置它的pressed状态,然后向事件队列中发送一个invalidate请求,UI线程会依次读取请求队列并且通知view去重绘。
当App在UI线程做一些比较繁重的操作的时候(比如网络请求、数据库操作等相关操作),就会阻塞UI线程,导致事件停止分发。对于用户来说,应用看起来就像卡住了,更坏的情况是,如果UI线程阻塞时间过长,用户就会看到应用无响应提示(ANR)。
所以,我们应该遵循两条基本原则 :
1. 不要阻塞UI线程;
2. 不要再UI线程外操作UI组件。
3.2 常见的耗时操作
- 数据库操作,禁止在UI线程操作数据库!
- 网络请求
- 下载操作
- 解析json数据,最好不要再UI线程进行解析json和转json的操作,这一点在优化过程中曾多次出现!
- 复杂的计算逻辑,如高斯模糊算法等
-
IO操作,如读取文化、写文件等操作
-
当页面是ViewPager结构时,通常在进入页面时会默认预加载左右各一屏的页面,导致UI线程工作异常堵塞,在实际的业务场景下,取消不必要的预加载非常重要,我们可以根据需要衡量预加载左右tab的必要性。 如:爱奇艺APP的vip tab又内外两层viewPager组成,启动APP后点击Vip tab在最好的情况下需要预加载并渲染4个页面(最差为6个页面,其中还包括部分RN页面,RN页面的初始化也较为耗时)的数据,给用户的感受就是点击后非常卡,我们做的优化就是:取消不必要的预加载,vip的渲染耗时提升为原来的35%左右!
-
在APP的启动阶段,常常要做大量的初始化操作,但是我们需要遵循的原则就是:
1、可以放到子线程执行的任务,不放到主线程。
2、按需加载,科学评估预加载、初始化等操作的必要。
3、分阶段解读任务,避免UI线程的工作挤在一个特定的时间窗口。
在实际的优化工作中,由于在初始化阶段延时执行耗时任务导致启动后页面加载受影响的case也并不少见,而这些case基本都可以通过合适的异步操作或者延时操作来解决。
5. 小心细小但频繁的工作频繁但细小的工作是在优化过程中最容易忽略的部分,但是恰恰是最容易优化的部分,细小的工作优化“一小步”,你会发现页面整体的流畅度会前进“一大步”!下面就介绍一些我们对爱奇艺APP优化过的一些点:
- “我的”页面pingback的字段拼接在主线程,看起来每次拼接好像并不是很耗时,但是页面进来会发送一批展示pingback,总体上的影响还是很大的
- 优化BarConfig工具类、ScreenTool工具类等,避免重复获取各种属性
- 在没有线程安全的要求时,字符串拼接用StringBuilder代替StringBuffer和String(Card页面url拼接方式优化,效率提高约40%)
- 对于频繁进行的SP存储,尽量不选择立即存储到文件(首页消息card存储优化)
- 谨防内存泄露
- 避免加载过大图片,压缩或者使用对象池后再使用,用Webp代替传统png图片
-
避免在循环(for、while、getView方法、onDraw)里创建对象
- 避免使用大量使用反射(EventBus优化,减少不必要的注册,杜绝无意义的反射)
页面性能优化是一项需要长期关注且重要的工作,页面的流畅与否直接影响到用户对我们产品的评价和褒贬。所以,只有我们开发者每个人都保持着对性能问题的高度警觉性,以良好的习惯和高效的实现面对每一行代码,我们的产品一定会越来越好。最后,感谢基线APP的各位同学在我们性能优化工作过程中的配合和协助!