什么是内存抖动?
短时间内大量的对象被创建,导致可用内存不足,从而引起频繁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:
可以很直观地看到,内存忽高忽低呈锯齿状,原因是Java中用 "+" 拼接字符串实际上
是创建了StringBuilder对象进行拼接,看一下反编译后的字节码文件:
这里用的是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不就完事了吗。
内存抖动一定是锯齿状吗?
我们再看一个案例:
这是一个看起来相对平滑的内存走势图,但是也能看出内存一直在增加,而且有gc触发,我们截取一个时间段看看详细信息:
我们看到
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个不同颜色的小方条在不断改变颜色。
我们完全可以把Color.parseColor放在构造方法中调用,并且存放在一个int数组中,避免在onDraw方法中创建对象。这个View中可优化的地方还有很多,比如onDraw中创建多个Path对象和paint对象,Profiler内存分析表中还有多个char[]数组对象、StringBuilder对象、String[]数组对象等等,用同样的流程都可以一一排查解决掉,这里就不挨个分析了。
总结 有的时候内存抖动不一定有很明显的锯齿状,可能它的内存增长的很缓慢,并不是一下子就占满了内存,甚至有时候看起来是波浪状的也未必一定有内存抖动(可能是profiler的bug),所以检测内存抖动需要细心观察和分析才能定位解决问题。
内存抖动的几点建议
- 避免在循环内部创建新对象。
- 注意尽量减少在View 的onDraw 方法中创建对象,由于View的刷新频率可能会很高,onDraw方法很可能会被频繁调用。
- 对于能够复用的对象,同理可以使用对象池将它们缓存起来。