性能优化相关的高频面试题

314 阅读10分钟

 --主要是今天看到鸿洋大神的微信公众号上今天的主题就“android性能优化的问题”,仔细一看

好多都记得含糊不清或者理解不够深入了。-_-||, 好记性不如烂笔头,总结一下,学习学习~

(虽然网上都有哈)

一、内存优化

1、为什么官方建议别在onDraw创建对象?

2、内存抖动为什么会导致程序卡顿与OOM?

3、内存泄漏产生的原因与排查方案?

4、GC是怎么回收对象的?怎么确定对象是否可被回收?

二、OOM问题与LeakCanary原理

1、OOM到底是如何产生的呢?

2、如何快速地解决OOM问题?

3、leakCanary是如何发现OOM的?

4、LeakCanary原理?

三、UI滑动卡顿问题

1、UI卡顿的原因有哪些?

2、MeasureSpec 的原理?

3、自定义View的measure时机是什么,为什么参数值时而是0,时而正确?

------------------------------------------------------------------------------------------------

1、为什么官方建议别在onDraw创建对象?

     1)每创建一个对象,就会有一块内存分配给它;每分配一块内存,程序的可用内存也就少一块;当程序被占用的内存达到一定临界程度,GC 也就是垃圾回收器(Garbage Collector)就会出动,来释放掉一部分不再被使用的内存。

     2)Android 里的 View.onDraw 方法在每次需要重绘的时候都会被调用,这就意味着,如果你在 onDraw 里写了创建对象的代码,在界面频繁刷新的时候,你就也会频繁创建出一大批只被使用一次的对象,这就会导致内存占用的迅速攀升;然后很快,可能就会触发 GC 的回收动作,也就是这些被你创建出来的对象被 GC 回收掉。垃圾内存太多了就被清理掉,这是 Java 的工作机制,这不是问题。问题在于,频繁创建这些对象会造成内存不断地攀升,在刚回收了之后又迅速涨起来,那么紧接着就是又一次的回收,对吧?这么往复下来,最终导致一种循环,一种在短时间内反复地发生内存增长和回收的循环。

 这种循环往复的状态就像是水波纹的颤动一样,它的专业称呼叫做 Memory Churn,Android 的官方文档里把它翻译做了内存抖动。

2、内存抖动为什么会导致程序卡顿与OOM?

CMS垃圾回收器(CMS是老年代垃圾收集器)老年代是标记-清除算法:不会移动存活的对象,会产生内存碎片。一块内存区域有很多内存可用,但却是不连续的,剩余内存空间中如果没有10个连续字节的内存,此时申请连续的10个字节的内存就会产生OOM。 比如申请bitmap时就很可能产生OOM

3、内存泄漏产生的原因与排查方案?

    1)、非静态内部类以及非静态匿名内部类持有对外部类的引用。 

    2)、Activity里的Handler: 

    3)、单例类传入context 

    4)、注册/反注册未成对使用引起的内存泄漏(addListener(this)) 

    5)、资源未关闭造成的内存泄漏 

    6)、集合对象没有及时清理引起的内存泄漏

4、GC是怎么回收对象的?怎么确定对象是否可被回收?

     1)、如何判断对象已“死”:

         1、引用计数法:给对象增加一个引用计数器,每当有一个地方引用它时,计数器就+1;当引用失效时,计数器就-1;任何时刻计数器为0的对象就是不能再被使用的,即对象已“死”。 但是!无法解决对象的循环引用问题。 

         2、可达性分析算法:通过一系列称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称为“引用链”,当一个对象到 GC Roots 没有任何的引用链相连时(从 GC Roots 到这个对象不可达)时,证明此对象不可用。 

    在Java语言中,可作为GC Roots的对象包含以下几种:

  1. 虚拟机栈(栈帧中的本地变量表)中引用的对象。
  2. 方法区中静态属性引用的对象
  3. 方法区中常量引用的对象
  4. 本地方法栈中(Native方法)引用的对象

  2)、Java内存回收方式:

      1、JVM内存结构由程序计数器、堆、栈、本地方法栈、方法区等部分组成。

             程序计数器:几乎不占有内存。用于取下一条执行的指令。

             栈:每个线程执行每个方法的时候都会在栈中申请一个栈帧,每个栈帧包括局部变量                 区和操作数栈,用于存放此次方法调用过程中的临时变量、参数和中间结果。

             本地方法栈:用于支持native方法的执行,存储了每个native方法调用的状态

             方法区: 存放了要加载的类信息、静态变量、final类型的常量、属性和方法信息。JVM用永久代(PermanetGeneration)来存放方法区,(在JDK的HotSpot虚拟机中,可以认为方法区就是永久代,但是在其他类型的虚拟机中,没有永久代的概念,有关信息可以看周志明的书)可通过-XX:PermSize和-XX:MaxPermSize来指定最小值和最大值。

             堆: 所有通过new创建的对象的内存都在堆中分配,其大小可以通过-Xmx和-Xms来控制。堆被划分为新生代和老年代。

5.OOM到底是如何产生的呢?

当JVM因为没有足够的内存来为对象分配空间并且垃圾回收器也已经没有空间可回收时,就会抛出这个error。

为什么会没有内存了呢?原因不外乎有两点:

1)分配的少了:比如虚拟机本身可使用的内存(一般通过启动时的VM参数指定)太少。

2)应用用的太多,并且用完没释放,浪费了。此时就会造成内存泄露或者内存溢出。

6.leakCanary是如何发现OOM的?

7.UI卡顿的原因?

 1、adapter嵌套adapter ,一个listView嵌套过深,导致UI渲染层次过深,耗时严重,导致视觉上可人为感知的卡顿,类似的还有XML层级过深,以及过度绘制

 2、主线程做了很多耗时操作,但是不足以导致anr

8、MeasureSpec原理:

简介:

测量规格(MeasureSpec) = 测量模式(mode) + 测量大小(size)

  • MeasureSpec 被封装在View类中的一个内部类里:MeasureSpec
  • MeasureSpec类 用1个变量封装了2个数据(size,mode)通过使用二进制,将测量模式(mode) & 测量大小(size)打包成一个int值来,并提供了打包 & 解包的方法

该措施的目的 = 减少对象内存分配

(这种方式很巧妙,用一个变量,来封装2个数据,之前都没怎么注意,工作中可以借鉴一下,很牛批)

其中,测量模式(Mode)的类型有3种:UNSPECIFIED、EXACTLY 和
AT_MOST。具体如下:

   1、ViewGroup 中measureChild 

    protected void measureChild(View child, int parentWidthMeasureSpec,
            int parentHeightMeasureSpec) {
        final LayoutParams lp = child.getLayoutParams();

        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom, lp.height);

        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

在这个方法中,先获取了子View的布局参数,然后通过getChildMeasureSpec方法分别得到子View的宽高测量规则,即childWidthMeasureSpec和childHeightMeasureSpec,最后调用子View的measure方法,至此测量过程就由父View传递到了子View.。MeasureSpec确定后就可以在onMeasure方法确定View的测量宽高了。

重点看下getChildMeasureSpec方法

    public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        int specMode = MeasureSpec.getMode(spec);  //返回父View的测量模式
        int specSize = MeasureSpec.getSize(spec);  //返回父View的测量大小

        int size = Math.max(0, specSize - padding);  //父View的测量大小 - 父View的padding占用的大小,剩余的即是子View可用的最大空间

        int resultSize = 0;
        int resultMode = 0;

        switch (specMode) {
        // Parent has imposed an exact size on us
        case MeasureSpec.EXACTLY:
            if (childDimension >= 0) {  //子View大小为具体数值的情况
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {   //子View大小为match_parent的情况
                // Child wants to be our size. So be it.
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {   //子View大小为wrap_content的情况
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // Parent has imposed a maximum size on us
        case MeasureSpec.AT_MOST: 
            if (childDimension >= 0) {
                // Child wants a specific size... so be it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size, but our size is not fixed.
                // Constrain child to not be bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // Parent asked to see how big we want to be
        case MeasureSpec.UNSPECIFIED:
            if (childDimension >= 0) {
                // Child wants a specific size... let him have it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size... find out how big it should
                // be
                resultSize = 0;
                resultMode = MeasureSpec.UNSPECIFIED;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size.... find out how
                // big it should be
                resultSize = 0;
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
        }
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }

此方法比较清晰,子View的MeasureSpec值根据子View的布局参数(LayoutParams)和父容器的MeasureSpec值计算得来的,具体计算逻辑封装在getChildMeasureSpec()里。如下图:

即:子view的大小由父viewMeasureSpec值 和 子viewLayoutParams属性 共同决定

 DecorView的MeasureSpec创建过程

普通View的MeasureSpec的创建过程阐述了怎样通过父View的MeasureSpec和子View的LayoutParams来确定子View的MeasureSpec。那顶级View,即DecorView的MeasureSpec创建过程又是怎样的呢?ViewRootImp的measureHierarchy方法中有如下代码:

    childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);
    childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);

接着来看getRootMeasureSpec方法

    private static int getRootMeasureSpec(int windowSize, int rootDimension) {
        int measureSpec;
        switch (rootDimension) {

        case ViewGroup.LayoutParams.MATCH_PARENT:
            // Window can't resize. Force root view to be windowSize.
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
            break;
        case ViewGroup.LayoutParams.WRAP_CONTENT:
            // Window can resize. Set max size for root view.
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
            break;
        default:
            // Window wants to be an exact size. Force root view to be that size.
            measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
            break;
        }
        return measureSpec;
    }

从上述源码,我们可以得出如下规则,具体根据它的LayoutParams来划分:

  • LayoutParams.MATCH_PARENT:精确模式 其大小就为屏幕的尺寸大小
  • ViewGroup.LayoutParams.WRAP_CONTENT:最大模式,大小不定,但是不能超过屏幕的大小
  • 具体数值(如40dp):精确模式,大小为LayoutParamas指定的大小。

-------------------------------------具体Measure过程--------------------------

1、测量由ViewRootImpl#performTraversals开始

在[由setContentView探究Activity加载流程]中,我们提到View三大工作流程是从ViewRootImpl#performTraversals方法开始的,其中performMeasure、performLayout、performDraw方法分别对应了View的测量、布局、绘制。如下:

    private void performTraversals() {
        ...
        int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
        int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);

        performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
        ...
        performLayout(lp, desiredWindowWidth, desiredWindowHeight);
        ...
        performDraw();
    }


    private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
        try {
            mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }
    }

可以看到,在performMeasure方法中调用了 mView.measure(childWidthMeasureSpec, childHeightMeasureSpec),这里的mView其实是DecorView,它并没有重写measure方法,因为View#measure方法被final修饰,不可被重写。因此我们看下View#measure方法。

    public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
          ······
          // measure ourselves, this should set the measured dimension flag back
                onMeasure(widthMeasureSpec, heightMeasureSpec);
          ······
    }

View#measure中又调用了onMeasure(widthMeasureSpec, heightMeasureSpec)方法。并且DecorView重写了onMeasure方法,在DecorView#onMeasure方法中主要是
进一步确定自己的widthMeasureSpecheightMeasureSpec,并调用super.onMeasure(widthMeasureSpec, heightMeasureSpec)FrameLayout#onMeasure方法。

2、ViewGroup 的Measure过程

ViewGroup是一个抽象类,它并没有重写onMeasure方法,具体的实现交由子类去处理,如LinearLayout、RelativeLayout、FrameLayout,这是因为不同ViewGroup的布局特性和实现细节各异,无法统一处理。在这里我们以FrameLayout为例分析ViewGroup的测量过程。

FrameLayout#onMeasure

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int count = getChildCount();//获取子View的数量
        
        final boolean measureMatchParentChildren =
                MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY ||
                MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;
        mMatchParentChildren.clear();

        int maxHeight = 0;
        int maxWidth = 0;
        int childState = 0;

        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            if (mMeasureAllChildren || child.getVisibility() != GONE) {
                measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                maxWidth = Math.max(maxWidth,
                        child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
                maxHeight = Math.max(maxHeight,
                        child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
                childState = combineMeasuredStates(childState, child.getMeasuredState());
                if (measureMatchParentChildren) {
                    if (lp.width == LayoutParams.MATCH_PARENT ||
                            lp.height == LayoutParams.MATCH_PARENT) {
                        mMatchParentChildren.add(child);
                    }
                }
            }
        }

        // Account for padding too
        maxWidth += getPaddingLeftWithForeground() + getPaddingRightWithForeground();
        maxHeight += getPaddingTopWithForeground() + getPaddingBottomWithForeground();

        // Check against our minimum height and width
        maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
        maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());

        // Check against our foreground's minimum height and width
        final Drawable drawable = getForeground();
        if (drawable != null) {
            maxHeight = Math.max(maxHeight, drawable.getMinimumHeight());
            maxWidth = Math.max(maxWidth, drawable.getMinimumWidth());
        }

        setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
                resolveSizeAndState(maxHeight, heightMeasureSpec,
                        childState << MEASURED_HEIGHT_STATE_SHIFT));

        count = mMatchParentChildren.size();
        if (count > 1) {
            for (int i = 0; i < count; i++) {
                final View child = mMatchParentChildren.get(i);

                final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
                int childWidthMeasureSpec;
                int childHeightMeasureSpec;
                
                if (lp.width == LayoutParams.MATCH_PARENT) {
                    childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(getMeasuredWidth() -
                            getPaddingLeftWithForeground() - getPaddingRightWithForeground() -
                            lp.leftMargin - lp.rightMargin,
                            MeasureSpec.EXACTLY);
                } else {
                    childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
                            getPaddingLeftWithForeground() + getPaddingRightWithForeground() +
                            lp.leftMargin + lp.rightMargin,
                            lp.width);
                }
                
                if (lp.height == LayoutParams.MATCH_PARENT) {
                    childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(getMeasuredHeight() -
                            getPaddingTopWithForeground() - getPaddingBottomWithForeground() -
                            lp.topMargin - lp.bottomMargin,
                            MeasureSpec.EXACTLY);
                } else {
                    childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,
                            getPaddingTopWithForeground() + getPaddingBottomWithForeground() +
                            lp.topMargin + lp.bottomMargin,
                            lp.height);
                }

                child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
            }
        }
    }

View的Measure过程

    /**
     * <p>
     * The actual measurement work of a view is performed in
     * {@link #onMeasure(int, int)}, called by this method. Therefore, only
     * {@link #onMeasure(int, int)} can and must be overridden by subclasses.
     * </p>
     */
    public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
          ······
          // measure ourselves, this should set the measured dimension flag back
                onMeasure(widthMeasureSpec, heightMeasureSpec);
          ······
    }

可以看到此方法是final的,不可以被重写,并且注释中也表明实际的测量工作是在onMeasure方法中进行的,所以我们直接看onMeasure方法即可。

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }

setMeasureDimension方法其实是用来存储View最终的测量大小的。这个方法我们稍后分析,先来看下getDefaultSize这个方法。

    public static int getDefaultSize(int size, int measureSpec) {
        int result = size;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);

        switch (specMode) {
        case MeasureSpec.UNSPECIFIED:
            result = size;
            break;
        case MeasureSpec.AT_MOST:
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
        }
        return result;
    }

单一View measure 流程如下:

借鉴参考:

blog.csdn.net/yearningsee…

blog.csdn.net/carson\_ho/…