玩转碎片复用下bitmap任意裁剪

3,489 阅读6分钟
原文链接: www.jianshu.com

Bitmap碎片复用任意操作开源库:github.com/Dawish/Bitm…

一、Bitmap庐山真面目

Bitmap 并不是一个图片。最开始接触Android的人可能以为 Bitmap 就一个图片,比如我,哈哈哈,我最开始接触Android时就是如此认为。

一句话总结:

Bitmap 是一个实现了 Parcelable 接口的 final 类,不能用 new 关键字来创建一个Bitmap,
Bitmap中的 java 功能方法基本都是调用native实现的。

Bitmap 这个单词分开看就是 bit 和 map ,就好像我们java中的HashMap这样,只是一个特殊的数据保存类。

Android中的Bitmap类是Android界面绘制数据存储类,我们的 Canvas 把绘制的东西保存在Bitmap中,然后交给底层去渲染,由于需要在内存中传递Bitmap,所以这也是实现了Parcelable 接口的原因。

二、Bitmap复用

2.1 不同版本存储区别:

Android 2.3.3及以前版本,bitmap的像素点数据是保存在native memory,而bitmap对象是保存在Dalvik heap, 从Android 3.0开始,像素点数据与bitmap对象一起存储在Dalvik heap中。

前两天看到最新的Android O把Bitmap的像素点数据又放在了native memory中了。
可以参考文章:www.cnblogs.com/xiaji5572/p…

2.2 不同版本复用区别:

在Android 3.0开始引入了inBitmap设置,通过设置这个参数,在图片加载的时候可以使用之前已经创建了的Bitmap,但是需要大小一样,以便节省内存,避免再次创建一个Bitmap。在Android4.4,新增了允许inBitmap设置的图片与需要加载的图片的大小不同的情况,只要inBitmap的图片比当前需要加载的图片 就好了。

2.3 Bitmap复用注意点:

Bitmap复用首选需要其 mIsMutable 属性为 true , mIsMutable 的表面意思为:易变的

Bitmap中的意思为: 控制bitmap的setPixel方法能否使用,也就是外界能否修改bitmap的像素。mIsMutable 属性为 true 那么就可以修改Bitmap的像素数据,这样也就可以实现Bitmap对象的复用了。

那么在创建一个Bitmap的时候这个 mIsMutable 属性怎么赋值成 true 呢?

1. 使用 Bitmap 的 createBitmap 方法创建一个新的Bitmap时 mIsMutable 属性 默认为 true
源码:

    public static Bitmap createBitmap(@Nullable DisplayMetrics display, int width, int height,
            @NonNull Config config, boolean hasAlpha, @NonNull ColorSpace colorSpace) {

           ......略

        // nullptr color spaces have a particular meaning in native and are interpreted as sRGB
        // (we also avoid the unnecessary extra work of the else branch)
        if (config != Config.ARGB_8888 || colorSpace == ColorSpace.get(ColorSpace.Named.SRGB)) {
            
            /**********第八个mutable属性直接赋值为true**********/
            bm = nativeCreate(null, 0, width, width, height, config.nativeInt, true, null, null);
        } else {
            if (!(colorSpace instanceof ColorSpace.Rgb)) {
                throw new IllegalArgumentException("colorSpace must be an RGB color space");
            }
            ColorSpace.Rgb rgb = (ColorSpace.Rgb) colorSpace;
            ColorSpace.Rgb.TransferParameters parameters = rgb.getTransferParameters();
            if (parameters == null) {
                throw new IllegalArgumentException("colorSpace must use an ICC "
                        + "parametric transfer function");
            }

            ColorSpace.Rgb d50 = (ColorSpace.Rgb) ColorSpace.adapt(rgb, ColorSpace.ILLUMINANT_D50);

           /**********第八个mutable属性直接赋值为true**********/
            bm = nativeCreate(null, 0, width, width, height, config.nativeInt, true,
                    d50.getTransform(), parameters);
        }

           ......略

}

2. 使用BitmapFactory结合 Options 去decode一个Bitmap
在设置解码属性Options的时候可以将 inMutable 设置为true

 1 final BitmapFactory.Options options = new BitmapFactory.Options();
 2         //size必须为1 否则是使用inBitmap属性会报异常
 3         options.inSampleSize = 1;
 4         //这个属性一定要在用在src Bitmap decode的时候 不然你再使用哪个inBitmap属性去decode时候会在c++层面报异常
 5         //BitmapFactory: Unable to reuse an immutable bitmap as an image decoder target.
 6         options.inMutable = true;
            //获得一个Bitmap对象inBitmap2 
 7         inBitmap2 = BitmapFactory.decodeFile(path1,options);
            //使用对象inBitmap2 
 8         iv.setImageBitmap(inBitmap2);
            //使用完了对象inBitmap2 后拿去复用此对象解码新的Bitmap
 9         options.inBitmap = inBitmap2;
10         long start=System.currentTimeMillis();
11         iv2.setImageBitmap(BitmapFactory.decodeFile(path2,options));
12         iv3.setImageBitmap(BitmapFactory.decodeFile(path3,options));
13         iv4.setImageBitmap(BitmapFactory.decodeFile(path4,options));

这种复用的方式都写在注释里了。

2.4 Bitmap复用实现:

1. BitmapFactory解码图片时使用缓存的bitmap
在上面的代码中可以看到,从缓存中获取一个可以被复用的bitmap在使用BitmapFactory去解码是把options.inBitmap赋值成这个bitmap,这样就把这个bitmap复用了。

2. 需要一个空的bitmap时可以从缓存中获取
可以把用完了的bitmap放在 LruCache 缓存中,比如我们在使用 Canvas 需要射中一个 bitmap,这样我们就可以从 LruCache 缓存获取一个可以被复用的 bitmap 设置给 Canvas

github上已经有一个开源的bitmap复用项目 github.com/amitshekhar…,使用非常简单。

GlideBitmapPool开源项目用法大致如下:

Using Glide Bitmap Pool in your application

Add this in your build.gradle

compile 'com.amitshekhar.android:glide-bitmap-pool:0.0.1'

Then initialize it in onCreate() Method of application class, :

GlideBitmapPool.initialize(10 * 1024 * 1024); // 10mb max memory size

Decoding the bitmap from file path

Bitmap bitmap = GlideBitmapFactory.decodeFile(filePath);

Decoding the bitmap from resources

Bitmap bitmap = GlideBitmapFactory.decodeResource(getResources(), R.drawable.testImage);

Decoding the down sample bitmap

Bitmap bitmap = GlideBitmapFactory.decodeFile(filePath,100,100);

Making the bitmap available for recycle or reuse

GlideBitmapPool.putBitmap(bitmap);

Getting the empty bitmap from the pool

Bitmap bitmap = GlideBitmapPool.getBitmap(width, height, config);

Clearing or Trimming Memory

GlideBitmapPool.clearMemory();
GlideBitmapPool.trimMemory(level);

三、Bitmap碎片复用的情况下任意裁剪

这里说的碎片复用就是在图片的裁剪过程中创建丢弃大量的Bitmap对象,如果不对这些Bitmap进行复用会造成多余的内存浪费,造成内存抖动

3.1 Bitmap裁剪保留下部分:

说明 前后效果对比
裁剪保留下部分,取一半高度 TIM图片20180228220755.png 裁剪后: Screenshot_2018-02-28-21-59-16-052_BitmapKit.png

裁剪代码:

    /**
     * 裁剪一定高度保留下面
     * @param srcBitmap
     * @param needHeight
     * @param recycleSrc 是否回收原图
     * @return
     */
    @DebugLog
    public static Bitmap cropBitmapBottom(Bitmap srcBitmap, int needHeight, boolean recycleSrc) {

        Log.d("danxx", "cropBitmapBottom before h : "+srcBitmap.getHeight());

        /**裁剪保留下部分的第一个像素的Y坐标*/
        int needY = srcBitmap.getHeight() - needHeight;

        /**裁剪关键步骤*/
        Bitmap cropBitmap = Bitmap.createBitmap(srcBitmap,0,needY,srcBitmap.getWidth(),needHeight);

        Log.d("danxx", "cropBitmapBottom after h : "+cropBitmap.getHeight());

        /**回收之前的Bitmap*/
        if (recycleSrc && srcBitmap != null && !srcBitmap.equals(cropBitmap) && !srcBitmap.isRecycled()) {
            GlideBitmapPool.putBitmap(srcBitmap);
        }

        return cropBitmap;
    }

3.2 Bitmap裁剪保留左部分:

说明 前后效果对比
裁剪保留左部分,取一半宽度 TIM图片20180228220755.png 裁剪后: Screenshot_2018-02-28-21-59-34-089_BitmapKit.png

裁剪代码:

    /**
     * 裁剪一定高度保留左边
     * @param srcBitmap
     * @param needWidth
     * @return
     */
    @DebugLog
    public static Bitmap cropBitmapLeft(Bitmap srcBitmap, int needWidth, boolean recycleSrc) {

        Log.d("danxx", "cropBitmapLeft before w : "+srcBitmap.getWidth());

        /**裁剪关键步骤*/
        Bitmap cropBitmap = Bitmap.createBitmap(srcBitmap, 0, 0, needWidth, srcBitmap.getHeight());

        Log.d("danxx", "cropBitmapLeft after w : "+cropBitmap.getWidth());

        /**回收之前的Bitmap*/
        if (recycleSrc && srcBitmap != null && !srcBitmap.equals(cropBitmap) && !srcBitmap.isRecycled()) {
            GlideBitmapPool.putBitmap(srcBitmap);
        }

        return cropBitmap;
    }

3.3 Bitmap裁剪保留右部分:

说明 前后效果对比
裁剪保留右部分,取一半宽度 TIM图片20180228220755.png 裁剪后: Screenshot_2018-02-28-22-00-03-095_BitmapKit.png

裁剪代码:

    /**
     * 裁剪一定高度保留左边
     * @param srcBitmap
     * @param needWidth
     * @return
     */
    @DebugLog
    public static Bitmap cropBitmapRight(Bitmap srcBitmap, int needWidth, boolean recycleSrc) {

        Log.d("danxx", "cropBitmapRight before w : "+srcBitmap.getWidth());

        int needX = srcBitmap.getWidth() - needWidth;

        /**裁剪关键步骤*/
        Bitmap cropBitmap = Bitmap.createBitmap(srcBitmap, needX, 0, needWidth, srcBitmap.getHeight());

        Log.d("danxx", "cropBitmapRight after w : "+cropBitmap.getWidth());

        /**回收之前的Bitmap*/
        if (recycleSrc && srcBitmap != null && !srcBitmap.equals(cropBitmap) && !srcBitmap.isRecycled()) {
            GlideBitmapPool.putBitmap(srcBitmap);
        }

        return cropBitmap;
    }

3.4 Bitmap裁剪保留上部分:

说明 前后效果对比
裁剪保留上部分,取一半高度 TIM图片20180228220755.png 裁剪后: Screenshot_2018-02-28-21-59-49-769_BitmapKit.png

裁剪代码:

   /**
     * 裁剪一定高度保留下面
     * @param srcBitmap
     * @param needHeight
     * @param recycleSrc 是否回收原图
     * @return
     */
    @DebugLog
    public static Bitmap cropBitmapTop(Bitmap srcBitmap, int needHeight, boolean recycleSrc) {

        Log.d("danxx", "cropBitmapBottom before h : "+srcBitmap.getHeight());

        /**裁剪保留上部分的第一个像素的Y坐标*/
        int needY = 0;

        /**裁剪关键步骤*/
        Bitmap cropBitmap = Bitmap.createBitmap(srcBitmap,0,needY,srcBitmap.getWidth(),needHeight);

        Log.d("danxx", "cropBitmapBottom after h : "+cropBitmap.getHeight());

        /**回收之前的Bitmap*/
        if (recycleSrc && srcBitmap != null && !srcBitmap.equals(cropBitmap) && !srcBitmap.isRecycled()) {
            GlideBitmapPool.putBitmap(srcBitmap);
        }

        return cropBitmap;
    }


3.5 Bitmap指定参数任意裁剪:

说明 前后效果对比
指定参数任意裁剪 TIM图片20180228220755.png 裁剪后: Screenshot_2018-02-28-22-00-13-606_BitmapKit.png

裁剪代码:

    /**
     * 自定义裁剪,根据第一个像素点(左上角)X和Y轴坐标和需要的宽高来裁剪
     * @param srcBitmap
     * @param firstPixelX
     * @param firstPixelY
     * @param needWidth
     * @param needHeight
     * @param recycleSrc
     * @return
     */
    @DebugLog
    public static Bitmap cropBitmapCustom(Bitmap srcBitmap, int firstPixelX, int firstPixelY, int needWidth, int needHeight, boolean recycleSrc) {

        Log.d("danxx", "cropBitmapRight before w : "+srcBitmap.getWidth());
        Log.d("danxx", "cropBitmapRight before h : "+srcBitmap.getHeight());

        if(firstPixelX + needWidth > srcBitmap.getWidth()){
            needWidth = srcBitmap.getWidth() - firstPixelX;
        }

        if(firstPixelY + needHeight > srcBitmap.getHeight()){
            needHeight = srcBitmap.getHeight() - firstPixelY;
        }

        /**裁剪关键步骤*/
        Bitmap cropBitmap = Bitmap.createBitmap(srcBitmap, firstPixelX, firstPixelY, needWidth, needHeight);

        Log.d("danxx", "cropBitmapRight after w : "+cropBitmap.getWidth());
        Log.d("danxx", "cropBitmapRight after h : "+cropBitmap.getHeight());


        /**回收之前的Bitmap*/
        if (recycleSrc && srcBitmap != null && !srcBitmap.equals(cropBitmap) && !srcBitmap.isRecycled()) {
            GlideBitmapPool.putBitmap(srcBitmap);
        }

        return cropBitmap;
    }


Bitmap碎片复用任意操作开源库:github.com/Dawish/Bitm…