oom问题瞎JB扯(记一次线上OOM问题排查)

oom问题瞎JB扯(记一次线上OOM问题排查)

三月 28, 2019

前言

OOM问题是每个Android开发者不得不面对的话题,在IOS系统上,每个应用理论上可以申请到的内存最多接近于系统的内存,而Android系统为了支持多应用限定了每个应用进程可以在JAVA层申请的最大堆内存,一旦应用申请的内存超过了这个阈值,系统就会抛出OOM(out of memory)异常。OOM问题是非常令人抓狂的问题,因为它不像其他Java层Crash那样有清晰的堆栈可以追踪,开发者可以看到报错的仅仅是压垮内存阈的最后一根稻草,不见得是引起OOM问题的原因;有些甚至仅仅就抛出一个异常,没有任何堆栈可以追踪,因此让排查OOM问题变得相对难度较大。

分析

OOM问题归根溯源就是由于进程内存申请过多,虚拟机在经过一次Full GC之后发现仍然无法腾出足够多的连续内存空间就会抛出异常。因此就可以分析一下内存暴增的原因,我觉得主要有两个原因导致:

  • 进程有内存泄漏,导致应用在运行过程中对象没有被及时释放内存随着时间增长不断增加最终超出阈值而崩溃。
  • 进程内存申请过多,存在大量的对象或者有内存占用量很大的对象(在Android中典型的就是Bitmap,绝大多数的OOM问题也是由于它引起)。

实际情况中,大部分APP或多或少应该都有些许内存泄漏问题,只不过没有极端操作情况(例如大量循环)一般不会引起内存暴增,因此大部分还是由于Bitmap引起的。在Android8.0之前,Bitmap存储的像素数据mBuffer变量是在java层,在8.0之后google将这部分移到了native层,因此在8.0之前更容易因Bitmap导致OOM问题。

排查

首先可以看一下OOM发生之前的log是否有相关的有用信息,然后现在很多线上监控平台都有用户操作轨迹,我们可以模拟用户的操作步骤进行尝试复现(OOM问题更容易在低端机上出现,可以拿老机器进行测试),就算没有复现出也没关系,我们可以通过studio 的profiler dump一下内存情况,然后分析当时场景的内存情况,找到内存占用大户,再定位代码位置。

实践

前一段时间项目的OOM问题飙升,严重影响了低端机用户的使用体验,在bugly上看到大部分OOM问题用户的操作轨迹都是在直播页面反复进出,因为基本可以确定问题出现在直播页,首先怀疑直播页有内存泄漏导致activity没被回收,在接入了leakCanary之后发现是直播页有部分handler的延时任务中强引用了view导致的。然后激动地把这个修复掉再操作然而还是浮现了= =。没办法继续往下找,进行了一顿操作之后用profiler dump了一下内存,首先分类选为Arrange by Package,定位到直播activity看了一下只有一个实例,好的验证了之前修复的结果,然后分类选为Arrange by class,看到如下:

排在第一位的FinalizerReference对象貌似对它没什么办法,但是下面的byte[]和Bitmap是很明显是Bitmap的锅了,byte[]数据存放的都是Bitmap的像素数据,看到第一行最大的一个居然有14m左右!!然后follow了一下它的持有对象:

工具有个很好用的功能,切到Bitmap Preview可以看到:

这不就是直播页的封面模糊图吗!?可是当时明明告诉过后端这个图返回一个320*420的图就行了啊,为什么会飙到14m呢?看了一下byte[]的大小,14745600byte,14745600除以4再开平方等于1920,好熟悉的数字..不就是手机高度吗?直觉告诉我这不是个巧合,因为封面图就是全屏展示的,看了一下加载图片的代码:

1
ImageLoader.with(ContextHolder.get()).transform(BlurTransformation(10)).load(coverUrl).into(obscureIv)

很正常没什么毛病呀,难道glide把这个图撑到全屏了?带着这个疑问看了一下glide的代码,几个关键点如下:

ViewTarget类:

1
2
3
4
5
6
7
8
9
10
11
12
13
private int getTargetHeight() {
int verticalPadding = view.getPaddingTop() + view.getPaddingBottom();
LayoutParams layoutParams = view.getLayoutParams();
int layoutParamSize = layoutParams != null ? layoutParams.height : PENDING_SIZE;
return getTargetDimen(view.getHeight(), layoutParamSize, verticalPadding);
}

private int getTargetWidth() {
int horizontalPadding = view.getPaddingLeft() + view.getPaddingRight();
LayoutParams layoutParams = view.getLayoutParams();
int layoutParamSize = layoutParams != null ? layoutParams.width : PENDING_SIZE;
return getTargetDimen(view.getWidth(), layoutParamSize, horizontalPadding);
}

可见这个类提供的是view的宽高,流程太长就不一个一个贴了,最终决定transform出来的Bitmap的宽高的是CenterCrop类,相关代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
/**
* A potentially expensive operation to crop the given Bitmap so that it fills the given
* dimensions. This operation is significantly less expensive in terms of memory if a mutable
* Bitmap with the given dimensions is passed in as well.
*
* @param pool The BitmapPool to obtain a bitmap from.
* @param inBitmap The Bitmap to resize.
* @param width The width in pixels of the final Bitmap.
* @param height The height in pixels of the final Bitmap.
* @return The resized Bitmap (will be recycled if recycled is not null).
*/
public static Bitmap centerCrop(@NonNull BitmapPool pool, @NonNull Bitmap inBitmap, int width,
int height) {
if (inBitmap.getWidth() == width && inBitmap.getHeight() == height) {
return inBitmap;
}
// From ImageView/Bitmap.createScaledBitmap.
final float scale;
final float dx;
final float dy;
Matrix m = new Matrix();
if (inBitmap.getWidth() * height > width * inBitmap.getHeight()) {
scale = (float) height / (float) inBitmap.getHeight();
dx = (width - inBitmap.getWidth() * scale) * 0.5f;
dy = 0;
} else {
scale = (float) width / (float) inBitmap.getWidth();
dx = 0;
dy = (height - inBitmap.getHeight() * scale) * 0.5f;
}

m.setScale(scale, scale);
m.postTranslate((int) (dx + 0.5f), (int) (dy + 0.5f));

Bitmap result = pool.get(width, height, getNonNullConfig(inBitmap));
// We don't add or remove alpha, so keep the alpha setting of the Bitmap we were given.
TransformationUtils.setAlpha(inBitmap, result);

applyMatrix(inBitmap, result, m);
return result;
}

Bitmap是decode出来待transform的bitmap,而width和height就是在上面取出来的,可以看到对于scaletype是centercrop的imageview glide将图片的bitmap拉伸到了其加载到的view的宽高,因此就出现了一张图片内存占用14m的情况..
然后加上了override覆盖了size,再run一遍代码,这下内存直接少了100多m,破案结束..