OMM常规问题侦察:如何避免大图片 OOM

程序申请内存过大,虚拟机无法满足我们,然后自杀了。这个现象通常出现在大图片的APP开发,或者需要用到很多图片的时候。通俗来讲就是我们的APP需要申请一块内存来存放图片的时候,系统认为我们的程序需要的内存过大,及时系统有充分的内存,比如1G,但是系统也不会分配给我们的APP,故而抛出OOM异常,程序没有捕捉异常,故而弹窗崩溃了

但是在面试中具体该如何回答呢?具体往下看:

图片内存占用

在Android开发中,加载图片很容易的碰到OOM。

何为OOM? OOM, 即out of memory,这里面的memory指的是堆内存。因为在Android中,应用程序都是有一定的内存限制的。当内存占用过高就容易出现OOM(OutOfMemory)异常。

我们可以通过下面的代码看出每个应用程序最高可用内存是多少:

arduino 复制代码
int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024 /1024);
Log.d("TAG", "Max memory is " + maxMemory + "MB");

(测试了一下,朵唯L5 Pro手机,可用内存是256M;)

每台手机的可用内存是不同的,而图片会对内存影响非常大。

举个例子,当我们加载一张分辨率为1960*1200,色彩模式为ARGB_8888,图片大小为4M的图片时,其所占用的内存空间并不是图片的大小,而是根据图片的分辨率来计算的。

这张图片需要的内存为:

yaml 复制代码
1960*1200*4(bit) / 1024 / 1024 = 8.79MB

我们先分析一下,例如:一张图片就占用了将近9M,如果是一组图片呢?可以想象的到,如果我们在加载图片的时候使用原图加载的话,程序分分钟就死掉了。

因此,在展示高分辨率图片的时候,最好先将图片进行压缩。压缩后的图片大小应该和用来展示它的控件大小相近,毕竟在一个很小的ImageView上显示一张超大的图片对显示的效果也会有什么好处,但却会占用相当多宝贵的内存,而且在性能上还可能会带来负面影响。因此对于高分辨率的图片,在不影响用户体验的情况下,尽量去做压缩。

如何处理大图

影响一张图片占用内存的有两方面的因素,(1)压缩尺寸 (2)色彩模式;

色彩模式

从色彩模式的角度,对于一个ARGB_8888的图片,在满足业务需求的情况下,比如并不要求这张图片特别清晰逼真,那么可以在压缩尺寸之前,可以同时将option的值重新设置一下,比如设置为RGB_565。

ini 复制代码
options.inPreferredConfig = Bitmap.Config.RGB_565;

ARGB_8888,表示一个像素占8+8+8+8=32位=4字节,而RGB_565,表示一个像素占用5+6+5=16位=2字节。这样设置之后图片内存占用会减半。

尺寸压缩

第一步、预先获取图片的原始尺寸

为了避免OOM异常,我们在解析每张图片之前,最好都能预先检查一下图片的大小,以保证这些图片不会过大,占用太多内存。

BitmapFactory这个类提供了多个解析方法(decodeByteArray, decodeFile, decodeResource等)用于创建Bitmap对象,我们应该根据图片的来源选择合适的方法。

比如SD卡中的图片可以使用decodeFile方法,网络上的图片可以使用decodeStream方法,资源文件中的图片可以使用decodeResource方法。但这些方法会尝试为已经构建的bitmap分配内存,这时就会很容易导致OOM出现。

为此每一种解析方法都提供了一个可选的BitmapFactory.Options参数,当将这个参数的inJustDecodeBounds属性设置为true时,我们再去解析图片,这是解析方法返回的bitmap对象为null, 但是BitmapFactory.Options的outHeight/outWidth/outMimeType等属性都会被赋值。

这个技巧让我们可以获取到图片的长宽值和MIME类型等信息,同时解析方法不会给bitmap分配内存。

如下代码所示:

ini 复制代码
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(getResources(), R.id.my_image, options);
int imageHeight = options.outHeight;
int imageWidth = options.outWidth;
String imageType = options.outMimeType;

第二步、压缩图片尺寸

现在图片的大小已经知道了,我们就可以决定是把整张图片加载到内存中还是加载一个压缩版的图片到内存中。

比如,你的ImageView只有128x96像素的大小,只是为了显示一张缩略图,这时候把一张1024x768像素的图片完全加载到内存中显然是不值得的。

那我们怎样才能对图片进行压缩呢?通过设置BitmapFactory.Options中inSampleSize的值就可以实现。

比如我们有一张2048x1536像素的图片,将inSampleSize的值设置为4,就可以把这张图片压缩成512x384像素。

原本加载这张图片需要占用12M的内存,压缩后就只需要占用0.75M了(假设图片是ARGB_8888类型,即每个像素点占用4个字节)。

下面的方法可以根据传入的宽和高,计算出合适的inSampleSize值:

arduino 复制代码
public static int calculateInSampleSize(
    BitmapFactory.Options options, int reqWidth, int reqHeight) {
    // 源图片的高度和宽度
    final int height = options.outHeight;
    final int width = options.outWidth; 
    int inSampleSize = 1;
    if (height > reqHeight || width > reqWidth) { 
        // 计算出实际宽高和目标宽高的比率
       final int heightRatio = Math.round((float) height / (float) reqHeight);
       final int widthRatio = Math.round((float) width / (float) reqWidth); 
       // 选择宽和高中最小的比率作为inSampleSize的值,这样可以保证最终图片的宽和高一定都会大于等于目标的宽和高。 
      inSampleSize = heightRatio < widthRatio ? heightRatio : widthRatio; 
    }
    return inSampleSize;
}

使用这个方法,首先你要将BitmapFactory.Options的inJustDecodeBounds属性设置为true,解析一次图片。然后将BitmapFactory.Options连同期望的宽度和高度一起传递到到calculateInSampleSize方法中,就可以得到合适的inSampleSize值了。之后再解析一次图片,使用新获取到的inSampleSize值,并把inJustDecodeBounds设置为false,就可以得到压缩后的图片了。

ini 复制代码
public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId, int reqWidth, int reqHeight) {
    // 第一次解析将inJustDecodeBounds设置为true,来获取图片大小
    final BitmapFactory.Options options = new BitmapFactory.Options();
    options.inJustDecodeBounds = true;
    BitmapFactory.decodeResource(res, resId, options);
    // 调用上面定义的方法计算inSampleSize值
    options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
    // 使用获取到的inSampleSize值再次解析图片
    options.inJustDecodeBounds = false;
    return BitmapFactory.decodeResource(res, resId, options);
}

下面的代码非常简单地将任意一张图片压缩成100x100的缩略图,并在ImageView上展示。

less 复制代码
mImageView.setImageBitmap(
    decodeSampledBitmapFromResource(getResources(), R.id.myimage, 100, 100));

第三步、加载大量图片 内存缓存技术

一张图片的问题解决了,有多张图片要加载怎么办?比如使用ListView, GridView 或者 ViewPager 这样的组件来加载图片,屏幕上显示的图片可以通过滑动屏幕等事件不断地增加,就有可能导致OOM。

为了保证内存的使用始终维持在一个合理的范围,通常会把被移除屏幕的图片进行回收处理。此时垃圾回收器也会认为你不再持有这些图片的引用,从而对这些图片进行GC操作。

但是这个带来另外一个问题。当某些图片滑出屏幕并回收之后,用户有可能又把它重新滑入屏幕,这时就需要把原来加载过的图片重新加载一遍。这样性能肯定是瓶颈。

使用内存缓存技术可以很好的解决这个问题,它可以让组件快速地重新加载和处理图片。

下面我们就来看一看如何使用内存缓存技术来对图片进行缓存,从而让你的应用程序在加载很多图片的时候可以提高响应速度和流畅性。

内存缓存技术对那些大量占用应用程序宝贵内存的图片提供了快速访问的方法。其中最核心的类是LruCache (此类在android-support-v4的包中提供) 。

LruCache 非常适合用来缓存图片,它的主要算法原理是把最近使用的对象用强引用存储在 LinkedHashMap 中,并且把最近最少使用的对象在缓存值达到预设定值之前从内存中移除。

之前非常流行的内存缓存技术使用的是软引用或弱引用 (SoftReference or WeakReference)。但是现在已经不再推荐使用这种方式了,因为从 Android 2.3 (API Level 9)开始,垃圾回收器会更倾向于回收持有软引用或弱引用的对象,这让软引用和弱引用变得不再可靠。另外,Android 3.0 (API Level 11)中,图片的数据会存储在本地的内存当中,因而无法用一种可预见的方式将其释放,这就有潜在的风险造成应用程序的内存溢出并崩溃。

为了能够选择一个合适的缓存大小给LruCache,需要考虑以下几个因素,例如:

复制代码
(1)应用程序最大可用内存是多少?
(2)设备屏幕上一次最多能显示多少张图片?有多少图片需要进行预加载,因为有可能很快也会显示在屏幕上?
(3)设备的屏幕大小和分辨率分别是多少?
    一个超高分辨率的设备比起一个较低分辨率的设备,在持有相同数量图片的时候,需要更大的缓存空间。
(4)图片的尺寸和大小,还有每张图片会占据多少内存空间。
(5)图片被访问的频率有多高?会不会有一些图片的访问频率比其它图片要高?
     如果有的话,你也许应该让一些图片常驻在内存当中,或者使用多个LruCache 对象来区分不同组的图片。
(6)平衡数量和质量。有时候,存储多个低像素的图片,同时在后台去开线程加载高像素的图片会更有效。

缓存大小不是固定的,应当具体情况具体分析。但是不能太小,也不能太大;因为如果缓存太小,有可能造成图片频繁地被释放和重新加载;而缓存太大,则有可能会引起OOM。 下面是一个使用 LruCache 来缓存图片的例子:

typescript 复制代码
private LruCache<String, Bitmap> mMemoryCache;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
    // 获取到可用内存的最大值,使用内存超出这个值会引起OutOfMemory异常。
    // LruCache通过构造函数传入缓存值,以KB为单位。
    int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
    // 使用最大可用内存值的1/8作为缓存的大小。
    int cacheSize = maxMemory / 8;
    mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
        @Override
        protected int sizeOf(String key, Bitmap bitmap) {
            // 重写此方法来衡量每张图片的大小,默认返回图片数量。
            return bitmap.getByteCount() / 1024;
    }
};
}
​
public void addBitmapToMemoryCache(String key, Bitmap bitmap) {
    if (getBitmapFromMemCache(key) == null) {
        mMemoryCache.put(key, bitmap);
    }
}
​
public Bitmap getBitmapFromMemCache(String key) {
    return mMemoryCache.get(key);
}

在这个例子当中,使用了系统分配给应用程序的八分之一内存来作为缓存大小。在中高配置的手机当中,这大概会有4兆(32/8)的缓存空间。

一个全屏幕的 GridView使用4张 800x480分辨率的图片来填充,则大概会占用1.5兆的空间(800x480x4)。因此,这个缓存大小可以存储2.5页的图片。

当向 ImageView 中加载一张图片时,首先会在 LruCache 的缓存中进行检查。

如果找到了相应的键值,则会立刻更新ImageView ,否则开启一个后台线程来加载这张图片。

ini 复制代码
public void loadBitmap(int resId, ImageView imageView) {
    final String imageKey = String.valueOf(resId);
    final Bitmap bitmap = getBitmapFromMemCache(imageKey);
    if (bitmap != null) {
        imageView.setImageBitmap(bitmap);
    } else {
        imageView.setImageResource(R.drawable.image_placeholder);
        BitmapWorkerTask task = new BitmapWorkerTask(imageView);
        task.execute(resId);
    }
}

BitmapWorkerTask 还要把新加载的图片的键值对放到缓存中。

scala 复制代码
class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
    // 在后台加载图片。
    @Override
    protected Bitmap doInBackground(Integer... params) {
        final Bitmap bitmap = decodeSampledBitmapFromResource(
                    getResources(), params[0], 100, 100);
        addBitmapToMemoryCache(String.valueOf(params[0]), bitmap);
        return bitmap;
    }
}

以上方法,是程序加载超大图片和大量图片的基本优化方法,也是开源框架如Universal-Image-Loader等的基本原理。掌握了这个,再去看一些图片加载框架的源码,应该就很轻松了。

今日分享到此结束,对你有帮助的话,点个赞再走呗,下期更精彩~
关注公众号:Android老皮

解锁 《Android十大板块文档》 ,让学习更贴近未来实战。已形成PDF版

内容如下

1.Android车载应用开发系统学习指南(附项目实战)
2.Android Framework学习指南,助力成为系统级开发高手
3.2023最新Android中高级面试题汇总+解析,告别零offer
4.企业级Android音视频开发学习路线+项目实战(附源码)
5.Android Jetpack从入门到精通,构建高质量UI界面
6.Flutter技术解析与实战,跨平台首要之选
7.Kotlin从入门到实战,全方面提升架构基础
8.高级Android插件化与组件化(含实战教程和源码)
9.Android 性能优化实战+360°全方面性能调优
10.Android零基础入门到精通,高手进阶之路

敲代码不易,关注一下吧。ღ( ´・ᴗ・` ) 🤔

相关推荐
xiangxiongfly9151 小时前
Android 圆形和圆角矩形总结
android·圆形·圆角·imageview
幻雨様7 小时前
UE5多人MOBA+GAS 45、制作冲刺技能
android·ue5
Jerry说前后端8 小时前
Android 数据可视化开发:从技术选型到性能优化
android·信息可视化·性能优化
Meteors.9 小时前
Android约束布局(ConstraintLayout)常用属性
android
alexhilton10 小时前
玩转Shader之学会如何变形画布
android·kotlin·android jetpack
whysqwhw14 小时前
安卓图片性能优化技巧
android
风往哪边走14 小时前
自定义底部筛选弹框
android
Yyyy48215 小时前
MyCAT基础概念
android
Android轮子哥15 小时前
尝试解决 Android 适配最后一公里
android
雨白16 小时前
OkHttp 源码解析:enqueue 非同步流程与 Dispatcher 调度
android