理解Android应用内存限制与高效加载大图片

时间:2022-01-11 01:29:06

谷歌对android系统的每个app做了内存限制,不同版本的android系统,不同的设备对每个app的内存限制可能有所不同,从早期的16M ,32M到现在的256M,384M...虽然内存增大了,但是不代表就不会出现OOM(OutOfMemory)异常,这个异常大家都懂,比如加载一些分辨率很大的图像就可能超出内存限制,所以我们在加载大图片时,还是要小心处理。

下面通过以下代码获得在Nexus_5X 5.0设备上,一个app的可用内存大小

ActivityManager activityManager = (ActivityManager)getSystemService(Context.ACTIVITY_SERVICE);
int memoryClass = activityManager.getMemoryClass();
Log.d("memoryinfo","memoryClass="+memoryClass);
log:
D/memoryinfo: memoryClass=384

 在Android3.0(Honeycomb) 有了 “largeHeap” 选项后,可以在app内存本身限定的大小内,调整到一个最大值

可以这么理解吧,在没有“largeHeap”最大内存之前,app的内存最大只能384M,超过这个值,就会出现OOM(OutOfMemory)异常,现在有“largeHeap” 这个概念,就多了一个最大值的概念,比如这个最大值512M,现在如果你在工程的AndroidManifest.xml中添加了android:largeHeap="true",表示该应用最大内存可以调整512M了,超过了512M才会出现OOM(OutOfMemory)异常。

通过以下代码获取在Nexus_5X 5.0设备上,一个app的最大可用内存大小

int largeMemoryClass = activityManager.getLargeMemoryClass();
Log.d("memoryinfo","largeMemoryClass="+largeMemoryClass);

log:

D/memoryinfo: largeMemoryClass=384

发现该设备两个最大值相等.不是所有设备都一样的

获取是否设置了largeHeap,用以下代码:

AndroidManifest.xml中添加

<application
android:largeHeap="true"

Log.d("memoryinfo","isLargeHeap="+isLargeHeap(this));

private  boolean isLargeHeap(Context context) {
return (context.getApplicationInfo().flags & ApplicationInfo.FLAG_LARGE_HEAP) != 0;
}

log:

D/memoryinfo: isLargeHeap=true

既然现在知道在这个设备上一个app的内存最大为384M,那么就来测试一把。

现在有一张片大小为35M左右的图片

理解Android应用内存限制与高效加载大图片

看如下代码:

public void click(View view){
Log.d("BitmapFactory","click");
BitmapFactory.Options options = new BitmapFactory.Options();
for(int i=0;i<5;i++){
Log.d("BitmapFactory","i="+i);

Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.image, options);
int bytes = bitmap.getAllocationByteCount();//Returns the size of the allocated memory used to store this bitmap's pixels.
Log.d("BitmapFactory","bytes="+bytes);
list.add(bitmap);
}

现在点击button,就添加该图片添加到集合中,先设置了添加5次,结果程序崩溃了,看log:

D/BitmapFactory: click
D/BitmapFactory: i=0
I/art: Alloc partial concurrent mark sweep GC freed 405(25KB) AllocSpace objects, 1(255MB) LOS objects, 40% free, 1755KB/2MB, paused 101us total 10.729ms
D/BitmapFactory: bytes=267845760
D/BitmapFactory: i=1
I/art: Forcing collection of SoftReferences for 255MB allocation
E/art: Throwing OutOfMemoryError "Failed to allocate a 267845772 byte allocation with 4194304 free bytes and 127MB until OOM"
D/skia: --- allocation failed for scaled bitmap
D/AndroidRuntime: Shutting down VM
E/AndroidRuntime: FATAL EXCEPTION: main
Process: cj.com.bitmapfactory, PID: 4180
java.lang.IllegalStateException: Could not execute method for android:onClick
at android.support.v7.app.AppCompatViewInflater$DeclaredOnClickListener.onClick(AppCompatViewInflater.java:293)
at android.view.View.performClick(View.java:4756)
at android.view.View$PerformClick.run(View.java:19749)
at android.os.Handler.handleCallback(Handler.java:739)
at android.os.Handler.dispatchMessage(Handler.java:95)
at android.os.Looper.loop(Looper.java:135)
at android.app.ActivityThread.main(ActivityThread.java:5221)
at java.lang.reflect.Method.invoke(Native Method)
at java.lang.reflect.Method.invoke(Method.java:372)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:899)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:694)
Caused by: java.lang.reflect.InvocationTargetException
at java.lang.reflect.Method.invoke(Native Method)
at java.lang.reflect.Method.invoke(Method.java:372)
at android.support.v7.app.AppCompatViewInflater$DeclaredOnClickListener.onClick(AppCompatViewInflater.java:288)
at android.view.View.performClick(View.java:4756) 
at android.view.View$PerformClick.run(View.java:19749) 
at android.os.Handler.handleCallback(Handler.java:739) 
at android.os.Handler.dispatchMessage(Handler.java:95) 
at android.os.Looper.loop(Looper.java:135) 
at android.app.ActivityThread.main(ActivityThread.java:5221) 
at java.lang.reflect.Method.invoke(Native Method) 
at java.lang.reflect.Method.invoke(Method.java:372) 
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:899) 
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:694) 
Caused by: java.lang.OutOfMemoryError: Failed to allocate a 267845772 byte allocation with 4194304 free bytes and 127MB until OOM
at dalvik.system.VMRuntime.newNonMovableArray(Native Method)
at android.graphics.BitmapFactory.nativeDecodeAsset(Native Method)
at android.graphics.BitmapFactory.decodeStream(BitmapFactory.java:609)
at android.graphics.BitmapFactory.decodeResourceStream(BitmapFactory.java:444)
at android.graphics.BitmapFactory.decodeResource(BitmapFactory.java:467)
at cj.com.bitmapfactory.MainActivity$override.click(MainActivity.java:31)
at cj.com.bitmapfactory.MainActivity$override.access$dispatch(MainActivity.java)
at cj.com.bitmapfactory.MainActivity.click(MainActivity.java:0)
at java.lang.reflect.Method.invoke(Native Method) 
at java.lang.reflect.Method.invoke(Method.java:372) 
at android.support.v7.app.AppCompatViewInflater$DeclaredOnClickListener.onClick(AppCompatViewInflater.java:288) 
at android.view.View.performClick(View.java:4756) 
at android.view.View$PerformClick.run(View.java:19749) 
at android.os.Handler.handleCallback(Handler.java:739) 
at android.os.Handler.dispatchMessage(Handler.java:95) 
at android.os.Looper.loop(Looper.java:135) 
at android.app.ActivityThread.main(ActivityThread.java:5221) 
at java.lang.reflect.Method.invoke(Native Method) 
at java.lang.reflect.Method.invoke(Method.java:372) 
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:899) 
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:694) 
I/Process: Sending signal. PID: 4180 SIG: 9

没错出现OOM异常,通过log发现应该是在第二次添加图片的时候发生了内存溢出。

int bytes = bitmap.getAllocationByteCount();
这个方法是获取存储该张图片开辟的内存大小

一共267845760字节,也就是255.43762207M左右,这就是为什么添加第二张的时候就出现内存溢出了,两张加起来就大于384M了。

但是是不是很奇怪,这张图片本身就35M左右啊,怎么应用给开辟了255M左右的内存呢??

Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.image, options);
答案在这个方法里。

解码资源文件获取的位图经过了缩放。缩放的依据是根据设备屏幕的密度来的,当前该设备的密度是:420DPI

理解Android应用内存限制与高效加载大图片

放大的倍数就是420/160,160就默认的标准密度,这样以来图片的宽高都放大了420/160倍,所以最终图片的大小差不多就是34.9×(420/160)×(420/160)结果大小就差不多250M了

可见虽然内存大小有348M,但是在加载大图片时,也很容易出现OOM异常,所以需要我们在解码图片资源的时候要对大的图片进行缩小。

下面就接着讲一下高效加载大图片的API

官方文档:

https://developer.android.com/training/displaying-bitmaps/index.html

https://developer.android.com/training/displaying-bitmaps/load-bitmap.html

这里就来缩小上边那张35M的大图片:

代码如下:

 public void click(View view){
Log.d("BitmapFactory","click");
Bitmap bitmap = decodeSampledBitmapFromResource(getResources(), R.drawable.image, 100, 100);
int byteCount = bitmap.getAllocationByteCount();
Log.d("BitmapFactory","byteCount="+byteCount);
}


还是去解码那张大图片,只不过现在我有要求了,要求经过处理的图片的宽高都是100,然后再打印一下程序为该图片分配的内存大小

原图的宽高:

理解Android应用内存限制与高效加载大图片

很大吧


private Bitmap decodeSampledBitmapFromResource(Resources res , int resId, int targetWidth, int tartgetHegiht){
// First decode with inJustDecodeBounds=true to check dimensions
final BitmapFactory.Options options = new BitmapFactory.Options();
/**
* If set to true, the decoder will return null (no bitmap), but
* the out... fields will still be set, allowing the caller to query
* the bitmap without having to allocate the memory for its pixels.
*/
options.inJustDecodeBounds = true;
Bitmap bitmap = BitmapFactory.decodeResource(res, resId, options);
Log.d("BitmapFactory",bitmap+"");

// Calculate inSampleSize
options.inSampleSize = calculateInSampleSize(options, targetWidth, tartgetHegiht);

// Decode bitmap with inSampleSize set
options.inJustDecodeBounds = false;

Bitmap bitmap2 = BitmapFactory.decodeResource(res, resId, options);
Log.d("BitmapFactory",bitmap2+"");
Log.d("BitmapFactory","bitmap2 height ="+bitmap2.getHeight()+" width=="+bitmap2.getWidth());
return bitmap2;
}

解码图片资源还是用BitmapFactory这个工具

该工具介绍

https://developer.android.com/reference/android/graphics/BitmapFactory.html

BitmapFactory结合这个BitmapFactory.Options来处理图片,首先是获取原始图片的大小,只要设置

options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(res, resId, options);暂时不会分配内存,只是查看图片信息,所以返回的位图为null。
然后通图片原始大小和期待的大小,算出一下缩小的比例:
private int calculateInSampleSize(
BitmapFactory.Options options, int reqWidth, int reqHeight) {
// Raw height and width of image
final int height = options.outHeight;
final int width = options.outWidth;
String imageType = options.outMimeType;

Log.d("BitmapFactory","Raw height ="+height+" width=="+width);
Log.d("BitmapFactory","options.outMimeType ="+imageType);
/**
* If set to a value > 1, requests the decoder to subsample the original
* image, returning a smaller image to save memory. The sample size is
* the number of pixels in either dimension that correspond to a single
* pixel in the decoded bitmap. For example, inSampleSize == 4 returns
* an image that is 1/4 the width/height of the original, and 1/16 the
* number of pixels. Any value <= 1 is treated the same as 1. Note: the
* decoder will try to fulfill this request, but the resulting bitmap
* may have different dimensions that precisely what has been requested.
* Also, powers of 2 are often faster/easier for the decoder to honor.
*/
int inSampleSize = 1;

if (height > reqHeight || width > reqWidth) {
final int halfHeight = height / 2;
final int halfWidth = width / 2;

// Calculate the largest inSampleSize value that is a power of 2 and keeps both
// height and width larger than the requested height and width.
while ((halfHeight / inSampleSize) >= reqHeight
&& (halfWidth / inSampleSize) >= reqWidth) {
inSampleSize *= 2;
}
}
Log.d("BitmapFactory","inSampleSize ="+inSampleSize);
return inSampleSize;
}

缩小的倍数就是2的多少次方,比如1,2,4,8...,
比如期待100*100,原始是480*800,那就是以小的值480为标准,缩小到接近100,但大于100,算出缩小倍数是4,缩小后的大小就是120*200了。
将缩小的比例值的赋值给
options.inSampleSize
然后再设置:
// Decode bitmap with inSampleSize set
options.inJustDecodeBounds = false;
重新解码图片资源,最好获取的位图就是缩小的了
看一下log:
D/BitmapFactory: click
D/BitmapFactory: null
D/BitmapFactory: Raw height =4160 width==2336
D/BitmapFactory: options.outMimeType =image/jpeg
D/BitmapFactory: inSampleSize =16
D/BitmapFactory: android.graphics.Bitmap@2c6cbd5d
D/BitmapFactory: bitmap2 height =683 width==383
D/BitmapFactory: byteCount=1046356

图片的宽高都缩放了16倍,咦,不对呀 4160/16 不等于683呀,这还是上面提到的,处理的图片还要根据屏幕密度(dpi)来适配设备,所以又放大了420/160倍,可以算一下就知道了。最终获取的图片的大小是1046356字节,大概1M左右。
 
因此为了防止OOM异常,有时候对图片的的缩小还是有必要的,图片的显示还要结合UI控件来。