喜马拉雅车载版-项目优化点小结
目前的现状
app在低版本上面卡顿比较明显,掉帧严重,布局嵌套严重
酷我的app业务层开始是第4层,平均ui显示在第7-11层,我们的app从第8层开始展现业务,平均层级在14-18层
酷我的车机执行滑动时绘制时间平均为500ms,我们的app绘制时间平均为800多ms(仅从主页来讲)
因此将部分在长安车机上面验证的优化点列下来,之后组内性能优化的时候可以有个参照,另外也附注了部分源码级别的解释,大家可以看看优化的原理,如果有不同的也可以讨论讨论。此文档长期维护,优化点会逐渐增加。
优化指标主要这几个方面:cpu占用率、内存占用、帧率、电量、流量、过度绘制、包大小、启动时间、crash率
整理的优化点
recyclerview
recyclerview的优化主要在数据预加载、复用性提升、降低绘制次数上面做文章,其中复用性的提升亦会带来绘制次数的降低
数据预加载
getExtraLayoutSpace
1 | LinearLayoutManager linearLayoutManager = new LinearLayoutManager(getContext(), LinearLayoutManager.HORIZONTAL, false){ |
初始化recyclerview的layoutmanager的时候,通过复写getextralayoutspace,可以通过增加绘制时间,来提前加载不可见区域的视图
其原理主要是在layoutmanager.onLayoutChildren->fill()中执行的下面这段
1 | while((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) { |
其影响到了remainingSpace的值,复写会提升remainingspace的大小,带来的效果就是会在加载完毕限制的viewholder之后在加载一段空间,起到了预加载的地步,但是由于其实际上会造成加载时间增加的问题,因此建议用在例如推荐卡片页,电台自选页面,分类自选页面这种没有分页的页面,写的数值根据各自的情况来定,数值的单位是像素。
复用性提升
setItemViewCacheSize
1 | setItemViewCacheSize() |
使用setItemViewCacheSize,可以在更新viewholder的时候指定复用的viewholder数量
1 | void updateViewCacheSize() { |
其原理如上,我们指定的viewholder数量会影响到将cachedViews的最大数量,而cachedView就是我们刚才看到的视图,如果viewholder一旦被从cachedView移除,进入recyclerpool之中的话,其页面的内容就会被清空,滑回去在希望显示的话,就会出现重新bind,重新绘制,影响效率。
setItemPrefetchEnabled(true)
上面updateViewCacheSize中还有一个mPrefetchMaxCountObserved,是recyclerview升级到25.1之后才有的特性
其原理数据预取
总结就是通过将主线程对viewholder的create和bind的操作提前到render画布绘制的过程中进行,以降低主线程卡顿掉帧的时间。这也也算是复用性提升的一点。
默认是开启的
setSupportsChangeAnimations(false)
1 | ((SimpleItemAnimator) rv.getItemAnimator()).setSupportsChangeAnimations(false); |
其生效代码为recyclerview.scrapview
1 | void scrapView(View view) { |
其作用总结为:当更新一个itemview的时候,其更新的步骤最终会走到scrapView这个方法中,这个方法决定是否废弃一个view,如果将其默认的itemanimator关闭的话,则不会废弃这个viewholder,而会继续使用这个viewholder,将其添加到attachscrap中进行数据更新的操作,反之,若没有关闭itemanimator的话,则会创建一个新的viewholder,而将老的viewholder塞入废弃表中。
复用性究极提升,复写RecyclerView.onViewRecycled(holder)
通过复写recyclerview.onviewrecycled的方法,可以取到状态变更时新建或者废弃的viewholder,通过我们自己兴建一个共享holder池的方法,可以全局管理我们项目中viewholder的数量。但是这个设计的操作过于复杂,改动的代码过于庞大,大家可以仅做了解。
降低绘制
填充数据时使用notifyItemXXX而非notifyDataSetChanged
众所周知,notifyItemXXX是部分刷新,而notifyDataSetChanged则是全部刷新,recyclerview中虽然对全部刷新中在cachedView这张list中的view做了复用处理,但是在部分情况下仍然会出现许多cachedview中的viewholder进入到了Scrap View、recyclerpool、甚至removedview这三张表中,导致刷新的时候走重新bind和重绘的操作。比较明显的是会带来一次闪烁。
因此项目中在刷新部分视图时,多使用notifyItemXXX方法,而非notifydatasetchanged。
setHasFixedSize(true)
在item的布局不会动态变化高度和宽度的时候,添加这个方法,可以降低requestlayout的次数
其作用的代码为recyclerview.onmeasure,recyclerview.triggerupdateprocessor
1 | protected void onMeasure(int widthSpec, int heightSpec) { |
其作用点在于将测量过程交与layoutmanager进行,不同的layoutmanager对于测量有自己的优化手段,另外就是在调用notifyItemXXX的时候,不在直接对整个recyclerview进行重绘请求,而是直接执行操作的runnable,调用consumePendingUpdateOperations,最终仅对变动的子item进行重绘,降低了很多重绘的成本。
这个方法发现了一个问题,就是如果仅仅只执行notifyItemXXX的时候,会造成不刷新。主要原因是新增加的数据改变了rv的宽高,导致的问题。因此需要使用这个的时候,在更改数据的时候,需要调用notifydataSetChanged()
针对4.2车机的优化
4.2的车机,目前接触的是长安车机,其有一个原生的bug,在recyclerview绘制过程中会重复调用resolveRtlPropertiesIfNeeded()最终在canResolveTextDirection()中浪费大量的时间,导致应用十分卡顿但却不会anr,针对这个问题处理的方式是在viewholder中对每一个view执行setLayoutDirection
模版代码如下:
1 | if ( Android.os.Build.VERSION.SDK_INT == Build.VERSION_CODES.JELLY_BEAN_MR1 ){ |
布局优化
使用constraintlayout
约束布局可以将复杂的布局打散,以达到减少到仅一层布局的地步,使用约束布局仅仅需要固定一个坐标系的原点view,即可生成一套完整的坐标系。
了解一下以下的知识点即可以使用约束布局替代任何布局
group:替代contaier的效果,操作一个group,可以将很多视图的可见性绑定起来进行控制
示例代码:
1 | <Android.support.constraint.Group |
在代码中控制R.id.container_coupon的visiability即可同时控制tv_coupon和bg_coupon的可见性
坑1:使用group控制的view,无法操作自己的可见性,其可见性会被group的可见性覆盖,因此需要将group的粒度控制到最小,确保不会出现一个group的view拥有自己独立的可见性
坑2:使用group控制的view,设置了自己的可见性,立即获取其可见性,此时是设置的可见性,而在下一次重绘之后,则会被覆盖变成group的可见性,业务代码尽量不通过getvisibility()的方式来写业务。
constraintDimensionRatio:可以自由的操作宽高比例
使用这个后可以完全替代项目中的FixedWidthHeightRatioXXXLayout
其用法为
1 | app:layout_constraintDimensionRatio="4:3" |
chain:替代weight,space,将几个view组合起来平分或者按比例分配竖直或者横向的空间
在xml视图中切换到视图模式,按住command可以连续选择几个view,然后右键,创建chain,即可使这几个view平分或者设置比例分配视图的空间。
goneMargin:视图属性为gone时,以自己为坐标的视图会额外拥有的margin边距
这个可以保证即使当前view不存在时,后续视图仍有坐标系可遵循,否则的话后续view将直接以当前view依赖的前置view直接显示自身,会导致布局错乱。
textview设置宽度为0dp,然后设置好startof和endof,即可充足利用一段空间,并且从头开始排布
这个既可以满足textview的宽度动态变化,又可以保证textview的绘制是从这段空间的起始处开始。
include和merge标签共用
代码如下
1 | <LinearLayout xmlns:Android="http://schemas.Android.com/apk/res/Android" |
其中A_layout可以写为如下
1 | <merge xmlns:Android="http://schemas.Android.com/apk/res/Android"> |
这样在绘制A_layout时会将这三个textview直接添加到linearlayout中,与helloworld的textview同层级。而往往我们会将A_layout写为有父layout的布局,导致多了一层绘制。
使用viewstub
在基于业务的场景下,很多复杂的布局在点击或者别的事件触发时才会加载,如果页面过于复杂,使用viewstub比较好。
但是viewstub涉及到了一次inflate,inflate是io操作,会导致主线程卡顿,在加载视图十分复杂时使用,简单视图不推荐使用。
去除背景
imageview如果是没有设置背景,而只设置了src的话,只有一层绘制,但是如果同时设置了背景和src的话,则就产生了两层绘制。
因此尽量不要使用setbackground的操作,设置的话可以通过设置背景为selector,尽可能区分状态,在大部分状态设为透明。
部分工具使用操作
检测app启动方法耗时
app启动速度分为冷启动和热启动,冷启动时进程不存在,热启动则相反。
启动方法耗时无法通过traceview实时抓取。仅可通过app自己抓取。
抓取样例