美文网首页Android性能优化Android性能优化Android开发经验谈
六、Android性能优化之UI卡顿分析之渲染性能优化

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

作者: 香沙小熊 | 来源:发表于2017-09-01 16:38 被阅读155次

    渲染功能是应用程序最普遍的功能,开发任何应用程序都是这样,一方面,设计师要求为用户展现可用性最高的超
    然体验,另一方面,那些华丽的图片和动画,并不是在所有的设备上都能刘畅地运行。我们来了解一下什么是渲染性能。
    首先,我们要知道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

    相关文章

      网友评论

        本文标题:六、Android性能优化之UI卡顿分析之渲染性能优化

        本文链接:https://www.haomeiwen.com/subject/suphjxtx.html