Android实践:如何高效加载Bitmap

一、BitmapFactory.Options简介
在Android开发中,加载图片过多、过大很容易引起OutOfMemoryError异常,即我们常见的内存溢出。因为Android对单个应用施加内存限制,默认分配的内存只有几M(具体视不同系统而定)。而载入的图片如果是JPG之类的压缩格式(JPG支持最高级别的压缩,不过该压缩是有损的),在内存中展开会占用大量的内存空间,也就容易形成内存溢出;

那么高效的加载Bitmap是很重要的事情。Bitmap在Android中指的是一张图片,可以是png格式也可以是jpg等常见的格式。BitmapFactory提供了如下四类方法,可分别用于从文件系统、资源、输入流以及字节数组中加载出一个Bitmap对象
  1.decodeFile;
  2.decodeResource;
  3.decodeStream;
  4.decodeByteArray;

如果高效的加载类图呢,其实核心就是BitmapFactory.Options来加载所需尺寸的图片。因为很多时间ImageView并没有图片原始尺寸那么大,把整个图片加载进来显然是没有必要的。我们可以使用BitmapFactory.Options从如下几种方式对图片进行采样压缩,降低内存的占有从而减少OOM的可能性:
  1. 降低图片加载到内存的分辨率(BitmapFactory.Options.inJustDecodeBounds/outWidth/outHeight/inSampleSize属性);
  2. 采用更节更节省内存的编码,如ARGB_4444(BitmapFactory.Options.inPreferredConfig属性);
  3. 采用缓存;
这里我们就从方法1和方法2进行处理,该方式需要了解BitmapFactory.Options,先介绍如下:
参数
说明
备注
inJustDecodeBounds 为true时,解码不会返回bitmap,只会返回bitmap的尺寸。 用于获取图片的尺寸,但有不想将其加载到内存中。
inSampleSize 当<1时,当做1处理;>1时会按照比例缩小bitmap的宽高,降低分辨率。 如with=100,height=100,inSampleSize=2,则返回width=50,height=50,像素50*50=250降为1/4;
inPreferredConfig 色彩模式,默认ARGB_8888,一个像素4bytes。如果对透明不做要求,采用RGB_565,一个像素2bytes;
ALPHA_8:每个像素1byte;
ARGB_444:每个像素2byte;
ARGB_8888:每个像素4byte;
RGB_565:每个像素2byte;
如果一个图片分辨率1024*768,采用ARGB_8888,占用空间为1024*768*4=3M,而采用ARGB_444内存就能减半1.5M;
inPremultiplied 和透明通道有关,默认true,返回的bitmap颜色通道上预先附加透明通道; 透明通道是计算机图形学术语,指的是“非彩色”通道,8位灰度通道,使用256级灰度来记录图像中的透明信息,定义透明、不透明和半透明。如32位存储的图片,8红+8绿+8蓝+8透明;
inDither 抖动解码,默认false,标识不采用抖动解码; Bitmap解码是根据它所记录的节点,按照一定的算法来补充两个节点之间的数据,可理解为补充像素点的颜色。一张颜色丰富的图用一个位数比较低的颜色模式解码的话,会感觉颜色不够用,颜色渐变区域有明显断裂带。因为一些丰富的颜色在位数较低的颜色模式下并没有,只能用相近的颜色补充,可能一大片没有,那么这大片都用一个颜色填充,就形成了断裂色带;如果采用抖动解码,就会在这些颜色上采用随机噪声色来填充,这样显示效果更好,色带不那么明显。如果不想有这些色带,就需要采用抖动解码;
inDensity 表示这个bitmap的像素密度; 对应DisplayMetrics.densityDpi,不是density
inTargetDensity 表示要被画出来时的目标像素密度;
inScreenDensity 标识实际设备的像素密度; inDensity,inTargetDensity,inScreenDensity这三个值的目的就是为了确定这个Bitmap的宽高和density。详细算法可以查看setDensityFromOptions()方法源码实现;
inScaled 设置这个bitmap是否可以被缩放,默认true;
inPurgeable/inInputShareable 一般一起使用,设置为true时,表示空间不够可以被释放,后者表示是否可以共享引用。Android5.0后被弃用;
outWidth/outHeight 表示bitmap的宽和高,一般和inJustDecodeBounds一起使用获取Bitmap的宽高,但不加载到内存中;  

二、BitmapFactory.Options实践
1.为了跟大家更好的体会和展示优化过程,首先我们先使用一个简单Demo,使用Gallery来展示几张图片来模拟OOM,代码如下:
BitmapActivity.java:

public class NextActivity extends AppCompatActivity {
    private int[] images = new int[]{R.drawable.p1, R.drawable.p2, R.drawable.p3};
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_next);
        Gallery gallery = (Gallery) findViewById(R.id.gallery);
        gallery.setAdapter(new GalleryAdapter());
    }
    class GalleryAdapter extends BaseAdapter {
        @Override
        public int getCount() {
            return images.length;
        }
        @Override
        public Object getItem(int position) {
            return images[position];
        }
        @Override
        public long getItemId(int position) {
            return images[position];
        }
        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            ImageView imageView = new ImageView(BitmapActivity.this);
            Bitmap bitmap = BitmapFactory.decodeResource(getResources(), images[position]);
            imageView.setImageBitmap(bitmap);
            return imageView;le = true;
        }
    }
}
2.运行后即OOM异常崩溃,错误日志输出如下:

再看看Monitor中,在两张图片加载图片过程中,内存两次迅速上升,达到200M后OOM崩溃;

3.接下来下面我们就使用BitmapFactory.Options来优化该OOM问题:

BitmapActivity.java
public class BitmapActivity extends AppCompatActivity {
    ... ...
    class GalleryAdapter extends BaseAdapter {
        ... ...
        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            ImageView imageView = new ImageView(BitmapActivity.this);

            BitmapFactory.Options options = new BitmapFactory.Options();
            //inJustDecodeBounds为true,不返回bitmap,只返回这个bitmap的尺寸
            options.inJustDecodeBounds = true;
            BitmapFactory.decodeResource(getResources(), images[position], options);

            //利用返回的原图片的宽高,我们就可以计算出缩放比inSampleSize,获取指定宽度为300像素,等长宽比的缩略图,减少图片的像素
            int toWidth = 300;
            int toHeight = options.outHeight * toWidth / options.outWidth;
            options.inSampleSize = options.outWidth / toWidth;
            options.outWidth = toWidth;
            options.outHeight = toHeight;

            //使用RGB_565减少图片大小
            options.inPreferredConfig = Bitmap.Config.RGB_565;
            //释放内存,共享引用(21版本后失效)
            options.inPurgeable = true;
            options.inInputShareable = true;
           
            //inJustDecodeBounds为false,返回bitmap
            options.inJustDecodeBounds = false;
            Bitmap bitmap = BitmapFactory.decodeResource(getResources(), images[position], options);
            imageView.setImageBitmap(bitmap);

            return imageView;
        }
    }
}
4.程序正常运行,Monitor监控内存减少至21M左右!!!


5.在实际的应用过过程中,inSampleSize计算并不会那么“理想 ”。比如ImagView的大小是100*100像素,而图片的原始大小是200*300呢?inSampleSize为2,则缩放后的图片大小为100*150像素,仍然是适合的;但是为3那么缩小后的图片大小就会小于ImageView的期望大小,这样图片就会拉伸从而导致模糊。下面我们就提供一种计算inSampleSize的计算方式,供大家参考:
public static int caculateInSampleSize(BitmapFactory.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 halfHeight = height / 2;
          final int halfWidth = width / 2; 
          while(((halfHeight / inSampleSize) >= reqHeight) && ((halfWidth / inSampleSize) >=reqWidth)){
               inSampleSize *= 2;
          }
     }
     return inSampleSize;
}
6.代码库
QProject:https://github.com/Pengchengxiang/QProject  分支:feature/bitmapoption

新技术,新未来!欢迎大家关注 “1024工场”微信服务号 ,时刻关注我们的最新的技术讯息! (甭客气!尽情的扫描或者长按!)

你可能感兴趣的:(Android实践,Android实战)