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

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


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

时间: 2024-10-25 19:28:54

一次优化列表页卡顿的经历的相关文章

记一次程序严重卡顿的经历

前几天在电脑上VS的安装后发现有一程序,叫applicaton verifier是用来记录某一个应用程序执行情况的工具,偶然就将工作中所使用的一个开后程序引入其中,当时并没有仔细研究如何使用. 过了几天因为工作上要使用此程序,启动后发现这个程序启动后Cpu占用非常严重,以致程序中SendKeys在发送Tab键时,这个程序就无响应挂掉了,无耐之下翻源代码定位问题,结果两句代码之间执行居然要30秒左右,无解了,后来将此程序执行文件重新命名后,居然一切正常了. 后来在Windows开始程序中发现这个程

【凯子哥带你学Android】Andriod性能优化之列表卡顿——以“简书”APP为例

这几天闲得无聊,就打开手机上的开发者模式里面的"GPU过度绘制"功能,看看别家的App做的咋样,然后很偶然的打开了"简书",然后就被它的过度绘制惊呆了,于是写了这篇性能分析的文章,从一个只有APK文件的角度,说下如何寻找布局中可能存在的性能问题,以及解决方案.本文章以简书Android最新版本1.9.1进行分析. GPU过度绘制 Hierarchy View SysTrace TraceView 总结 分析资源下载 GPU过度绘制 首先打开下面两个功能开关 开发者模

嵌入页面的VLC播放rtsp流卡顿

目前正在开发开源软件VLC嵌入到页面中播放rtsp数据流,但是发现嵌入页面的VLC(页面启动时,安装VLC的插件)播放rtsp流相比较VLC的播放器而言,会有卡顿的现象,请问有没有朋友遇到类似的问题,如果有请帮忙解答下,不甚感激.

[UI列表]LoopScrollRect无限滑动不卡顿

应用场景 对于背包界面,排行榜列表,聊天消息,等有大量的UI列表的界面,常规做法是为每一条数据生成一个格子,在数据量越大的情况下,会生成越来越多的Gameobject,引起卡顿. 这篇文章讲述的就是解决UI列表卡顿的方法,在列表中只生成指定数量的Gameobject,滑动时进行数据更新,保证性能. LoopScrollRect(无限滑动不卡顿) 插件地址:https://github.com/qiankanglai/LoopScrollRect 中文文档:http://qiankanglai.m

Android app 性能优化的思考--性能卡顿不好的原因在哪?

说到 Android 系统手机,大部分人的印象是用了一段时间就变得有点卡顿,有些程序在运行期间莫名其妙的出现崩溃,打开系统文件夹一看,发现多了很多文件,然后用手机管家 APP 不断地进行清理优化 ,才感觉运行速度稍微提高了点,就算手机在各种性能跑分软件面前分数遥遥领先,还是感觉无论有多大的内存空间都远远不够用.相信每个使用 Android 系统的用户都有过以上类似经历,确实,Android 系统在流畅性方面不如 IOS 系统,为何呢,明明在看手机硬件配置上时,Android 设备都不会输于 IO

频繁GC会造成卡顿

频繁GC会造成卡顿 https://www.cnblogs.com/qcloud1001/p/9525078.html 一款app除了要有令人惊叹的功能和令人发指交互之外,在性能上也应该追求丝滑的要求,这样才能更好地提高用户体验. 以下是本人在工作中对经历过的性能优化的一些总结,依据故事的发展路线,将其分为了5个部分,分别是:常见的性能问题:产生性能问题的一些可能原因:解决性能问题的套路:代码建议及潜在性能问题排查项. img1.png 如看不清大图,下文会有拆解 一 首先,我们先了解一下都有哪

Android App 卡顿分析

极力推荐Android 开发大总结文章:欢迎收藏 程序员Android 力荐 ,Android 开发者需要的必备技能 Android App 反应卡顿,从技术上将就是UI 渲染慢. UI渲染是从您的应用程序生成一个框架并将其显示在屏幕上的行为. 为了确保用户与您的应用程序的交互顺利,您的应用程序应该在16ms内渲染帧数达到每秒60帧(为什么60fps?). 如果您的应用程序因UI渲染速度缓慢而受到影响,那么系统将被迫跳过帧,用户将感觉到您的应用程序中出现卡顿. 我们把这个叫做jank. 本篇文章

跳出小程序 video组件 卡顿、黑屏、全屏等坑

前些天,朋友遇到一个小程序的问题叫我帮忙看看,说是ios上video组件会有严重的黑屏现象,这就有意思了. 知道问题后,我就开始试一试,发现如果页面只有一个video组件的话,是没有什么问题的.但是但页面有多个video的时候,问题就有点严重了: 1.设置了播放方式为非自动播放,但是进到页面的是还是时不时有一两个会自动播放 2.卡,页面很卡 3.进入全屏的时候,视频方向是根据宽高自己适配,但是退出全屏的时候,会出现这种情况:刚刚视频是横屏播放,退出了页面也是横屏 4.退出全屏后,页面上除了刚刚那

Android 卡顿检测方案

应用的流畅度最直接的影响了 App 的用户体验,轻微的卡顿有时导致用户的界面操作需要等待一两秒钟才能生效,严重的卡顿则导致系统直接弹出 ANR 的提示窗口,让用户选择要继续等待还是关闭应用. 所以,如果想要提升用户体验,就需要尽量避免卡顿的产生,否则用户经历几次类似场景之后,只会动动手指卸载应用,再顺手到应用商店给个差评.关于卡顿的分析方案,已经有以下两种: 分析 trace 文件.通过分析系统的/data/anr/traces.txt,来找到导致 UI 线程阻塞的源头,这种方案比较适合开发过程