Android OOM 排查与解决——图片加载优化
1、OOM 引起与表现
在 Android 这种移动设备上,如果代码没有处理好,很容易引发内存持续占用与泄漏,导致 OOM(OutOfMemoryError)
异常,进而导致 App 程序 Crash 挂掉。
在 Android 开发中,一个典型的 OOM 异常如下:
OOM 异常一旦碰上了这类错误,我们往往需要去排查内存了。导致 OOM 的一些情况比较常见,大多数情况下,大家可能遇到的都是同一种情况:
- Activity 泄漏导致;
- 层次庞大复杂的 View 视图导致;
- 大量图片持续占用导致;
- 其他资源持续未释放导致。
2、Android Studio 查看内存占用
在 Android Studio 里面,我们可以在 Monitors
窗口中,实时对 App 内存进行监控,我们可以看出 App 的 HeapSize
、已经使用的内存大小
、剩余内存大小
以及峰值变化
。有了这些信息,我们可以在某个页面打开和关闭时进行监控,从而对比该页面占用内存变化,可以很方便的定位问题。
在这张图中,如果已使用的内存大小(Allocated
)接近到 HeapSize
的大小,App 将会处于非常危险的状态中,很有可能下一个操作就会直接导致 OOM,通过 Android Studio,我们可以防患于未然,在 Debug 阶段进行预防。
3、adb 查看内存占用
adb
工具也是一个非常有用的工具,我们可以通过它来查看 App 内存占用。
3.1、查看 JVM 的 HeapSize
等参数
通过命令 adb shell getprop dalvik.vm.heapsize
可以直接查看 Dalvik 虚拟机为 App 规定的最大 HeapSize
:
一般来说,App 可达到的最大 HeapSize 为 dalvik.vm.heapgrowthlimit
所规定的大小。但是如果我们在 AndroidManifest.xml
中为 Application 添加 android:largeHeap="true"
属性,App 可达到的最大 HeapSize 则被调整为 dalvik.vm.heapsize
规定的值。
虽然添加 android:largeHeap="true"
属性将大大降低 OOM 的概率,但除非万不得已的情况下,否则不要使用该属性。出现 OOM 后,我们首先应该排查整个 App,找出内存瓶颈予以解决。
3.2、查看 App 内存占用
通过命令 adb shell dumpsys meminfo [package_name]
可以查看 App 所占用内存:
通过这个命令,App 所占资源情况一目了然,甚至我们可以看到整个 App 中 View 个数、Activity 个数——这对于排查 Activity 泄漏和优化 View 层级也是非常有帮助的。
4、图片加载导致 OOM
而在一个 App 中,图片处理不恰当往往是 OOM 错误出现的元凶——因为 App 中所有图片动辄占用几十 M 的内存。如果我们能优先着手排查这一块,将会对 App 的内存优化带来 最直接最明显
的改观。而图片的不恰当处理操作一般有如下一些:
- 直接加载
超大尺寸
图片; - 图片加载后
未及时释放
; - 在页面中,同时加载
非常多
的图片;
4.1、超大尺寸图片处理
现在的手机摄像头像素比较高,摄制出来的照片尺寸非常大,比如在一款还算老旧的手机上面,拍摄的图片尺寸竟然达到了 2368 x 4224
!因为采用 jpeg 格式的缘故,这张图片在磁盘上才1.9M,但如果我们不加任何处理,按原尺寸加载到内存中,占用的内存将会非常可观。
所以,针对大图的加载,比较常用的方法是进行 DownSampling(向下采样)
,许多博客或技术站点对该方案有详细的描述,在此不再赘述,简单原理用代码表述如下:
public static int calcInSampleSize(
int width, int height, int requestWidth, int requestHeight) {
int inSampleSize = 1;
if (requestWidth <= 0 || requestHeight <= 0) {
return inSampleSize;
}
if (width > requestWidth || height > requestHeight) {
int widthRatio = Math.round((float) width / (float) requestWidth);
int heightRatio = Math.round((float) height / (float) requestHeight);
inSampleSize = Math.min(widthRatio, heightRatio);
}
return inSampleSize;
}
public static Bitmap decodeBitmapFromUri(
Context context, Uri uri, int requestWidth, int requestHeight) {
BitmapFactory.Options options = getResourceOptions(context, uri);
options.inSampleSize = calcInSampleSize(
options.outWidth, options.outHeight, requestWidth, requestHeight);
options.inJustDecodeBounds = false;
// ...
ContentResolver resolver = context.getContentResolver();
Bitmap bitmap = BitmapFactory.decodeStream(
resolver.openInputStream(uri), new Rect(), options);
// ...
return bitmap;
}
这样,如果我们要加载一张图片到 View 上,我们可以通过 view.getMeasuredWidth() 和 view.getMeasuredHeight()
得到 View 的宽和高,然后按这个大小进行采样,得到的 Bitmap 将会是尺寸适合的图片,不会占用额外内存,图片在 View 上展示出来质量也比较高。
4.2、及时释放图片
一般不要静态缓存图片,就算有缓存,也可以结合 LRU
机制来保证缓存图片的个数和占用内存。Android SDK 已经提供了 LruCache
类来实现 LRU 机制。
4.3、避免同时加载大量图片
避免同一时间加载大量的图片,也可以为我们的内存优化提供不小的收益。比如,在一个 ScrollView
中有非常多的 ImageView,这时候,占用的内存往往非常客观,因为就算一些 View 我们在屏幕视野里面看不到,它还是持续占用内存。我们可以通过 RecyclerView
或者 ListView
来予以替换,从而达到内存优化的效果。
在我的开发过程中,就遇到了这样一个例子。一个页面用 ScrollView 来布局,里面有 26 张左右的图片,这时候,整个 App 的内存占用长期达到了 90M
左右!一直徘徊在 OOM 边缘。在我把这个页面用 RecyclerView 替换掉 ScrollView 后,整个 App 内存竟然下降了 40M
之多!!!整个 App 变得非常顺滑。
5、采用开源库加载图片
现在已经有非常多的图片加载库供我们使用了,比较流行的有:Fresco
、Universal-Image-Loader
、Picasso
、Volley
等等。这些开源库一般来说,对内存的优化已经比较全面了,比我们自己手工管理内存来的好。所以,可以根据项目的实际情况灵活选用。
比如,我目前所使用的 Fresco
库,就可以灵活设定图片尺寸,避免加载大尺寸的图片(setResizeOptions
):
public static void displayImage(DraweeView draweeView, Uri uri) {
Size size = getAppropriateSize(draweeView);
ImageRequest request = ImageRequestBuilder
.newBuilderWithSource(uri)
.setResizeOptions(new ResizeOptions(size.mWidth, size.mHeight))
.setAutoRotateEnabled(true)
.build();
DraweeController controller = Fresco.newDraweeControllerBuilder()
.setUri(uri)
.setImageRequest(request)
.setOldController(draweeView.getController())
.build();
draweeView.setController(controller);
}
private static Size getAppropriateSize(View view) {
int width = view.getMeasuredWidth();
int height = view.getMeasuredHeight();
if (width <= 0 || height <= 0) {
width = view.getWidth();
height = view.getHeight();
}
Size size = MiscUtils.getScreenSize();
if (width <= 0 || height <= 0 || width > size.mWidth || height > size.mHeight) {
width = size.mWidth;
height = size.mHeight;
}
return new Size(width, height);
}
当然,我们还要在 ImagePipelineConfig
中开启 DownSampling
(setDownsampleEnabled(true)
):
public static void initFresco(Context context) {
ImagePipelineConfig config = ImagePipelineConfig
.newBuilder(context)
.setDownsampleEnabled(true)
.build();
Fresco.initialize(context, config);
}