美文网首页
Android布局优化(二),减少过度绘制

Android布局优化(二),减少过度绘制

作者: William_hi | 来源:发表于2018-05-15 11:34 被阅读0次

    已经有人总结的很好了,自己再重新写,也还是那些点,直接拷贝过来。(下面会有转载地址)

    什么是过度绘制(OverDraw)


    在多层次重叠的UI结构里面,如果不可见的UI也在做绘制的操作,会导致某些像素区域被绘制了多次。这样就会浪费大量的CPU以及GPU资源。过度绘制最直观的影响就是会导致APP卡顿。还好系统有提供GPU过度绘制调试工具会在屏幕上用不同的颜色,来表明一个像素点位被重复绘制的次数。

    怎样开启GPU过度绘制调试工具?

    1.点击进入“设置”;
    2.点击进入“开发者选项”
    3.选中“调试GPU过度绘制”
    4.选中“显示过度绘制区域”

    此时,你会注意到屏幕的颜色变化了,别紧张。切换到你的应用,现在我们开始了解怎么通过改善布局来解决过度绘制问题。

    屏幕上不同的颜色代表着什么?

    1.原色没有被过度绘制 – 这部分的像素点只在屏幕上绘制了一次。
    2.蓝色1次过度绘制– 这部分的像素点只在屏幕上绘制了两次。
    3.绿色2次过度绘制 – 这部分的像素点只在屏幕上绘制了三次。
    4.粉色3次过度绘制 – 这部分的像素点只在屏幕上绘制了四次。
    5.红色4次过度绘制 – 这部分的像素点只在屏幕上绘制了五次。

    1502545-6f0d53fbf7aede42.png

    怎么解决应用过度绘制?

    由上面的知识,我们知道要解决过度绘制。即是要尽量减少屏幕上的红色区域,增加屏幕上的蓝色和绿色区域。我们的目标是要控制界面最多被过度绘制2次(不出现粉色和红色)。

    1.合理选择控件容器
    既然overdraw是因为重复绘制了同一片区域的像素点,那我们首先想到的是解决布局问题。Android提供的Layout控件主要包括LinearLayout、TableLayout、FrameLayout、RelativeLayout(这里我们不考虑AbsoluteLayout)。同一个界面我们可以使用不同的容器控件来表达,但是各个容器控件描述界面的复杂度是不一样的。一般来说LinearLayout最易,RelativeLayout较复杂。但是尺有所短,寸有所长,LinearLayout只能用来描述一个方向上连续排列的控件,容易导致布局文件嵌套太深,不符合布局扁平化的设计原理。而RelativeLayout几乎可以用于描述任意复杂度的界面。但是表达能力越强的容器控件,性能往往略低一些,因为RelativeLayout主要在onMeasure和onLayout阶段会耗费更多时间。综上所述:LinearLayout易用,效率高,表达能力有限。RelativeLayout复杂,表达能力强,但是效率稍逊。所以当某一界面在使用LinearLayout并不会比RelativeLayout带来更多的控件数和控件层级时,我们要优先考虑LinearLayout。但是要根据实际情况来做一个取舍,在保证性能的同时尽量避免OverDraw。


    2.去掉window的默认背景
    当我们使用了Android自带的一些主题时,window会被默认添加一个纯色的背景,这个背景是被DecorView持有的。当我们的自定义布局时又添加了一张背景图或者设置背景色,那么DecorView的background此时对我们来说是无用的,但是它会产生一次Overdraw,带来绘制性能损耗。去掉window的背景可以在onCreate()中setContentView()之后调用
    getWindow().setBackgroundDrawable(null);

    或者在theme中添加
    android:windowbackground="null";


    3.去掉其他不必要的背景
    有时候为了方便会先给Layout设置一个整体的背景,再给子View设置背景,这里也会造成重叠,如果子View宽度mach_parent,可以看到完全覆盖了Layout的一部分,这里就可以通过分别设置背景来减少重绘。再比如如果采用的是selector的背景,将normal状态的color设置为“@android:color/transparent",也同样可以解决问题。这里只简单举两个例子,我们在开发过程中的一些习惯性思维定式会带来不经意的Overdraw,所以开发过程中我们为某个View或者ViewGroup设置背景的时候,先思考下是否真的有必要,或者思考下这个背景能不能分段设置在子View上,而不是图方便直接设置在根View上。


    4.ClipRect & QuickReject
    为了解决Overdraw的问题,Android系统会通过避免绘制那些完全不可见的组件来尽量减少消耗。但是不幸的是,对于那些过于复杂的自定义的View(通常重写了onDraw方法),Android系统无法检测在onDraw里面具体会执行什么操作,系统无法监控并自动优化,也就无法避免Overdraw了。但是我们可以通过canvas.clipRect()来帮助系统识别那些可见的区域。这个方法可以指定一块矩形区域,只有在这个区域内才会被绘制,其他的区域会被忽视。这个API可以很好的帮助那些有多组重叠组件的自定义View来控制显示的区域。同时clipRect方法还可以帮助节约CPU与GPU资源,在clipRect区域之外的绘制指令都不会被执行,那些部分内容在矩形区域内的组件,仍然会得到绘制。除了clipRect方法之外,我们还可以使用canvas.quickreject()来判断是否没和某个矩形相交,从而跳过那些非矩形区域内的绘制操作。
    clip方法详解


    5.使用ViewStub占位
    ViewStub是个什么东西?一句话总结:高效占位符。我们经常会遇到这样的情况,运行时动态根据条件来决定显示哪个View或布局。常用的做法是把View都写在上面,先把它们的可见性都设为View.GONE,然后在代码中动态的更改它的可见性。这样的做法的优点是逻辑简单而且控制起来比较灵活。但是它的缺点就是,耗费资源。虽然把View的初始可见View.GONE但是在Inflate布局的时候View仍然会被Inflate,也就是说仍然会创建对象,会被实例化,会被设置属性。也就是说,会耗费内存等资源。推荐的做法是使用android.view.ViewStub,ViewStub是一个轻量级的View,它一个看不见的,不占布局位置,占用资源非常小的控件。可以为ViewStub指定一个布局,在Inflate布局的时候,只有ViewStub会被初始化,然后当ViewStub被设置为可见的时候,或是调用了ViewStub.inflate()的时候,ViewStub所向的布局就会被Inflate和实例化,然后ViewStub的布局属性都会传给它所指向的布局。这样,就可以使用ViewStub来方便的在运行时,要还是不要显示某个布局。

      <ViewStub
             android:id="@+id/stub_view"
             android:inflatedId="@+id/panel_stub"
             android:layout="@layout/progress_overlay"
             android:layout_width="fill_parent"
             android:layout_height="wrap_content"
             android:layout_gravity="bottom" />
    
    

    当你想加载布局时,可以使用下面其中一种方法:

      //方法一
      ((ViewStub) findViewById(R.id.stub_view)).setVisibility(View.VISIBLE);
      //方法二
      View importPanel = ((ViewStub) findViewById(R.id.stub_view)).inflate();
    
    

    6.用Merge减少布局深度
    Merge标签有什么用呢?简单粗暴点回答:干掉一个view层级。Merge的作用很明显,但是也有一些使用条件的限制。有两种情况下我们可以使用Merge标签来做容器控件。第一种子视图不需要指定任何针对父视图的布局属性,就是说父容器仅仅是个容器,子视图只需要直接添加到父视图上用于显示就行。另外一种是假如需要在LinearLayout里面嵌入一个布局(或者视图),而恰恰这个布局(或者视图)的根节点也是LinearLayout,这样就多了一层没有用的嵌套,无疑这样只会拖慢程序速度。而这个时候如果我们使用merge根标签就可以避免那样的问题。另外Merge只能作为XML布局的根标签使用,当Inflate以<merge />开头的布局文件时,必须指定一个父ViewGroup,并且必须设定attachToRoot为true。
    使用HierarchyViewer检查布局层级


    7.善用draw9patch
    给ImageView加一个边框,你肯定遇到过这种需求,通常在ImageView后面设置一张背景图,露出边框便完美解决问题,此时这个ImageView,设置了两层drawable,底下一层仅仅是为了作为图片的边框而已。但是两层drawable的重叠区域去绘制了两次,导致overdraw。优化方案: 将背景drawable制作成draw9patch,并且将和前景重叠的部分设置为透明。由于Android的2D渲染器会优化draw9patch中的透明区域,从而优化了这次overdraw。 但是背景图片必须制作成draw9patch才行,因为Android 2D渲染器只对draw9patch有这个优化,否则,一张普通的Png,就算你把中间的部分设置成透明,也不会减少这次overdraw。


    8.慎用Alpha
    假如对一个View做Alpha转化,需要先将View绘制出来,然后做Alpha转化,最后将转换后的效果绘制在界面上。通俗点说,做Alpha转化就需要对当前View绘制两遍,可想而知,绘制效率会大打折扣,耗时会翻倍,所以Alpha还是慎用。如果一定做Alpha转化的话,可以采用缓存的方式。

       view.setLayerType(LAYER_TYPE_HARDWARE);
       doSmoeThing();
       view.setLayerType(LAYER_TYPE_NONE);
    
    

    通过setLayerType方式可以将当前界面缓存在GPU中,这样不需要每次绘制原始界面,但是GPU内存是相当宝贵的,所以用完要马上释放掉。


    9.避免“OverDesign”
    overdraw会给APP带来不好的体验,overdraw产生的原因无外乎:复杂的Layout层级,重叠的View,重叠的背景这几种。开发人员无节制的View堆砌,究其根本无非是产品无节制的需求设计。有道是“由俭入奢易,由奢入俭难",很多APP披着过度设计的华丽外衣,却忘了简单易用才是王道的本质,纷繁复杂的设计并不会给用户带来好的体验,反而会让用户有压迫感,产品本身也有可能因此变得卡顿。当然,一切抛开业务谈优化都是空中楼阁,这就需要产品设计也要有一个权衡,在复杂的业务逻辑与简单易用的界面展现中做一个平衡,而不是一味的OverDesign。

    作者:Rave_Tian
    转载地址:https://www.jianshu.com/p/2cc6d5842986
    來源:简书
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

    以下为补充

    1.为什么背景会造成过度绘制?

    View展示是通过onDraw方法实现的,看下onDraw的源码

    public void draw(Canvas canvas) {
            final int privateFlags = mPrivateFlags;
            final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
                    (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
            mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;
    
            /*
             * Draw traversal performs several drawing steps which must be executed
             * in the appropriate order:
             *
             *      1. Draw the background
             *      2. If necessary, save the canvas' layers to prepare for fading
             *      3. Draw view's content
             *      4. Draw children
             *      5. If necessary, draw the fading edges and restore layers
             *      6. Draw decorations (scrollbars for instance)
             */
    
            // Step 1, draw the background, if needed
            int saveCount;
    
            if (!dirtyOpaque) {
                drawBackground(canvas);
            }
    
            // skip step 2 & 5 if possible (common case)
            final int viewFlags = mViewFlags;
            boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
            boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
            ...
     }
    

    源码里的注释写的很清楚,第一步先Draw the background

    private void drawBackground(Canvas canvas) {
            final Drawable background = mBackground;
            if (background == null) {
                return;
            }
    
            setBackgroundBounds();
    
            // Attempt to use a display list if requested.
            if (canvas.isHardwareAccelerated() && mAttachInfo != null
                    && mAttachInfo.mHardwareRenderer != null) {
                mBackgroundRenderNode = getDrawableRenderNode(background, mBackgroundRenderNode);
    
                final RenderNode renderNode = mBackgroundRenderNode;
                if (renderNode != null && renderNode.isValid()) {
                    setBackgroundRenderNodeProperties(renderNode);
                    ((DisplayListCanvas) canvas).drawRenderNode(renderNode);
                    return;
                }
            }
            ...
    

    如果没有background,就不会在canvas上画背景,减少了一层绘制。

    2. ViewStub原理

    调用infalte或者ViewStub.setVisibility(View.VISIBLE);时,会调用inflate()方法

      public View inflate() {
             // 1、首先要求父控件是ViewGroup才可以
            final ViewParent viewParent = getParent();
          
            if (viewParent != null && viewParent instanceof ViewGroup) {
                // 其次要给mLayoutResource赋值,因为mLayoutResource就是要懒加载显示的界面对应的布局
                if (mLayoutResource != 0) {
                    final ViewGroup parent = (ViewGroup) viewParent;
                    final LayoutInflater factory;
                    if (mInflater != null) {
                        factory = mInflater;
                    } else {
                        factory = LayoutInflater.from(mContext);
                    }
                    // 2、这就是重点了,直接调用常见的LayoutInflater.from().inflate系列方法来初始化需要懒加载的View
                    final View view = factory.inflate(mLayoutResource, parent,
                            false);
    
                    if (mInflatedId != NO_ID) {
                        view.setId(mInflatedId);
                    }
                    // 从父视图中获取当前ViewStub在父视图中的位置  
                    final int index = parent.indexOfChild(this);
                    // 当前ViewStub也是个View仅仅只是用来占位,所以先把占位的ViewStub视图删除
                    parent.removeViewInLayout(this);
    
                    // 3 、此处获取的是ViewStub上面设置的参数
                    final ViewGroup.LayoutParams layoutParams = getLayoutParams();
                    if (layoutParams != null) {
                        parent.addView(view, index, layoutParams);
                    } else {
                        parent.addView(view, index);
                    }
                    // 目的是在复写的setVisibility方法中使用, 因为ViewStub.setVisibility操作的是被加载视图并非当前ViewStub视图  
                    mInflatedViewRef = new WeakReference<View>(view);
    
                    if (mInflateListener != null) {
                        mInflateListener.onInflate(this, view);
                    }
                    // 通过返回的这个View  我们就可以拿来各种findViewById 就能显示需要显示的View了
                    return view;
                } else {
                    throw new IllegalArgumentException("ViewStub must have a valid layoutResource");
                }
            } else {
                throw new IllegalStateException("ViewStub must have a non-null ViewGroup viewParent");
            }
        }
    

    从以上代码,可以看出来,在设置可见时候,才去用LayoutInflater去inflate layoutResource,这也是和普通View在布局里设置gone的区别,gone是已经inflate过加载到内存了,只是没有显示。

    另外,在Layout Inspector查看布局的时候,最开始ViewStub是不显示的

    public final class ViewStub extends View {
    
        public ViewStub(Context context) {
            initialize(context);
        }
         
         private void initialize(Context context) {
            mContext = context;
            setVisibility(GONE); // 初始化时把自己设置为隐藏
            setWillNotDraw(true);
        }
         
        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            setMeasuredDimension(0, 0); // 所有子视图都设置为宽高为0
        }
    
        @Override
        public void draw(Canvas canvas) { // 不对自身与子视图进行绘制
        }
    
        @Override
        protected void dispatchDraw(Canvas canvas) {
        }
    
        @Override
        public void setVisibility(int visibility) {
            if (mInflatedViewRef != null) {
                View view = mInflatedViewRef.get(); //弱引用,获取真正的view,而非此ViewStub
                if (view != null) {
                    view.setVisibility(visibility);
                } else {
                    throw new IllegalStateException("setVisibility called on un-referenced view");
                }
            } else {
                super.setVisibility(visibility);
                if (visibility == VISIBLE || visibility == INVISIBLE) {
                    inflate();
                }
            }
        }
    }
    

    可以看出ViewStub用尽所有办法让自己添加到视图树上是不显示ViewStub自身。

    ViewStub的原理简单描述是

    1. ViewStub是一个宽高均为0dp的View,会被添加到界面上,占位。
    2. 当调用infalte或者ViewStub.setVisibility(View.VISIBLE);时(两个都使用infalte方法逻辑),先从父视图上把当前ViewStub删除,再把加载的android:layotu视图添加上
    3. 把ViewStub layoutParams 添加到加载的android:layout视图上,而其根节点layoutParams 设置无效。
    4. ViewStub是指用来占位的视图,通过删除自己并添加android:layout视图达到懒加载效果

    3.merge原理

    XML布局最终会执行以下代码被添加到 Activity 的DecorView根布局上

    mLayoutInflater.inflate(layoutResID, mContentParent);
    
    public View inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot) {
            synchronized (mConstructorArgs) {
    
                final AttributeSet attrs = Xml.asAttributeSet(parser);
                Context lastContext = (Context)mConstructorArgs[0];
                mConstructorArgs[0] = mContext;
                View result = root;
    
                try {
                    // Look for the root node.
                    int type;
                    while ((type = parser.next()) != XmlPullParser.START_TAG &&
                            type != XmlPullParser.END_DOCUMENT) {
                        // Empty
                    }
    
                    if (type != XmlPullParser.START_TAG) {
                        throw new InflateException(parser.getPositionDescription()
                                + ": No start tag found!");
                    }
    
                    final String name = parser.getName();
    
                    if (TAG_MERGE.equals(name)) {
                        if (root == null || !attachToRoot) {
                            throw new InflateException("<merge /> can be used only with a valid "
                                    + "ViewGroup root and attachToRoot=true");
                        }
                        rInflate(parser, root, attrs, false, false);
                    } else {
                        // Temp is the root view that was found in the xml
                        final View temp = createViewFromTag(root, name, attrs, false);
    
                        ViewGroup.LayoutParams params = null;
    
                        if (root != null) {
                            // Create layout params that match root, if supplied
                            params = root.generateLayoutParams(attrs);
                            if (!attachToRoot) {
                                // Set the layout params for temp if we are not
                                // attaching. (If we are, we use addView, below)
                                temp.setLayoutParams(params);
                            }
                        }
    
                        // Inflate all children under temp
                        rInflate(parser, temp, attrs, true, true);
    
                        // We are supposed to attach all the views we found (int temp)
                        // to root. Do that now.
                        if (root != null && attachToRoot) {
                            root.addView(temp, params);
                        }
    
                        // Decide whether to return the root that was passed in or the
                        // top view found in xml.
                        if (root == null || !attachToRoot) {
                            result = temp;
                        }
                    }
    
                } catch (XmlPullParserException e) {
                    InflateException ex = new InflateException(e.getMessage());
                    ex.initCause(e);
                    throw ex;
                } catch (IOException e) {
                    InflateException ex = new InflateException(
                            parser.getPositionDescription()
                            + ": " + e.getMessage());
                    ex.initCause(e);
                    throw ex;
                } finally {
                    // Don't retain static reference on context.
                    mConstructorArgs[0] = lastContext;
                    mConstructorArgs[1] = null;
                }
    
                Trace.traceEnd(Trace.TRACE_TAG_VIEW);
    
                return result;
            }
        }
    

    可以看到,如果解析到时 <merge />标签

      //第二个参数是parent,将解析到的子view添加到这个parent上,可以看到,这里的parent参数是当前的root view,所以就少了一层布局
      rInflate(parser, root, attrs, false, false);
    

    如果不是<merge />标签

      //这里的parent参数是temp view,即当前从xml解析出来的ViewGroup
        rInflate(parser, temp, attrs, true, true);
    

    以下为xml解析流程图


    4.relativelayout和LinearLayout在实现效果同等情况下选择使用哪个?为什么?

    我们知道一个View要绘制到屏幕上,会经历onMeasure、onLayout、onDraw三个阶段,要探讨它们的性能问题,就是比较这三个阶段的执行时间的长短。

    将几个TextView垂直摆放在屏幕上,分别使用LinearLayout和RelativeLayout,然后使用Hierarchy Viewer进行观察。


    从结果看,两种实现方式中onLayout、onDraw的执行时间基本一致,onMeasure的执行时间LinearLayout比RelativeLayout要短很多。

    为什么会出现这种现象呢?这就需要从LinearLayout和RelativeLayout的源码入手分析了。

    查看onMeasure方法源码我们发现RelativeLayout会对子View做两次measure。这是由于RelativeLayout是基于相对位置的,而且子View会在横向和纵向两个方向上分布,因此,需要在横向和纵向分别进行一次measure过程。

    LinearLayout只进行横向或者纵向的measure,因此measure的时间要比RelativeLayout短,这也就印证了之前我们观察到的结果。但是,如果LinearLayout设置了weight属性,就有些不同了。如果使用weight属性,LinearLayout会避开设置过weight属性的view做一次measure,然后再对设置过weight属性的view做第二次measure。也就是说,设置了weight属性的LinearLayout的绘制效率比没有设置的要差。





    参考:https://blog.csdn.net/feiduclear_up/article/details/46732879
    参考:https://blog.csdn.net/goodlixueyong/article/details/51396803

    相关文章

      网友评论

          本文标题:Android布局优化(二),减少过度绘制

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