Android 掉帧优化

对于传统的60刷新率手机来说,每16ms会发出一个VSync信号,复制CPU/GPU放在缓存中的图像,再通知CPU/GPU计算下一帧要显示的内容,再把刚复制的图像显示在屏幕上,这就是一个屏幕刷新周期。而如果在16ms内没有计算完毕的话,该帧就无法展示,屏幕进入下一个刷新周期,就产生了所谓的掉帧现象。

1. 掉帧监控 监控掉帧现象时,我们可以使用下方的adb命令,具体可见参考.

adb shell dumpsys gfxinfo <packageName>

复制

该命令展示的信息比较完整,如下所示。

Applications Graphics Acceleration Info:
Uptime: 275941522 Realtime: 391854346

** Graphics info for pid 6887 [packageName] **

Stats since: 275926453465347ns
Total frames rendered: 523   // 本次共收集了523帧的信息
Janky frames: 26 (4.97%)   // 有26帧的耗时超过16ms,掉帧率为4.97%
50th percentile: 5ms   // 50%的帧耗时在5ms以内
90th percentile: 8ms
95th percentile: 16ms
99th percentile: 20ms
Number Missed Vsync: 0   // 垂直同步失败的帧
Number High input latency: 259   // 处理input时间超时的帧数
Number Slow UI thread: 1   // 因UI线程上的工作导致超时的帧数
Number Slow bitmap uploads: 0   // 因bitmap的加载耗时的帧数
Number Slow issue draw commands: 0   // 因绘制导致耗时的帧数
Number Frame deadline missed: 1
HISTOGRAM: 5ms=346 6ms=72 7ms=31 .........   // 耗时0-5ms的帧有346......

各种缓存......
Total GPU memory usage:
  40704248 bytes, 38.82 MB (36.77 MB is purgeable)
......

复制

如果只看掉帧率,可以用adb shell dumpsys gfxinfo| grep "Janky frames"命令。 如果想重新开始计算帧率信息,可以通过adb shell dumpsys gfxinforeset重置。

当然我们也可以通过可视化界面查看UI性能,打开"开发者选项"中的"GPU渲染模式分析",即可在屏幕上看到每一帧绘制时间的直方图,某个值越大,代表该帧绘制的时间越长。如下图所示,冷启动APP时有不少帧的绘制时间已经远远超过了16ms。

除了"GPU渲染模式分析",还有Android Studio中的CPU Profile用于查看APP运行时的方法调用栈,辅助开发人员定位热点方法并优化。我们来做个实验,在Demo中的onBindViewHolder()中添加Thread.sleep(5),使每次绑定ItemView都会多消耗5ms。

运行程序后打开Profile,可以看到CPU、MEMORY、NETWORK和ENERGY四个动态图表,点击CPU后,下方出现CPU Profile界面,如下所示,点击"record"即可开始记录,点击"stop"后得到这一段时间内的方法调用栈。

得到方法调用栈信息后,先从"Flame Chart"模式来看热点方法,很明显sleep函数耗时较多。

如果想要数字化的信息,可以通过"Top Down"模式查看每个方法及其子方法的耗时和百分比,分析时一般点击耗时占比高的方法查看它的子方法哪个耗时较多,再一步步追踪下去。 在我们的例子中,sleep()函数占总耗时的49.58%,是耗时最多的方法。

总结一下,CPU Profile为开发者提供了强大的分析工具,我们很容易定位APP运行时耗时多的方法,然后具体问题具体分析。当然CPU Profile不仅仅用于掉帧优化,有优化的地方就有它的身影,例如启动优化等。

2. 掉帧优化措施

① 正确使用缓存

关于mCachedViews: mCachedViews针对ItemView的position进行缓存。当一个Item滑出可视区域时,它会先被放入mCachedViews中;而当一个Item滑入可视区域时,Recycler也会优先去mCachedViews中查找。 根据这个特性,当用户频繁地上下滑动时,mCachedViews的利用率会较高。那么针对频繁上下滑动的场景,我们可以通过RecyclerView.setItemViewCacheSize(…)来增大mCachedViews的容量,这样Recycler更容易在mCachedViews中找到缓存,减少之后的onBindViewHolder()和onCreateViewHolder()调用。

关于RecyclerPool: RecyclerPool针对某个ViewType进行缓存,默认大小为5,但是对于某些场景这是远远不够的。试想一个能在可视区域展示n(n>>5)条数据的RecyclerView(如历史记录),当滑动的时候RecyclerPool的缓存明显不够,会不断地创建ViewHolder,很消耗性能。针对这种情况,可以通过RecyclerView.getRecycledViewPool().setMaxRecycledViews(int viewType, int max)增大特定ViewType的缓存容量。

如果多个RecyclerView的内容性质相同,例如在信息流中,多个Fragment中的Item类型相同。那么可以为它们设置同一个RecyclerPool(默认是1个RecyclerView创建一个RecyclerPool),通过RecyclerView.setRecycledViewPool(pool)设置即可。

② 优化onBindViewHolder()耗时

从RecyclerPool中取出的ViewHolder都会调用onBindViewHolder()加载数据,该方法是在主线程运行的,处理不当时很容易造成滑动卡顿。 当为ItemView设置点击监听时,不要在onBindViewHolder()中新建OnClickListener,这不仅会新建多余的对象消耗内存,也会增加onBindViewHolder()的耗时。可以让所有的Item共用一个监听器,然后根据具体的Item来处理事件。

平时重写的onBindViewHolder(ViewHolder holder, int pos)会更新ItemView的所有内容,如果想要局部更新,可以重写onBindViewHolder(ViewHolder holder, int pos, Listpayloads)。当ItemView更新时,调用Adapter.notifyItemChanged(position, payLoad)即可。具体可见参考5,通过这个方法解决了ItemView更新时图片闪烁的问题。

③ 布局优化 布局优化一个比较典型的优化项就是优化过度绘制,打开"开发者选项"中的"调试GPU过度绘制",就能看到屏幕上每个像素点在屏幕上绘制了多少次。

对过度绘制进行优化时,首先要考虑合适的控件容器,也就是Layout。虽然Google推出了约束布局ConstraintLayout,但是它性能上并不优秀,不建议使用。 其次要善用merge和ViewStub。merge用于减少布局层级,例如自定义ViewGroup时,可以用作为根布局。ViewStub是布局文件中的占位符,对于某些在特殊场景下才需要显示的控件,可以先用ViewStub代替,等到需要显示时再加载。

还有一个常见的优化项就是layout_weight,该属性可以很轻松地实现空间分配,但是也很容易成为性能瓶颈,能不用就不用。

④ measure()优化和减少requestLayout()调用

当RecyclerView宽高的测量模式都是EXACTLY时,onMeasure()方法不需要执行dispatchLayoutStep1()等方法来进行测量。而当RecyclerView的宽高不确定并且至少一个child的宽高不确定时,要measure两遍。 因此将RecyclerView的宽高模式都设置为EXACTLY有助于优化性能。

protected void onMeasure(int widthSpec, int heightSpec) {
    // ......
    if (mLayout.isAutoMeasureEnabled()) {
        final int widthMode = MeasureSpec.getMode(widthSpec);
        final int heightMode = MeasureSpec.getMode(heightSpec);

        mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);

        final boolean measureSpecModeIsExactly =
                widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY;
        if (measureSpecModeIsExactly || mAdapter == null) {
            return;
        }
        // ......
}

复制

还有一个方法RecyclerView.setHasFixedSize(true)可以避免数据改变时重新计算RecyclerView的大小,来看一下方法注释。 注释上说,如果Adapter的变化不会影响RecyclerView的size,那么可以设置mHasFixedSize为true来避免Adapter改变时RecyclerView刷新整个Layout。也就是说,不管数据变成什么样,如果RecyclerView的宽高都不会变,那么设置这个属性为true。

/**
 * RecyclerView can perform several optimizations if it can know in advance that RecyclerView's
 * size is not affected by the adapter contents. RecyclerView can still change its size based
 * on other factors (e.g. its parent's size) but this size calculation cannot depend on the
 * size of its children or contents of its adapter (except the number of items in the adapter).
 * <p>
 * If your use of RecyclerView falls into this category, set this to {@code true}. It will allow
 * RecyclerView to avoid invalidating the whole layout when its adapter contents change.
 *
 * @param hasFixedSize true if adapter changes cannot affect the size of the RecyclerView.
 */
public void setHasFixedSize(boolean hasFixedSize) {
    mHasFixedSize = hasFixedSize;
}

复制

当Adapter调用onItemRangeChanged(), onItemRangeInserted(), onItemRangeRemoved(), onItemRangeMoved()这4个方法时会调用triggerUpdateProcessor(),当mHasFixedSize为true时,不会调用requestLayout()重新计算宽高。 注意:如果调用notifyDataSetChanged()还是会调用requestLayout()去计算宽高。

void triggerUpdateProcessor() {
    if (POST_UPDATES_ON_ANIMATION && mHasFixedSize && mIsAttached) {
        ViewCompat.postOnAnimation(RecyclerView.this, mUpdateChildViewsRunnable);
    } else {
        mAdapterUpdateDuringMeasure = true;
        requestLayout();
    }
}
  • 17
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值