内存优化——内存抖动

什么是内存抖动?

短时间内大量的对象被创建,导致可用内存不足,从而引起频繁gc回收对象,这种已用内存忽高忽低的现象就叫内存抖动。由于gc的过程会 “stop the world” 停止其他的一切工作,gc太频繁无疑会造成界面卡顿,而且gc回收后可能会产生内存碎片,如果这时其他线程需要申请大块内存还有可能发生OOM,所以内存抖动的情况必须要避免。

什么情况会出现内存抖动呢?

我们来看一段代码:

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        String result = addStr();
    }

    private String addStr() {
        String result = "";
        for (int i = 0; i < 100000; i++) {
            result += "result";
        }
        return result;
    }
}

这是很常见的字符串拼接操作,运行一下看看Android studio自带的内存检测工具Profiler:


启动Profiler.png
内存抖动.png

可以很直观地看到,内存忽高忽低呈锯齿状,原因是Java中用 "+" 拼接字符串实际上
是创建了StringBuilder对象进行拼接,看一下反编译后的字节码文件:


字节码.png

这里用的是Android studio的反编译插件ASM Bytecode Viewer,也可以在对应目录下用javap命令查看;字节码中很明显用了StringBuilder的append方法拼接字符串,所以总结下来就是我们在for循环中每拼接一次字符串就new 出一个新的StringBuilder对象,随着循环继续就会创建大量新对象,内存很快就被用完,gc不得不出面回收内存,然后就出现对象创建-销毁-创建-销毁...的过程,内存抖动由此而来。
这个问题解决也很简单:

    private String addStr() {
        StringBuilder builder = new StringBuilder();
        for (int i = 0; i < 100000; i++) {
            builder.append("result");
        }
        return builder.toString();
    }

直接用一个StringBuilder不就完事了吗。

内存抖动一定是锯齿状吗?

我们再看一个案例:


不明显的内存抖动.png

这是一个看起来相对平滑的内存走势图,但是也能看出内存一直在增加,而且有gc触发,我们截取一个时间段看看详细信息:


内存分析.png

我们看到
1、堆内存中在这个时间段有444个String类型的对象,很可能有频繁创建对象的嫌疑,选中这个条目,右上角出现详细的String对象列表。

2、选中其中一个String对象,右下角显示出这个对象所在的具体的类、方法、行数,我们点击进入代码 IOSStyleLoadingView1 --> onDraw --> 115行:

public class IOSStyleLoadingView1 extends View {

    ......

    String color[] = new String[]{
            "#a5a5a5",
            "#b7b7b7",
            "#c0c0c0",
            "#c9c9c9",
            "#d2d2d2",
            "#dbdbdb",
            "#e4e4e4",
            "#e4e4e4"
    };
    ......
    public IOSStyleLoadingView1(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        this.context = context;
        radius = UIKits.dip2Px(context, 9);
        insideRadius = UIKits.dip2Px(context, 5);
    }

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

        Paint paint = new Paint();
        paint.setAntiAlias(true);
        paint.setStrokeWidth(UIKits.dip2Px(context, 2));
        paint.setStyle(Paint.Style.STROKE);
        paint.setStrokeCap(Paint.Cap.ROUND);

        Path p0 = new Path();
        paint.setColor(Color.parseColor(color[0]));
        p0.moveTo(northwestXStart, northwestYStart);
        p0.lineTo(northwestXEnd, northwestYEnd);
        canvas.drawPath(p0, paint);

        Path p1 = new Path();
        paint.setColor(Color.parseColor(color[1]));
        p1.moveTo(northXStart, northYStart);
        p1.lineTo(northXEnd, northYEnd);
        canvas.drawPath(p1, paint);

        Path p2 = new Path();
        paint.setColor(Color.parseColor(color[2]));
        p2.moveTo(notheastXStart, notheastYStart);
        p2.lineTo(notheastXEnd, notheastYEnd);
        canvas.drawPath(p2, paint);

        Path p3 = new Path();
        paint.setColor(Color.parseColor(color[3]));
        p3.moveTo(eastXStart, eastYStart);
        p3.lineTo(eastXEnd, eastYEnd);
        canvas.drawPath(p3, paint);

        Path p4 = new Path();
        // 这里是我们要找的115行
        paint.setColor(Color.parseColor(color[4]));
        p4.moveTo(southeastXStart, southeastYStart);
        p4.lineTo(southeastXEnd, southeastYEnd);
        canvas.drawPath(p4, paint);

        Path p5 = new Path();
        paint.setColor(Color.parseColor(color[5]));
        p5.moveTo(southXStart, southYStart);
        p5.lineTo(southXEnd, southYEnd);
        canvas.drawPath(p5, paint);

        Path p6 = new Path();
        paint.setColor(Color.parseColor(color[6]));
        p6.moveTo(southwestXStart, southwestYStart);
        p6.lineTo(southwestXEnd, southwestYEnd);
        canvas.drawPath(p6, paint);

        Path p7 = new Path();
        paint.setColor(Color.parseColor(color[7]));
        p7.moveTo(westXStart, westYStart);
        p7.lineTo(westXEnd, westYEnd);
        canvas.drawPath(p7, paint);

    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    ......
    }

    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        startAnimation();
    }

    private ValueAnimator valueAnimator;

    public void startAnimation() {
        valueAnimator = ValueAnimator.ofInt(7, 0);
        valueAnimator.setDuration(400);
        valueAnimator.setRepeatCount(ValueAnimator.INFINITE);
        valueAnimator.setInterpolator(new LinearInterpolator());
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                if ((int) animation.getAnimatedValue() != currentColor) {
                    String b[] = new String[color.length];//移动后的数组
                    for (int c = 0, size = color.length - 1; c < size; c++) {
                        b[c + 1] = color[c];
                    }
                    b[0] = color[color.length - 1];
                    color = b;
                    invalidate();
                    currentColor = (int) animation.getAnimatedValue();
                }
            }
        });
        valueAnimator.start();
    }
}

这是一个自定义View,115行是
paint.setColor(Color.parseColor(color[4]));
这句代码没有创建String对象啊,点击Color的parseColor方法看看:

    public static int parseColor(@Size(min=1) String colorString) {
        if (colorString.charAt(0) == '#') {
            // Use a long to avoid rollovers on #ffXXXXXX
            // 这里调用了 String类的substring方法
            long color = Long.parseLong(colorString.substring(1), 16);
            if (colorString.length() == 7) {
                // Set the alpha value
                color |= 0x00000000ff000000;
            } else if (colorString.length() != 9) {
                throw new IllegalArgumentException("Unknown color");
            }
            return (int)color;
        } else {
            Integer color = sColorNameMap.get(colorString.toLowerCase(Locale.ROOT));
            if (color != null) {
                return color;
            }
        }
        throw new IllegalArgumentException("Unknown color");
    }

原来这里调用了String类的substring方法,我们都知道String类是不可变类,每一个字符串对应一个String对象,每次调用substring方法截取字符串就要新建一个String对象来接收截取后的值,所以我们选中的String对象就来自这里。
回头看看onDraw方法中有8处都调用了Color.parseColor这个方法,我们再看一下下面的startAnimation方法,startAnimation里面执行了一个动画,这个动画每400毫秒重复一次,并且在onAnimationUpdate回调中调用了invalidate()方法,那就不奇怪为什么会有那么多String对象了,因为invalidate()会调用onDraw方法来更新视图。

    /**
     * Invalidate the whole view. If the view is visible,
     * {@link #onDraw(android.graphics.Canvas)} will be called at some point in
     * the future.    onDraw会在未来的某个时间点被调用
     * 

* This must be called from a UI thread. To call from a non-UI thread, call * {@link #postInvalidate()}. */ public void invalidate() { invalidate(true); }

这个View就是一个旋转的进度条动画,8个不同颜色的小方条在不断改变颜色。


进度条.png

我们完全可以把Color.parseColor放在构造方法中调用,并且存放在一个int数组中,避免在onDraw方法中创建对象。这个View中可优化的地方还有很多,比如onDraw中创建多个Path对象和paint对象,Profiler内存分析表中还有多个char[]数组对象、StringBuilder对象、String[]数组对象等等,用同样的流程都可以一一排查解决掉,这里就不挨个分析了。

总结 有的时候内存抖动不一定有很明显的锯齿状,可能它的内存增长的很缓慢,并不是一下子就占满了内存,甚至有时候看起来是波浪状的也未必一定有内存抖动(可能是profiler的bug),所以检测内存抖动需要细心观察和分析才能定位解决问题。

内存抖动的几点建议

  • 避免在循环内部创建新对象。
  • 注意尽量减少在View 的onDraw 方法中创建对象,由于View的刷新频率可能会很高,onDraw方法很可能会被频繁调用。
  • 对于能够复用的对象,同理可以使用对象池将它们缓存起来。

你可能感兴趣的:(内存优化——内存抖动)