六、Android性能优化之UI卡顿分析之渲染性能优化

渲染功能是应用程序最普遍的功能,开发任何应用程序都是这样,一方面,设计师要求为用户展现可用性最高的超
然体验,另一方面,那些华丽的图片和动画,并不是在所有的设备上都能刘畅地运行。我们来了解一下什么是渲染性能。
首先,我们要知道Android系统每隔16ms就重新绘制一次Activity,也就是说,我们的应用必须在16ms内完成屏幕刷新的全部逻辑操作,这样才能达到每秒60帧,然而这个每秒帧数的参数由手机硬件所决定,现在大多数手机屏幕刷新率是60赫兹(赫兹是国际单位制中频率的单位,它是每秒中的周期性变动重复次数的计量),也就是说我们有16ms(1000ms/60次=16.66ms)的时间去完成每帧的绘制逻辑操作,如果错过了,比如说我们花费34ms才完成计算,那么就会出现我们称之为丢帧的情况。

简单理解16ms应该完成所有事情
GC回收时间过长导致卡顿

渲染管线

Android系统的渲染管线分为两个关键组件:CPU和GPU,它们共同工作,在屏幕上绘制图片,每个组件都有自身定义>的特定流程。我们必须遵守这些特定的操作规则才能达到效果。

Android系统的渲染管线

在CPU方面,最常见的性能问题是不必要的布局和失效,这些内容必须在视图层次结构中进行测量、清除并重新创建,引发这种问题通常有两个原因:一是重建显示列表的次数太多,二是花费太多时间作废视图层次并进行不必要的重绘,这两个原因在更新显示列表或者其他缓存GPU资源时导致CPU工作过度。
在GPU方面,最常见的问题是我们所说的过度绘制(overdraw),通常是在像素着色过程中,通过其他工具进行后期着色时浪费了GPU处理时间。
接下来我们将讲解更多关于失效布局和重绘的内容,以及如何使用SDK中的工具找出拖累应用性能的原因

渲染优化

想要开发一款性能优越的应用,我们必须了解底层是如何运行的。有一个主要问题就是,Activity是如何绘制到屏幕上的?那些复杂的XML布局文件和标记语言,是如何转化成用户能看懂的图像的?
实际上,这是由格栅化操作来完成的,栅格化就是将例如字符串、按钮、路径或者形状的一些高级对象,拆分到不同的像素上在屏幕上进行显示,栅格化是一个非常费时的操作。我们所有人的手机里面都有一块特殊硬件,它就是图像处理器(GPU显卡的处理器),目的就是加快栅格化的操作,GPU在上个世纪90年代被引入用来帮助加快栅格化操作。

加快格栅化的操作

GPU使用一些指定的基础指令集,主要是多边形和纹理,也就是图片,CPU在屏幕上绘制图像前会向GPU输入这些指令,这一过程通常使用的API就是Android的OpenGL ES,这就是说,在屏幕上绘制UI对象时无论是按钮、路径或者复选框,都需要在CPU中首先转换为多边形或者纹理,然后再传递给GPU进行栅格化。

polygons多边形和textures纹理

我们要知道,一个UI对象转换为一系列多边形和纹理的过程肯定相当耗时,从CPU上传处理数据到GPU同样也很耗时。所以很明显,我们需要尽量减少对象转换的次数,以及上传数据的次数,幸亏,OpenGL ES API允许数据上传到GPU后可以对数据进行保存,当我们下次绘制一个按钮时,只需要在GPU存储器里引用它,然后告诉OpenGL如何绘制就可以了,一条经验之谈:渲染性能的优化就是尽可能地上传数据到GPU,然后尽可能长地在不修改的情况下保存数据,因为每次上传资源到GPU时,我们都会浪费宝贵的处理时间,Android系统的Honeycomb版本发布之后,整个UI渲染系统就在GPU中运行,之后各个版本都在渲染系统性能方面有更多改进。
Android系统在降低、重新利用GPU资源方面做了很多工作,这方面完全不用担心,举例说,任何我们的主题所提供的资源,例如Bitmaps、Drawables等都是一起打包到统一的纹理当中,然后使用网格工具上传到GPU,例如Nine Patches等,这样每次我需要绘制这些资源时,我们就不用做任何转换,他们已经存储在GPU中了,大大加快了这些视图类型的显示。然而随着UI对象的不断升级,渲染流程也变得越来越复杂,例如说绘制图像,就是把图片上传到CPU存储器,然后传递到GPU中进行渲染。路径使用时完全另外一码事,我们需要在CPU中创建一系列的多边形,甚至在GPU中创建掩蔽纹理来定义路径。绘制字符就更加复杂一些,首先我们需要在CPU中把字符绘制制成图像,然后把图像上传到GPU进行渲染再返回到CPU,在屏幕上为字符串的每个字符绘制一个正方形。

总结上述原因,在我们的绘制渲染机制里面比较耗时的:
1.CPU计算时间

CPU的优化,从减轻加工View对象成Polygons和Texture来下手
View Hierarchy中包涵了太多没有的View,这些view根本就不会显示在屏幕上面,一旦触发测量和布局操作,就会拖累应用的性能表现。

2.CPU将计算好的Polygons和Texture传递到GPU的时候也需要时间

OpenGL ES API允许数据上传到GPU后可以对数据进行保存,缓存到display list。因此,我们平移等操作一个view是几乎不怎么耗时的。

3.GPU进行栅格化
CPU优化建议

针对CPU的优化,从减轻加工View对象成Polygons和Texture来下手:

View Hierarchy中包涵了太多的没有用的view,这些view根本就不会显示在屏幕上面,一旦触发测量和布局操作,就会拖累应用的性能表现。那么我们就需要利用工具进行分析。

如何找出里面没用的view呢?或者减少不必要的view嵌套。

我们利用工具:Hierarchy Viewer进行检测,优化思想是:查看自己的布局,层次是否很深以及渲染比较耗时,然后想办法能否减少层级以及优化每一个View的渲染时间。

我们打开APP,然后打开Android Device Monitor,然后切换到Hierarchy Viewer面板。除了看层次结构之外,还可以看到一些耗时的信息:

Hierarchy Viewer

三个圆点分别代表:测量、布局、绘制三个阶段的性能表现。
1)绿色:渲染的管道阶段,这个视图的渲染速度快于至少一半的其他的视图。
2)黄色:渲染速度比较慢的50%。
3)红色:渲染速度非常慢。

优化思想:查看自己的布局,层次是否很深以及渲染比较耗时,然后想办法能否减少层级以及优化每一个View的渲染时间。

优化前
优化后

优化建议:

1.当我们的布局是用的FrameLayout的时候,我们可以把它改成merge,可以避免自己的帧布局和系统的ContentFrameLayout帧布局重叠造成重复计算(measure和layout)。
2.使用ViewStub:当加载的时候才会占用。不加载的时候就是隐藏的,仅仅占用位置。

GPU优化建议就是一句话:尽量避免过度绘制(overdraw)

GPU的主要问题 -过度绘制(overdraw)
如果我们曾经粉刷过房子,我们应该知道,给墙壁粉刷工作量非常大,如果我们需要重新粉刷,第一次的粉刷就白干了。同样的道理,我们的应用程序会因为过度绘制,从而导致性能问题,如果我们想兼顾高性能和完美的设计,往往会碰到一种性能问题,即过度绘制。过度绘制是一个术语,指的是屏幕上的某个像素点在同一帧的时间内被绘制了多次。假如我们有一堆重叠的UI卡片,最接近用户的卡片在最上面,其余卡片都藏在下面,也就是说我们花大力气绘制的那些下面的卡片基本都是不可见的。

过度绘制

问题就在于此,因为每次像素经过渲染后,并不是用户最后看到的部分,这就是在浪费GPU的时间。目前流行的一些布局是一把双刃剑,带给我们漂亮视觉感受的同时,也造成过度绘制的问题,为了最大限度地提高应用程序的性能,我们必须尽量减少过度绘制。幸运的是,Android手机提供了查看过度绘制情况的工具,在开发者选项中打开“Show GPU overdraw”选项,手机屏幕显示会出现一些异常不用过于惊慌,Android在屏幕上使用不同颜色,标记过度绘制的区域,如果某个像素点只渲染了一次,我们看到的是它原来的颜色,随着过度绘制的增多,标记颜色也会逐渐加深,例如1倍过度绘制会被标记为蓝色,2倍、3倍、4倍过度绘制遵循同样的模式。所以当我们调试应用程序的用户界面时,目标就是尽可能的减少过度绘制,将红色区块转变成蓝色区块,为了完成目标有两种清楚过度绘制的方法,首先要从视图中清楚那些,不必要的背景和图片,他们不会在最终渲染图像中显示,记住,这些都会影响性能。其次,对视图中重叠的屏幕区域进行定义,从而降低CPU和GPU的消耗,接下来我们深入了解过度绘制

1、背景经常容易造成过度绘制。

手机开发者选项里面找到工具:Debug GPU overdraw,其中,不同颜色代表了绘制了几次:

Show GPU overdraw工具

举例
由于我们布局设置了背景,同时用到的MaterialDesign的主题会默认给一个背景。解决的办法:将主题添加的背景去掉:

//将主题的背景去掉
getWindow().setBackgroundDrawable(null);

又例如我们的根布局经常会设置重复的背景,那么这时候就应该去掉一些不必要的背景。

还有的就是,我们在写列表控件的时候,如果Item在没有图片的时候需要一个背景色的时候,那么我们这时候就需要灵活地利用透明色来防止过度绘制:

        if (chat.getAuthor().getAvatarId() != 0) {
            Picasso.with(getContext()).load(chat.getAuthor().getAvatarId()).into(
                    chat_author_avatar);
        }
        chat_author_avatar.setBackgroundColor(chat.getAuthor().getColor());
优化前
ListView item中设置图片的同时,要设置背景Color.TRANSPARENT 防止因复用导致的过度绘制
if (chat.getAuthor().getAvatarId() == 0) {
    //没有头像的时候,需要把Drawable设置为透明,防止过度绘制(每次都要设置,因为Item会复用)
    Picasso.with(getContext()).load(android.R.color.transparent).into(chat_author_avatar);
    //没有头像的时候,需要设置默认的背景色
    chat_author_avatar.setBackgroundColor(chat.getAuthor().getColor());
} else {
    //有头像的时候,直接设置头像,并且把背景色设置为透明,同样也是防止过度绘制
    Picasso.with(getContext()).load(chat.getAuthor().getAvatarId()).into(
            chat_author_avatar);
    chat_author_avatar.setBackgroundColor(Color.TRANSPARENT);
}
优化后

发现设置的图片过度绘制颜色由红色变为绿色,减少了过渡绘制

2.自定义控件处理过度绘制。

如果我们的自定义控件存在一些被遮挡的不需要显示的区域,可以通过画布的裁剪来处理

public class DroidCardsView extends View {
    //图片与图片之间的间距
    private int mCardSpacing = 150;
    //图片与左侧距离的记录
    private int mCardLeft = 10;

    private List<DroidCard> mDroidCards = new ArrayList<DroidCard>();

    private Paint paint = new Paint();

    public DroidCardsView(Context context) {
        super(context);
        initCards();
    }

    public DroidCardsView(Context context, AttributeSet attrs) {
        super(context, attrs);
        initCards();
    }



    /**
     * 初始化卡片集合
     */
    protected void initCards(){
        Resources res = getResources();
        mDroidCards.add(new DroidCard(res,R.drawable.alex,mCardLeft));

        mCardLeft+=mCardSpacing;
        mDroidCards.add(new DroidCard(res,R.drawable.claire,mCardLeft));

        mCardLeft+=mCardSpacing;
        mDroidCards.add(new DroidCard(res,R.drawable.kathryn,mCardLeft));
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        for (DroidCard c : mDroidCards){
            drawDroidCard(canvas, c);
        }

        invalidate();
    }

    /**
     * 绘制DroidCard
     * @param canvas
     * @param c
     */
    private void drawDroidCard(Canvas canvas, DroidCard c) {
        canvas.drawBitmap(c.bitmap,c.x,0f,paint);
    }
}
裁剪前过度绘制情况
自定义控件中,通过画布的裁剪,处理掉不需要显示的区域

canvas.clipRect(c.x,0.0f,mDroidCards.get(i+1).x,c.height); //裁剪函数


public class DroidCardsView extends View {
    //图片与图片之间的间距
    private int mCardSpacing = 150;
    //图片与左侧距离的记录
    private int mCardLeft = 10;

    private List<DroidCard> mDroidCards = new ArrayList<DroidCard>();

    private Paint paint = new Paint();

    public DroidCardsView(Context context) {
        super(context);
        initCards();
    }

    public DroidCardsView(Context context, AttributeSet attrs) {
        super(context, attrs);
        initCards();
    }



    /**
     * 初始化卡片集合
     */
    protected void initCards(){
        Resources res = getResources();
        mDroidCards.add(new DroidCard(res,R.drawable.alex,mCardLeft));

        mCardLeft+=mCardSpacing;
        mDroidCards.add(new DroidCard(res,R.drawable.claire,mCardLeft));

        mCardLeft+=mCardSpacing;
        mDroidCards.add(new DroidCard(res,R.drawable.kathryn,mCardLeft));
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        for(int i=0;i<mDroidCards.size() -1;i++){
            DroidCard droidCard = mDroidCards.get(i);
            drawDroidCard(canvas,droidCard,i);
        }
        int last = mDroidCards.size() -1;
        if(last>=0){
            drawDroidCard(canvas,mDroidCards.get(last));
        }

        invalidate();
    }

    /**
     * 绘制DroidCard
     * @param canvas
     * @param c
     */
    private void drawDroidCard(Canvas canvas, DroidCard c,int i) {
      //  canvas.drawBitmap(c.bitmap,c.x,0f,paint);
        canvas.save();
        canvas.clipRect(c.x,0.0f,mDroidCards.get(i+1).x,c.height);
        canvas.drawBitmap(c.bitmap,c.x,0f,paint);
        canvas.restore();//裁剪和绘制完毕后恢复画布
    }


    /**
     * 绘制最后一张
     * @param canvas
     * @param c
     */
    private void drawDroidCard(Canvas canvas, DroidCard c) {
        canvas.drawBitmap(c.bitmap,c.x,0f,paint);

    }
}



裁剪后过度绘制情况

特别感谢:
小楠总
动脑学院Ricky

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,524评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,869评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,813评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,210评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,085评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,117评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,533评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,219评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,487评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,582评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,362评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,218评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,589评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,899评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,176评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,503评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,707评论 2 335

推荐阅读更多精彩内容