Quantcast
Channel: CSDN博客移动开发推荐文章
Viewing all articles
Browse latest Browse all 5930

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

$
0
0

谷歌对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左右的图片


看如下代码:

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


放大的倍数就是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,然后再打印一下程序为该图片分配的内存大小

原图的宽高:


很大吧


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控件来。


作者:hehe26 发表于2016/10/18 11:48:36 原文链接
阅读:40 评论:0 查看评论

Viewing all articles
Browse latest Browse all 5930

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>