程序请求内存过大,虚拟机无法满足咱们,然后自杀了。这个现象一般出现在大图片的APP开发,或许需求用到许多图片的时分。通俗来讲便是咱们的APP需求请求一块内存来存放图片的时分,体系以为咱们的程序需求的内存过大,及时体系有充分的内存,比方1G,可是体系也不会分配给咱们的APP,故而抛出OOM反常,程序没有捕捉反常,故而弹窗崩溃了

可是在面试中具体该怎么回答呢?具体往下看:

图片内存占用

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

何为OOM? OOM, 即out of memory,这里面的memory指的是堆内存。由于在Android中,使用程序都是有一定的内存限制的。当内存占用过高就简单出现OOM(OutOfMemory)反常。

咱们能够经过下面的代码看出每个使用程序最高可用内存是多少:

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

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

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

举个例子,当咱们加载一张分辨率为1960*1200,颜色形式为ARGB_8888,图片巨细为4M的图片时,其所占用的内存空间并不是图片的巨细,而是依据图片的分辨率来核算的。

这张图片需求的内存为:

1960*1200*4(bit) / 1024 / 1024 = 8.79MB

咱们先剖析一下,例如:一张图片就占用了将近9M,假如是一组图片呢?能够幻想的到,假如咱们在加载图片的时分运用原图加载的话,程序分分钟就死掉了。

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

怎么处理大图

影响一张图片占用内存的有两方面的要素,(1)紧缩尺度 (2)颜色形式;

颜色形式

从颜色形式的角度,关于一个ARGB_8888的图片,在满足业务需求的情况下,比方并不要求这张图片特别明晰逼真,那么能够在紧缩尺度之前,能够一起将option的值从头设置一下,比方设置为RGB_565。

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分配内存。

如下代码所示:

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只有128×96像素的巨细,仅仅为了显现一张缩略图,这时分把一张1024×768像素的图片完全加载到内存中显然是不值得的。

那咱们怎样才能对图片进行紧缩呢?经过设置BitmapFactory.Options中inSampleSize的值就能够实现。

比方咱们有一张2048×1536像素的图片,将inSampleSize的值设置为4,就能够把这张图片紧缩成512×384像素。

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

下面的办法能够依据传入的宽和高,核算出适宜的inSampleSize值:

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,就能够得到紧缩后的图片了。

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);
}

下面的代码十分简单地将任意一张图片紧缩成100×100的缩略图,并在ImageView上展现。

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 来缓存图片的例子:

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张 800×480分辨率的图片来填充,则大概会占用1.5兆的空间(800x480x4)。因而,这个缓存巨细能够存储2.5页的图片。

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

假如找到了相应的键值,则会马上更新ImageView ,否则敞开一个后台线程来加载这张图片。

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 还要把新加载的图片的键值对放到缓存中。

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零根底入门到精通,高手进阶之路

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