Bitmap位图

1. Bitmap

在Android开发中,通过Bitmap对象可以很方便的对图片进行操作,但是bitmap特别耗内存,如果图片处理不当就会造成内存溢出(OOM),可通过压缩(重点在属性inSampleSize值的计算)、裁剪和及时回收(recycle方法)来避免内存泄漏。Bitmap在Android中指的是一张图片,图片类型可以是png、jpg等。

(1) Bitmap在内存中的大小
我们日常用像素形容图片的大小,android也不例外,但是像素并不代表图片在内存中的大小,还和ARGB通道有很大关系,其中A:表示透明度,R:表示三原色中的红,G:表示三原色中的绿,B:表示三原色中的蓝。即像素(长*宽)和像素位数(色彩模式)是用来描述图片的,可以通过这些信息计算出图片的像素占用内存的大小。而在Bitmap的一个内部类Config中就定义了图片的色彩模式,也就确定了图片在内存中所占的大小:

Config 常量值(色彩模式) 描述 内存消耗(字节/像素)
Bitmap.Config ARGB_8888 32位的ARGB位图 4 (argb_8888模式每个像素占用内存4byte)
Bitmap.Config ARGB_4444 16位的ARGB位图 2 (argb_444模式每个像素占用内存2byte)
Bitmap.Config RGB_565 16位的RGB位图 2 (rgb_565模式每个像素占用内存2byte)
Bitmap.Config ALPHA_8 8位的Alpha位图 1 (alpha_8模式每个像素占用内存1byte)

Bitmap在内存中的大小与图片像素色彩模式有关,图片不同的色彩模式每个像素都有着不同的内存消耗。如:加载一张分辨率为1024 * 768的图片,若采用argb_8888色彩模式,则占用空间10247684=3M(1M=1024KB=1024*1024字节);如果采用argb_444占用内存就能减半,所以如果不考虑透明通道的话,建议使用RGB_565色彩模式,内存消耗较小。
ARGB_8888:四个通道都是8位,每个像素占用4个字节,图片质量是最高的,但是占用的内存也是最大的;
ARGB_4444:四个通道都是4位,每个像素占用2个字节,图片的失真比较严重;
RGB_565:没有A通道,每个像素占用2个字节,图片失真小,但是没有透明度;
ALPHA_8:只有A通道,每个像素占用1个字节大大小,只有透明度,没有颜色值。
使用场景总结:ARGB_4444失真严重,基本不用;ALPHA_8使用场景特殊,比如设置遮盖效果等;不需要设置透明度,RGB_565是个不错的选择;既要设置透明度,对图片质量要求又高,就用ARGB_8888。

(2) 构造方法

/**
     * Private constructor that must received an already allocated native bitmap
     * int (pointer).
     */
    // called from JNI
    Bitmap(long nativeBitmap, byte[] buffer, int width, int height, int density,
            boolean isMutable, boolean requestPremultiplied,
            byte[] ninePatchChunk, NinePatch.InsetStruct ninePatchInsets) {
        if (nativeBitmap == 0) {
            throw new RuntimeException("internal error: native bitmap is 0");
        }

        mWidth = width;
        mHeight = height;
        mIsMutable = isMutable;
        mRequestPremultiplied = requestPremultiplied;
        mBuffer = buffer;

        mNinePatchChunk = ninePatchChunk;
        mNinePatchInsets = ninePatchInsets;
        if (density >= 0) {
            mDensity = density;
        }

        mNativePtr = nativeBitmap;
        long nativeSize = NATIVE_ALLOCATION_SIZE;
        if (buffer == null) {
            nativeSize += getByteCount();
        }
        NativeAllocationRegistry registry = new NativeAllocationRegistry(
            Bitmap.class.getClassLoader(), nativeGetNativeFinalizer(), nativeSize);
        registry.registerNativeAllocation(this, nativeBitmap);
    }

构造方法不是public的,也就是说不能直接的去new Bitmap();而且注释说明 called from JNI,也就是要使用bitmap的辅助类BitmapFactory来操作图片。掌握使用Bitmap的两个辅助类BitmapFactory和Options来对图片进行解码(即加载)--->操作--->保存--->释放(内存)等操作,掌握了BitmapFactory和Options即掌握了Bitmap。

2. BitmapFactory

2.1 解码(加载)图片

BitmapFactory类提供了四类方法来加载图片:decodeResource、decodeFile、decodeStream、decodeByteArray。
(1) BitmapFactory.decodeResource(Resources,int),通过本地资源文件加载bitmap
(2) BitmapFactory.decodeResource(Resources,int,Options),设置图片的Options属性

private void getDecodeResourceBitmap(){
        Bitmap bitmap = BitmapFactory.decodeResource(getContext().getResources(),R.drawable.sample);
        mImage.setImageBitmap(bitmap);
    }

(3) BitmapFactory.decodeFile(String),通过本地文件路径加载图片
(4) BitmapFactory.decodeFile(String,Options),设置图片的Options属性

private void getDecodeFileBitmap() {
        String fileName = "/sdcard/daniulivelogo.png";  //自己手机sd图片路径
        File file = new File(fileName);
        if (file.exists()) {
            Bitmap bm = BitmapFactory.decodeFile(fileName);
            mImage.setImageBitmap(bm);
        } else {
            Log.v("hjz","文件不存在");
        }
    }

(5) public static Bitmap decodeStream(InputStream),从输入流中中解码图片数据
(6) public static Bitmap decodeStream(InputStream,Rect,Options),设置Rect和Options属性

try {
    FileInputStream in = new FileInputStream("/sdcard/Download/sample.png");
} catch (FileNotFoundException e) {
    e.printStackTrace();
}

Bitmap bitmap = BitmapFactory.decodeStream(in);//从输入流中加载图片
Bitmap bm = BitmapFactory.decodeFile(sd_path); //从文件中加载图片
private void getDecodeStreamBitmap(){
        new Thread(new Runnable() {
            @Override
            public void run() {
                final Bitmap bitmap = getImageFromNet("http://img5.imgtn.bdimg.com/it/u=274881988,3237971911&fm=27&gp=0.jpg");
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        mImage.setImageBitmap(bitmap);
                    }
                });
            }
        }).start();
    }
 
private Bitmap getImageFromNet(String path) {
        HttpURLConnection httpURLConnection = null;
        InputStream inputStream = null;
        try {
            URL url = new URL(path);
            httpURLConnection = (HttpURLConnection) url.openConnection();
            httpURLConnection.setRequestMethod("GET");
            httpURLConnection.setReadTimeout(6 * 1000);
            if (httpURLConnection.getResponseCode() == HttpURLConnection.HTTP_OK) {
                inputStream = httpURLConnection.getInputStream();
                return BitmapFactory.decodeStream(inputStream);
            }
        } catch (MalformedURLException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            if (inputStream != null){
                try {
                    inputStream.close();
                    inputStream = null;
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (httpURLConnection != null){
                httpURLConnection.disconnect();
            }
        }
        return null;
    }

(7) public static Bitmap decodeByteArray(byte[] data, int offset, int length),从字节数组中加载图片

  • data:图片数据
  • offset:解码起始位置
  • length:要解码的长度

(8) public static Bitmap decodeByteArray(byte[] data, int offset, int length,Options),设置图片的Options属性

// InputStream转换成byte[]
Bitmap bm = BitmapFactory.decodeByteArray(myByte,0,myByte.length);
private void getBitmap(){
        //主要是把网络图片的数据流读入到内存中
        new Thread(new Runnable() {
            @Override
            public void run() {
                //Android4.0以后,网络请求一定要在子线程中进行
                final byte[] data = getImages("http://img5.imgtn.bdimg.com/it/u=274881988,3237971911&fm=27&gp=0.jpg");
                //更新UI可以在runOnUiThread这个方法或通过handler
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        Bitmap bitmap = BitmapFactory.decodeByteArray(data,0,data.length);
                        mImage.setImageBitmap(bitmap);
                    }
                });
 
            }
        }).start();
    }
 
private byte[] getImages(String path){
        try {
            URL url = new URL(path);
            HttpURLConnection httpURLConnection = (HttpURLConnection) url.openConnection();
            httpURLConnection.setRequestMethod("GET");
            httpURLConnection.setReadTimeout(6*1000);
            InputStream inputStream = null;
            ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
            byte[] buffer = new byte[1024];
            int len = -1;
            if (httpURLConnection.getResponseCode() == HttpURLConnection.HTTP_OK){
                inputStream = httpURLConnection.getInputStream();
                while ((len = inputStream.read(buffer)) !=-1){
                    outputStream.write(buffer,0,len);
                }
                outputStream.close();
                inputStream.close();
            }
            return outputStream.toByteArray();
        } catch (MalformedURLException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

这些方法没什么特殊的,重点是Options类,弄明白Options就算搞定Bitmap了

2.2 高效加载类Options

备注:高效加载的核心主要是采样压缩(Options)缓存策略(LRU算法)异步加载(Fresco)等。

Options是BitmapFractory的一个内部类,定义了若干属性,图片加载时通过设置Options的各个属性来指定图片的各种属性、压缩、宽高控制等等。常用属性如下:

属性名 类型 默认值 备注
inJustDecodeBounds boolean - 解码时返回仅图片宽高还是本身数据:设置为true则解码只返回bitmap尺寸,不为其分配内存;设置为false,解码返回位图本身数据,为其分配内存。当我们加载时只需获取图片宽高时,就可以设置为true,减低对内存的占用。
inSampleSize int - 解码时图片压缩比例:设置值>1时,按相关比例缩放bitmap宽高,返回较小图片再保存;设置值<=1时,就当做1来处理。 inSampleSize = 原图尺寸/显示尺寸 ,如:width=100,height=100,inSampleSize = 2,则返回width=50,height=50,像素50*50=250 ,图片为原来1/4。
inPreferredConfig 4种色彩模式 Bitmap.Config.ARGB_8888 设置图片色彩模式:若不考虑透明通道的话,建议使用RGB_565模式(一个像素2byte),内存消耗较小。
inPremultiplied boolean true 解码返回的bitmap颜色通道上预先附加透明通道
inDither boolean false 抖动解码:图片不同的色彩模式每个像素都有着不同的内存消耗,32位的图像比16位的清晰,所保存的颜色数据自然要多得多。当要把一张32位的图像解码成16位的图像的时候,自然会丢失很多颜色的数据,会使得图片失真,特别是有颜色渐变的时候会产生较为明显的断裂,这时候我们如果使用抖动解码的话能在一定程度上缓解这种现象
inScaled boolean true Bitmap是否可以被缩放
outWidth和outHeight int - Bitmap的宽和高(解码错误值均为-1):一般和inJustDecodeBounds一起使用获取Bitmap的宽高
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
Bitmap bmp = BitmapFactory.decodeFile(path, options);
//这时候的bmp只能取到图片的宽高

2.3 对Bitmap的常见操作

(1) 采样压缩(BitmapFactory.Options示例,重点在属性inSampleSize值的计算)

开发中很多时候并不需要图片原图大小和原图这么高清,通过Options对图片进行采样压缩,降低内存占有空间,从而减少了内存溢出(OOM)问题。

String fileName = "/sdcard/aa.jpg";//(1) 原图路径
BitmapFactory.Options options = getBitmapFactory(fileName,400,400);//(2) 设置Options对象各属性值,并返回Options
Bitmap bmp = BitmapFactory.decodeFile(fileName,options);//(3) 加载(解码)图片
mImage2.setImageBitmap(bmp);//(4) 显示图片
 
    /**
     * 设置Options对象各属性值,并返回Options
     * @param  fileName 原图路径
     * @param pixelW 原图宽度
     * @param pixelH 原图高度
     * @return 返回Options对象
     */
    private BitmapFactory.Options getBitmapFactory(String fileName,int pixelW,int pixelH){
        BitmapFactory.Options options = new BitmapFactory.Options();
        File file = new File(fileName);
        if (file.exists()){
            //inJustDecodeBounds为true,不返回bitmap,只返回这个bitmap的尺寸
            options.inJustDecodeBounds = true;
            //设置图片色彩
            options.inPreferredConfig = Bitmap.Config.RGB_565;//使用RGB_565减少图片大小 
            //预加载图片
            Bitmap bitmap = BitmapFactory.decodeFile(fileName,options);
            //获取原始图片宽高
            int originalW = options.outWidth;
            int originalH = options.outHeight;
            //上面设置为true获取bitmap尺寸大小,在这里一定要重新设置为false,否则位图加载不出来
            options.inJustDecodeBounds = false;
            options.inSampleSize = getSampleSize(originalW,originalH,pixelW,pixelH);//利用返回的原图片的宽高,可以计算出缩放比inSampleSize(只能是2的整数次幂)
            return options;
        }
        return null;
    }

    /**
     * 计算压缩比:官网提供的算法
     * @param options
     * @param reqWidth
     * @param reqHeigth
     * @return
     */
    private int calculateInSampleSize(BitmapFactory.Options options,int reqWidth,int reqHeigth){
        int inSampleSize = 1;//默认压缩比
        final int width = options.outWidth;//原图宽度
        final int heigth = options.outHeight;//原图高度
        if (width > reqWidth || heigth > reqHeigth){//原图宽或高大于指定图片的尺寸
            if (width > heigth){
                inSampleSize = Math.round((float)heigth / (float)reqHeigth);
            }else {
                inSampleSize = Math.round((float)width / (float)reqWidth);
            }
        }
        return inSampleSize;
    }
 
    /**
     * 计算压缩比例inSampleSize的值
     * @param originalW 原图宽
     * @param originalH 原图高
     * @param pixelW 指定图片宽度
     * @param pixelH 指定图片高度
     * @return 返回原图缩放大小
     */
    private int getSampleSize(int originalW, int originalH, int pixelW, int pixelH) {
        int simpleSize = 1;
        if (originalW > originalH && originalW > pixelW){
            simpleSize = originalW / pixelW;
        }else if (originalH > originalW && originalH > pixelH){
            simpleSize = originalH / pixelH;
        }
        if (simpleSize <=0){
            simpleSize = 1;
        }
        return simpleSize;
    }

  /**
  * 计算压缩比例inSampleSize的值
  * @param ctx
  * @param file 图片
  * @param reqHeight 指定图片高度
  * @param reqWidth 指定图片宽度
  * @return
  */
  private int getSampleSize(Context ctx, Object file, int reqHeight, int reqWidth) {
      int inSampleSize = 1;
      Bitmap bitmap = null;
 
      BitmapFactory.Options ops = new BitmapFactory.Options();
      ops.inJustDecodeBounds = true;//  //这里我们只需要图片的宽高,而不需要图片的真实数据
 
      if (file instanceof String) {
          bitmap = BitmapFactory.decodeFile((String) file, ops);//从本地文件中加载图片
      } else if (file instanceof Integer) {
          bitmap = BitmapFactory.decodeResource(ctx.getResources(), (Integer) file, ops);//从本地资源文件中加载图片
      }
 
      int height, width;
      if (bitmap == null) {
          return inSampleSize;
      }
 
      height = ops.outHeight;//获取原始图片高度
      width = ops.outWidth;//获取原始图片宽度
 
      if (height > reqHeight || width > reqWidth) {
          if (height / reqHeight > width / reqWidth) {
              inSampleSize = height / reqHeight;
          } else {
              inSampleSize = width / reqWidth;
          }
      }
      return inSampleSize;
  }


  /**
  * 计算压缩比例inSampleSize的值
  * @param options
  * @param reqWidth 指定图片宽度
  * @param reqHeight 指定图片高度
  * @return
  */
  private int getSampleSize(BitmapFactory.Options options,int reqWidth,int reqHeight){
    int inSampleSize = 1;
    int width = options.outWidth;
    int height =options.outHeight;
    int halfWidth = width / 2;
    int halfHeight = height / 2;
    while((halfWidth / inSampleSize) >= reqWidth && (halfHeight / inSampleSize) >= reqHeight){
        inSampleSize *= 2;
    }
    return inSampleSize;
  }

/**
* 计算压缩比例inSampleSize的值
* @param path 图片路径
* @param reqHeight 指定图片高度
* @param reqWidth 指定图片宽度
*/
private int getSampleSize(String path, int reqHeight, int reqWidth) {
    int inSampleSize = 1;

    BitmapFactory.Options options = new BitmapFactory.Options();

    //(1) 获取原图尺寸
    options.inJustDecodeBounds = true;//设置为true则解码只返回bitmap尺寸,不为其分配内存
    BitmapFactory.decodeFile(path, options);//解码
    int height = options.outHeight;
    int width = options.outWidth;
 
    //(2) 计算压缩比例inSampleSize的值
    if (height > reqHeight || width > reqWidth) {
        final int heightRatio = Math.round((float) height / (float) reqHeight);
        final int widthRatio = Math.round((float) width / (float) reqWidth);
        options.inSampleSize = heightRatio < widthRatio ? heightRatio : widthRatio;
    } else {
        options.inSampleSize = 1;
    }
    return inSampleSize;
}

这里要特别注意的是inSampleSize的值最终都会被换算成2的整数倍,也就是如果我们计算出来的是5,最终取值为4,所以准确性上来说回稍有误差,如果过于追求性能,可以考虑使用Matrix。

(2) 使用compress()对bitmap进行压缩

  private static Bitmap compressImage(Bitmap image) {
        if (image == null) {
            return null;
        }
        ByteArrayOutputStream baos = null;
        try {
            baos = new ByteArrayOutputStream();
            /**
             * 参数1:压缩格式
             * 参数2:压缩率,30表示图片被压缩了70%,100表示不压缩
             * 参数3:将压缩完成后的图片数据存入流中
             */
            image.compress(Bitmap.CompressFormat.JPEG, 30, baos);
            byte[] bytes = baos.toByteArray();
 
            //根据字节数组获取一个输入流
            ByteArrayInputStream isBm = new ByteArrayInputStream(bytes);
 
            //将输入流转换成最终的bitmap
            Bitmap bitmap = BitmapFactory.decodeStream(isBm);
            return bitmap;
        } catch (OutOfMemoryError e) {
        } finally {
            try {
                if (baos != null) {
                    baos.close();
                }
            } catch (IOException e) {
            }
        }
        return null;
    }

2.4 存储

2.3以前:图片存储在native内存中。虚拟机无法自动进行垃圾回收,必须手动使用recycle,很容易导致内存泄露。
3.0以后:图片存储在Java堆中。垃圾回收能够自动进行,内存占用也能方便的展示在monitor中。
4.0以后:传输方式发生变化,大数据会通过ashmem(匿名共享内存)来传递(不占用Java内存),小数据通过直接拷贝的方式(在内存中操作),放宽了图片大小的限制。参考:Android4.0 Bitmap存储以及Parcel传输源码分析
6.0以后:加强了ashmen存储图片的方式。参考:Android6.0 Bitmap存储以及Parcel传输源码分析

2.5 内存优化

优化处理:如采样压缩获取缩略图(在不同的情形下加载不同大小的图片到内存,避免原图加载造成OOM)、缓存方式选择(如:三级缓存)、内存管理等。
(1) 采样压缩获取缩略图:计算压缩比例inSampleSize,根据inSampleSize值获取缩略图并存储在内存

package comi.example.liy.mytestdemo;

import android.graphics.Bitmap;
import android.graphics.BitmapFactory;

/**
 * Created by liy on 2019-11-14 10:19
 * 自定义Bitmap工具类
 */
public class BitmapUtils {

    /**
     * 计算压缩比:官网提供的方法
     * @param options
     * @param reqWidth 指定图片宽度
     * @param reqHeigth 指定图片高度
     * @return
     */
    public int calculateInSampleSize(BitmapFactory.Options options,int reqWidth,int reqHeigth){
        int inSampleSize = 1;//默认压缩比
        final int width = options.outWidth;//原图宽度
        final int heigth = options.outHeight;//原图高度
        if (width > reqWidth || heigth > reqHeigth){//原图宽或高大于指定图片的尺寸
            if (width > heigth){
                inSampleSize = Math.round((float)heigth / (float)reqHeigth);
            }else {
                inSampleSize = Math.round((float)width / (float)reqWidth);
            }
        }
        return inSampleSize;
    }

    /**
     * 获取缩略图
     * @param pathName
     * @param maxWidth
     * @param maxHeigth
     * @return
     */
    public Bitmap getThumbnail(String pathName,int maxWidth,int maxHeigth){
        BitmapFactory.Options options = new BitmapFactory.Options();
        //计算压缩比
        options.inJustDecodeBounds = true;//设置加载图片仅返回图片尺寸
        Bitmap bitmap = BitmapFactory.decodeFile(pathName,options);//加载图片inJustDecodeBounds设置为true时,加载图片仅返回尺寸(不用为其分配内存)
        int inSampleSize = calculateInSampleSize(options,maxWidth,maxHeigth);//获取压缩比(options中仅包含原图尺寸)
        //设置options各属性
        options.inPreferredConfig = Bitmap.Config.RGB_565;//设置色彩模式:如果不考虑透明通道的话,建议使用RGB_565色彩模式,内存消耗较小
        options.inSampleSize =inSampleSize;//设置压缩比
        options.inJustDecodeBounds = false;//设置加载返回图片本身(需分配内存):上面设置为true获取bitmap尺寸大小,在这里一定要重新设置为false,否则位图加载不出来
        //加载图片
        if (bitmap != null || !bitmap.isRecycled()){
            bitmap.recycle();//回收
        }
        bitmap = BitmapFactory.decodeFile(pathName,options);
        return bitmap;
    }
    public Bitmap getThumbnailTest(String pathName,int maxWidth,int maxHeigth){
        BitmapFactory.Options options = new BitmapFactory.Options();
        //计算压缩比
        options.inJustDecodeBounds = true;//设置加载图片仅返回图片尺寸
        BitmapFactory.decodeFile(pathName,options);//加载图片:inJustDecodeBounds设置为true时,加载图片仅返回尺寸(不用为其分配内存)
        int inSampleSize = calculateInSampleSize(options,maxWidth,maxHeigth);//获取压缩比(options中仅包含原图尺寸)
        //设置options各属性
        options.inPreferredConfig = Bitmap.Config.RGB_565;//设置色彩模式:如果不考虑透明通道的话,建议使用RGB_565色彩模式,内存消耗较小
        options.inSampleSize =inSampleSize;//设置压缩比
        options.inJustDecodeBounds = false;//设置加载返回图片本身(需分配内存):上面设置为true获取bitmap尺寸大小,在这里一定要重新设置为false,否则位图加载不出来
        //加载图片
        Bitmap bitmap = BitmapFactory.decodeFile(pathName,options);
        return bitmap;
    }

}

(2) LRU算法:三级缓存会用到

LRU算法(最近最少使用)实现原理:LRU算法定义了一个泛型类LRUCache,内部用LinkedHashMap的实现。通过put()get()方法来添加和获取缓存对象,当缓存队列满的时候,通过trimToSize()方法将最近最少使用的缓存对象从缓存队列中移除(remove方法),并将新的缓存对象添加到缓存队列。

(3) 三级缓存:网络缓存、本地缓存和内存缓存

三级缓存包括网络缓存、本地缓存和内存缓存:当首次加载一张图片时,需向网络请求,网络获取图片后将图片保存到本地和内存各一份,当再次加载显示图片时就从本地或内存来获取。

2.6 开源框架(起码阅读Glide和Fresco源码)

常用的图片加载框架有: ImageLoader、Glide(google)、Fresco(FaceBook)、Picasso(Square)。

  • Picasso包体积小、清晰,但功能有局限不能加载gif、只能缓存全尺寸
  • Glide功能全面,擅长大型图片流,提交较大
  • Fresco内存优化,减少oom,体积更大

你可能感兴趣的:(Bitmap位图)