写下这篇文章的日期是2016年4月初。当时来到公司,项目之前是外包出去的,代码乱糟糟的,需要重构掉,
摆在面前的问题不是重构项目,而是一些列表页的紧急的性能优化。
1.先优化item的层级
其实层级只要不是太深的话,比如5层,6层,对性能的差别在中等性能的机器上几乎看不出来的,但是想要做到 极致,
我就得死扣细节,原来代码是有4层的,其实有一点点接近可优化的范围了,我把原来的4层降到1层。
1层的话在item的话,在cpu进行计算测量的时候就速度很快了。
下面是我用DDMS去查看某台和我台的列表的控件层级对比。
如图:某台的列表的item的层级,三层:Grid>Linear>Frame>Rela
我台 的列表页的优化后
的item的层级,一层:Grid>Rela
其实敌台列表页卡顿也是严重,而且肉眼可见的控件,敌台比我台少了一个圆形头像的控件,bitmap占大头,虽然敌台有几个textview,但是textview本身就几乎没什么可以性能优化的东西了。但是我依旧要做的比敌台还要流畅。
2.优化OVERDRAW过渡绘制的问题。
旧的代码,先来看下listview的getview方法的代码:
@Override
public View getView(int position, View convertView, ViewGroup parent) {
if (convertView == null) {
convertView = LayoutInflater.from(context).inflate(R.layout.item_live_adapter, parent, false);
}
LinearLayout layout_item_live = BaseViewHolder.get(convertView, R.id.layout_item_live);
if (position % 2 == 0) {//重点在于这里
layout_item_live.setPadding(20, 20, 10, 0);
} else if (position % 2 == 1) {
layout_item_live.setPadding(10, 20, 20, 0);
}
ImageView iv_item_live_cover = BaseViewHolder.get(convertView, R.id.iv_item_live_cover);
TextView tv_live_hostname = BaseViewHolder.get(convertView, R.id.tv_live_hostname);
TextView tv_item_live_viewernum = BaseViewHolder.get(convertView, R.id.tv_item_live_viewernum);
TextView tv_item_live_title = BaseViewHolder.get(convertView, R.id.tv_item_live_title);
CircleImageView iv_item_recycle_host_head = BaseViewHolder.get(convertView, R.id.iv_item_recycle_host_head);
bitmapUtils.configDefaultLoadFailedImage(R.mipmap.live_default);
bitmapUtils.configDefaultLoadingImage(R.mipmap.live_default);
bitmapUtils.display(iv_item_live_cover, list_info.get(position).get("thumb").toString());
bitmapUtils.display(iv_item_recycle_host_head , list_info.get(position).get("avatar").toString());
tv_live_hostname.setText(list_info.get(position).get("nick").toString());
int view = Integer.parseInt(list_info.get(position).get("view").toString());
if(view > 10000){
BigDecimal b1 = new BigDecimal(view);
BigDecimal b2 = new BigDecimal(10000);
tv_item_live_viewernum.setText(b1.divide(b2,1,BigDecimal.ROUND_HALF_UP).doubleValue()+"W");
}else{
tv_item_live_viewernum.setText(view + "");
}
tv_item_live_title.setText(list_info.get(position).get("title").toString());
return convertView;
}
setPadding
理论上来说会出发childMeasure
方法,然后就是一堆的UI线程的不断去计算的东西。然后子层级和自控件也不是特别少,所以这里问题也是很大啊!!
用traceview追踪卡顿的过程CPU耗时在哪里比较多
旧的代码运行时,滚动列表,为了公平,在listview上下滚动过之后,保证有了图片的内存缓存之后,清理下log,再滚动抓取的截图如下
ui线程只有空闲的时候才会去循环loop。如果我的list滚动的性能不好loop的占用的百分比肯定低。目前,Looper的占比只有50%左右。
往下翻,请仔细看30那行,虽然我选中的时25行!!!
30这行指向了我们自己的com.maimiao…的一个自定义控件,占了35%的cpu耗时,所以这里有点严重了。如果去除这35%,基本上也是接近于90%的loope。
贴出traceview指向的这个自定义控件的一些方法
RoundImageViewByXfermode
这个类是旧的代码里用于做圆角而使用的自定义控件。其实实现圆角有三种方案,我按性能高低排序往下说,
- 第一种,画矩形图,不修圆角,使用和背景色一致OVERCOLOR盖住四个角
- 第二种,bitmapShader。
- 第三种,Xfermode。旧的代码正是用的这种。
@SuppressLint("DrawAllocation")
@Override
protected void onDraw(Canvas canvas)
{
//在缓存中取出bitmap
Bitmap bitmap = mWeakBitmap == null ? null : mWeakBitmap.get();
if (null == bitmap || bitmap.isRecycled())
{
//拿到Drawable
Drawable drawable = getDrawable();
if (drawable != null)
{
//获取drawable的宽和高
int dWidth = drawable.getIntrinsicWidth();
int dHeight = drawable.getIntrinsicHeight();
//创建bitmap
bitmap = Bitmap.createBitmap(getWidth(), getHeight(),
Config.ARGB_8888);
float scale = 1.0f;
//创建画布
Canvas drawCanvas = new Canvas(bitmap);
//按照bitmap的宽高,以及view的宽高,计算缩放比例;因为设置的src宽高比例可能和imageview的宽高比例不同,这里我们不希望图片失真;
if (type == TYPE_ROUND)
{
// 如果图片的宽或者高与view的宽高不匹配,计算出需要缩放的比例;缩放后的图片的宽高,一定要大于我们view的宽高;所以我们这里取大值;
scale = Math.max(getWidth() * 1.0f / dWidth, getHeight()
* 1.0f / dHeight);
} else
{
scale = getWidth() * 1.0F / Math.min(dWidth, dHeight);
}
//根据缩放比例,设置bounds,相当于缩放图片了
drawable.setBounds(0, 0, (int) (scale * dWidth),
(int) (scale * dHeight));
drawable.draw(drawCanvas);
if (mMaskBitmap == null || mMaskBitmap.isRecycled())
{
mMaskBitmap = getBitmap();
}
// Draw Bitmap.
mPaint.reset();
mPaint.setFilterBitmap(false);
mPaint.setXfermode(mXfermode);
//绘制形状
drawCanvas.drawBitmap(mMaskBitmap, 0, 0, mPaint);
mPaint.setXfermode(null);
//将准备好的bitmap绘制出来
canvas.drawBitmap(bitmap, 0, 0, null);
//bitmap缓存起来,避免每次调用onDraw,分配内存
mWeakBitmap = new WeakReference<Bitmap>(bitmap);
}
}
//如果bitmap还存在,则直接绘制即可
if (bitmap != null)
{
mPaint.setXfermode(null);
canvas.drawBitmap(bitmap, 0.0f, 0.0f, mPaint);
return;
}
}
/**
* 绘制形状
* @return
*/
public Bitmap getBitmap()
{
Bitmap bitmap = Bitmap.createBitmaps(getWidth(), getHeight(),
Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setColor(Color.BLACK);
if (type == TYPE_ROUND)
{
canvas.drawRoundRect(new RectF(0, 0, getWidth(), getHeight()),
mBorderRadius, mBorderRadius, paint);
} else
{
canvas.drawCircle(getWidth() / 2, getWidth() / 2, getWidth() / 2,
paint);
}
return bitmap;
}
如上,代码明显性能很差,createBitmap,Bitmap的色彩用的也是ARGB_8888,
内存消耗也很大,同时还频繁使用setXfermode模式,这些东西都在UI线程走,性能极差。
为了证明traceview没骗我,我也没冤枉他,我把自定义控件改成了imageview,padding也去掉,性能确实提升了很高。
为了解决这些问题我打算用fresco去完成这些工作,fresco的老东家把android性能优化做到了极致,研究了好多年,也贡献了很多文献资料开源的东西。
fresco,对于圆角和圆形有两种方案
- 第一种,OVERLAY_COLOR,就是正常展示一张正方形图,圆角的地方用非透明颜色绘制遮挡的方案,来完成,这种性能极高。但是,如果碰到控件背景非透明的情况,就没法用了。
- 第二种,BITMAP_ONLY,原型图他是用BitmapShader来完成性能不怎样,但是fresco只能尽量帮你做到不重复创建bitmap之类的性能问题,来实现圆形或者圆角。
fresco绝对不用xfermode方案。
使用SystemClock去抓取getView或者bindHolder方法之行的耗时
贴出优化之前的getview中代码段的耗时,为了公平,不把LayoutInflat这行代码纪录进来。因为后面我打重构这个list之后,我用的是RecyclerView,bind方法和create方法是分开的,我要保证公平。
明显这个旧的getView的代码已经明显出现耗时的抖动问题了,重构之后的那段BindHolder
代码的cpu耗时标准的在2-4ms之间,后面会贴出截图。旧的代码他猛起来居然可以跑到7,其实有些地方截图没截到,还有10ms的,一个getView都10ms,那一些log看不到的地方再算下来会更加严重的,这就是为什么这个list这么卡的原因。
android屏幕刷新频率是16ms,如果一旦UI线程繁忙超过16ms,那么就很容易被看出卡顿,
当然页面静止的情况下页面卡顿的话也是很难看出来,因为卡是静止的,不卡也是静止的。
但是到列表页上之后,就不同了,到了列表页,很多问题都容易被凸显出来,滚动不流畅,明显掉帧,这些都要去仔细找问题。
为了找到这段getView中cpu耗时抖动的问题,我先猜测一些第三方的东西,那就是xutils了,其次时BaseViewHolder这个查找view的代码,他内部是SparseArray,理论上来说性能也是蛮高的,所以先从xutils下手,我不能没有证据的责怪xutils,所以我再次打log在xutils那三四行代码上,不抓取其他的代码耗时。
为了保证公平,我多上下滚动几遍,等xutils把图片都缓存到内存中之后,我清掉日志,开始滚动,看到的数据是这样子。
确实很明显,这里只抓取xutils的三四行代码,居然有两次他cpu耗时到了4,安静下来的时候也可以是0,所以,无疑这个地方xutils难逃干系。
下面是重构方案
重构之后
重构之后这里圆角和圆形都使用fresco来完成这些,圆角的大图由于背景色是白色就可以用OVERLAY_COLOR,那个圆形头像背景需要透明,所以还是得BITMAP_ONLY模式。
//贴出重构之后的bindHolder代码
@Override
public void onBindItemViewHolder(LiveFragholder holder, final int position) {
String url=getList().get(position).get("avatar").toString()+"";
FrescoUtils.displayAvatar(holder.iv_item_recycle_host_head,url);
FrescoUtils.displayUrl(holder.iv_item_live_cover, getList().get(position).get("thumb").toString());
holder.tv_live_hostname.setText(getList().get(position).get("nick").toString());
int view = Integer.parseInt(getList().get(position).get("view").toString());
if(view > 10000){
BigDecimal b1 = new BigDecimal(view);
BigDecimal b2 = new BigDecimal(10000);
holder.tv_item_live_viewernum.setText(b1.divide(b2,1,BigDecimal.ROUND_HALF_UP).doubleValue()+"W");
}else{
holder.tv_item_live_viewernum.setText(view + "");
}
holder.tv_item_live_title.setText(getList().get(position).get("title").toString());
final Context context=holder.iv_item_live_cover.getContext();
holder.iv_item_live_cover.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
HashMap<String,String> mob_map = new HashMap<>();
//因为某些圆形这里还在使用hasmap
mob_map.put("position",position + "");
MobclickAgent.onEvent(context, UmengCollectConfig.ZB_FL,mob_map);
Bundle bundle = new Bundle();
bundle.putString("uid", getList().get(position).get("uid").toString());
Intent intent = new Intent();
intent.setClass(context, TheLiveActivity.class);
intent.putExtras(bundle);
context.startActivity(intent);
}
});
}
来看下优化之后的滚动过程的时traceview抓取的数据。
明显看到一个点,90.2%的耗时都在Looper.loop上。这说明是正常的,上面说了ui线程只有空闲的时候才会去循环的loop。如果我的list滚动的性能不好loop的占用的百分比肯定低。
在我的BindHoder
代码开头做一个毫秒级时间戳的记录,在结尾处也做个纪录,对比两个时间戳的差值,查看具体我在BindHoder
中的代码耗时。
cpu耗时标准的在2-4ms之间。非常的稳定,没有乱飘的现象。
简直完美!!!!对比下敌台的列表页,肉眼明显可见的敌台列表页卡顿比我们还严重,而且对方的item还比我们多了一个圆形头像的控件。终于可以拍着膀子说我们比敌台性能好。
目前这个列表页,我台依旧要比敌台性能好,只是首页的第一个tab页面,我台也很卡,敌台也很卡,
只是目前我还没有抽出时间去处理第一个tab页,后期会去优化,另外就是我台是哪里,我台是**全民TV**。
这是我的公众号,胆敢扫我,给你好看!