码迷,mamicode.com
首页 > 其他好文 > 详细

一次优化列表页卡顿的经历

时间:2016-05-27 11:57:15      阅读:618      评论:0      收藏:0      [点我收藏+]

标签:

写下这篇文章的日期是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**。

这是我的公众号,胆敢扫我,给你好看!
技术分享

一次优化列表页卡顿的经历

标签:

原文地址:http://blog.csdn.net/weizongwei5/article/details/51502728

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!