Android加载大分辨率图片到手机内存中的实例方法

如何加载一一个Bitmap,Bitmap 在Android
中指的是一张图片,可以是png格式也可以是jpg 等其他常见的图片格式。那么如何加载
个图片呢? BitmapFactory类提供了四类方法: decodeFile,decodeResource.decodeStream
和decodeByteAmay,分别用于支持从文件系统、资源、输入流以及字节数组中加载出- 一个
Bitmap对象,其中decodeFile和decodeResource 又间接调用了decodeStream 方法,这四类
方法最终是在Android的底层实现的,对应着BitmapFactory类的几个native 方法。
如何高效地加载Bitmap 呢? 其实核心思想也很简单,那就是采用BitmapFactory.
Options来加载所需尺寸的图片。这里假设通过1mageView来显示图片,很多时候ImageView
并没有图片的原始尺寸那么大,这个时候把整个图片加我进来后再设给ImageView,这显
然是没必要的,因为ImageView 并没有办法显示原始的图片。通过BitmapFactoryOptions
就可以按- -定的采样率来加载缩小后的图片,将缩小后的图片在ImageView中显示,这样
就会降低内存占用从面在一-定程度上避免OOM.提高了Bitmap 加载时的性能。
BitmapFactory 提供的加载图片的四类方法都支持BitmapFactory.Options 参数,通过它们就
可以很方便地对-一一个图片进行采样缩放。
通过BitmapFactory.Options 来缩放图片,主要是用到了它的inSampleSize 参数,即采
样率。当inSampleSize为1时,采样后的图片大小为图片的原始大小; 当inSampleSize大
F 1时,比如为2.那么采样后的图片其宽高均为原图大小的1/2,面像素數为原图的1/4,
其占有的内存大小也为原图的1/4.拿一-张1024X1024像素的图片来说,假定采用ARGB888
格式存储,那么它占有的内存为1024x1024x4,即4MB,如果inSampleSize 为2.那么采
样后的图片其内存占用只有512x512x4,即1MB.可以发现采样率inSampleSize必须是大
F 1的整数图片才会有缩小的效果,并且采样率同时作用于宽高,这将导致缩放后的图片
大小以采样率的2 次方形式递减,即缩放比例为1/ (inSampleSize 的2 次方),比如
inSampleSize 为4,那么缩放比例就是1/16。有一种特殊情况,那就是当inSampleSize 小于
1时,其作用相当于1.即无缩放效果。另外最新的官方文档中指出,inSampleSize的取值
应该总是为2的指数,比如1.2.4.8.16.等等。如果外界传递给系统的inSampleSize
不为2的指数,那么系统会向下取整并选择- 一个最接近的2 的指数来代替,比如3,系统会选择2 来代替,但是经过验证发现这个结论并非在所有的Android版本上都成立,因此
把它当成- “个开发建议即可。
考虑以下实际的情况,比如ImageView 的大小是100X100像素,而图片的原始大小为
200x200,那么只需将采样率inSampleSize设为2即可。但是如果图片大小为200X300呢?
这个时候采样率还应该选择2.这样缩放后的图片大小为100X150 像素,仍然是适合
ImageView 的,如果采样率为3,那么缩放后的图片大小就会小于ImageView 所期望的大
小,这样图片就会被拉伸从而导致模糊。
通过采样率即可有效地加载图片,那么到底如何获取采样率呢? 获取采样率也很简单,
遵循如下流程:
(1) 将BitmapFactory.Options的inJustDecodeBounds参数设为true 并加载图片。
(2) 从BitmapFactory.Options 中取出图片的原始宽商信息,它们对1于outWidth 和
outHeight 参数。
(3) 根据采样率的规则并结合目标View的所需大小计算出采样率inSampleSize.
(4) 将BitmapFactoryOptions 的inJustDecodeBounds 参数设为false,然后 重新加载
图片。
经过上面4个步骤,加载出的图片就是最终缩放后的图片,当然也有可能不需要缩放。
这里说明- 下inJustDecodeBounds多数,当此参数设为true时,BitmapFactory只会解析图
片的原始宽/高信息,并不会去真正地加载图片,所以这个操作是轻量级的。另外需要往意
的是,这个时候BitmapFactory 获取的图片宽/高信息和图片的位置以及程序运行的设备有
关,比如同- 一张图片放在不同的drawable目录下或者程序运行在不同屏幕密度的设备上,
这都可能导致BitmapFactory 获取到不同的结果,之所以会出现这个现象,这和Android的
资源加载机制有关,相信读者平日里肯定有所体会,这里就不再详细说明了。

有些图片的分辨率比较高,把它直接加载到手机内存中之后,会导致堆内存溢出的问题,下面就讲解一下Android的堆内存以及如何在Android应用中加载一个高分辨率的图片的方法
还原堆内存溢出的错误
首先来还原一下堆内存溢出的错误。首先在SD卡上放一张照片,分辨率为(3776 X 2520),大小为3.88MB,是我自己用相机拍的一张照片。应用的布局很简单,一个Button一个ImageView,然后按照常规的方式,使用BitmapFactory加载一张照片并使用一个ImageView展示。

代码如下:

btn_loadimage.setOnClickListener(new View.OnClickListener() {

            @Override
            public void onClick(View v) {
                Bitmap bitmap=BitmapFactory.decodeFile("/sdcard/a.jpg");
                iv_bigimage.setImageBitmap(bitmap);
            }
}

当点击按钮后,程序会报错,查看日志为:

这里写图片描述

先来分析一下这个错误,首先dalvikvm(Android虚拟机)发现需要的内存38MB大于应用的堆内存24MB,这个时候尝试使用软加载的方式加载数据,我们知道当内存不足的时候dalvikvm会自动进行GC(Garbage Collection),大概清理了55k的空间出来,耗时203毫秒,但是内存还是不够,所以最后发生堆内存溢出的错误。

分析堆内存溢出

Android系统主要用于低能耗的移动设备,所以对内存的管理有很多限制,一个应用程序,Android系统缺省会为其分配最大16MB(某些机型是24MB)的空间作为堆内存空间,我这里使用的模拟器调试的,这个模拟器被设定为24MB,可以在Android Virtual Device Manager中查看到。

这里写图片描述

而这里的图片明明只有3.88MB,远远小于Android为应用分配的堆内存,而加载到内存中,为什么需要消耗大约38MB的内存呢?
我们都知道,图片是由一个一个点分布组成的(分辨率),通常加载这类数据都会在内存中创建一个二维数组,数组中的每一项代表一个点,而这个图片的分辨率是3776 * 2520,每一点又是由ARGB色组成,每个色素占4个Byte,所以这张图片加载到内存中需要消耗的内存为:
3776 * 2520 * 4byte = 38062080byte
大约需要38MB的内存才能正确加载这张图片,这就是上面错误描述需要38MB的内存空间,大小略有出入,因为图片还有一些Exif信息需要存储,会比仅靠分辨率计算要大一些。

如何加载大分辨率图片
有时候我们确实会需要加载一些大分辨率的图片,但是对于移动设备而言,哪怕加载能成功那么大的内存也是一种浪费(屏幕分辨率限制),所以就需要想办法把图片按照一定比率压缩,使分辨率降低,以至于又不需要耗费很大的堆内存空间,又可以最大的利用设备屏幕的分辨率来显示图片。这里就用到一个BitmapFactory.Options对象,下面来介绍它。
BitmapFactory.Options为BitmapFactory的一个内部类,它主要用于设定与存储BitmapFactory加载图片的一些信息。下面是Options中需要用到的属性:
inJustDecodeBounds:如果设置为true,将不把图片的像素数组加载到内存中,仅加载一些额外的数据到Options中。
outHeight:图片的高度。
outWidth:图片的宽度。
inSampleSize:如果设置,图片将依据此采样率进行加载,不能设置为小于1的数。例如设置为4,分辨率宽和高将为原来的1/4,这个时候整体所占内存将是原来的1/16。

示例Demo

下面通过一个简单的Demo来演示上面提到的内容,代码中注释比较清晰,这里就不再累述了。

代码如下:

package cn.bgxt.loadbigimg;

import android.os.Bundle;
import android.os.Environment;
import android.app.Activity;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.BitmapFactory.Options;
import android.view.Menu;
import android.view.View;
import android.view.WindowManager;
import android.widget.Button;
import android.widget.ImageView;

public class MainActivity extends Activity {
    private Button btn_loadimage;
    private ImageView iv_bigimage;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        btn_loadimage = (Button) findViewById(R.id.btn_loadimage);
        iv_bigimage = (ImageView) findViewById(R.id.iv_bigimage);

        btn_loadimage.setOnClickListener(new View.OnClickListener() {

            @Override
            public void onClick(View v) {
                // Bitmap bitmap=BitmapFactory.decodeFile("/sdcard/a.jpg");
                // iv_bigimage.setImageBitmap(bitmap);

                BitmapFactory.Options opts = new Options();
                // 不读取像素数组到内存中,仅读取图片的信息
                opts.inJustDecodeBounds = true;
                BitmapFactory.decodeFile("/sdcard/a.jpg", opts);
                // 从Options中获取图片的分辨率
                int imageHeight = opts.outHeight;
                int imageWidth = opts.outWidth;

                // 获取Android屏幕的服务
                WindowManager wm = (WindowManager) getSystemService(WINDOW_SERVICE);
                // 获取屏幕的分辨率,getHeight()、getWidth已经被废弃掉了
                // 应该使用getSize(),但是这里为了向下兼容所以依然使用它们
                int windowHeight = wm.getDefaultDisplay().getHeight();
                int windowWidth = wm.getDefaultDisplay().getWidth();

                // 计算采样率
                int scaleX = imageWidth / windowWidth;
                int scaleY = imageHeight / windowHeight;
                int scale = 1;
                // 采样率依照最大的方向为准
                if (scaleX > scaleY && scaleY >= 1) {
                    scale = scaleX;
                }
                if (scaleX < scaleY && scaleX >= 1) {
                    scale = scaleY;
                }

                // false表示读取图片像素数组到内存中,依照设定的采样率
                opts.inJustDecodeBounds = false;
                // 采样率
                opts.inSampleSize = scale;
                Bitmap bitmap = BitmapFactory.decodeFile("/sdcard/a.jpg", opts);
                iv_bigimage.setImageBitmap(bitmap);

            }
        });
    }
}

总结
这里讲解了如何加载一个大分辨率的图片到内存中并使用它。不过一般好一点的图片处理软件,都会有图片放大功能,如果仅做此处理,单纯的把处理后的图片放大,会影响显示效果,图片还原度不高。一般会重新获取放大区域的图片的分辨率像素数组,然后重新处理加载到内存中进行显示。

直接在ImageView显示类似于一张图片利用手指放大缩小大imageView大小,但真实大小并不会改变,所一如果我们的图片分辨率远大于手机分辨率,纯属浪费内存,还可能会造成内存泄露,所以我们需要在设置图片之前对将要加载的图片进行判断以进行相应的压缩,避免内存泄露,节省app有限的内存

你可能感兴趣的:(Android基础)