我是靠谱客的博主 体贴飞鸟,最近开发中收集的这篇文章主要介绍(五)图片压缩 —— 优化图片文件、内存一、图片压缩二、文件压缩三、压缩原理四、内存压缩五、手写图片缓存六、附,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

版权声明:本文为博主原创文章,未经博主允许不得转载。
本文纯个人学习笔记,由于水平有限,难免有所出错,有发现的可以交流一下。

一、图片压缩

Android 中图片是以 Bitmap 形式存在的,而且 Bitmap 是比较占内存的。所以,如果能对 Bitmap 进行压缩,对内存优化这块有很大的帮助。我们首先需要知道 Bitmap 占用内存的计算方式:

图片长上的像素点 * 图片宽上的像素点 * 一个像素点占用的字节数

从公式可以看出,只要我们减小 Bitmap 所占内存的三个因素之一,都可以压缩图片所占内存。

  1. Android 中 RGB 编码格式

RGB888(int):R、G、B分量各占8位
RGB565(short):R、G、B分量分别占5、6、5位
RGB555(short):RGB 分量都用5位表示(剩下的1位不用)
ARGB8888(int):A、R、G、B分量各占8位
ARGB4444(short):A、R、G、B分量各占4位
  1. 图片存在形式

在安卓中,图片有三种形式存在。

1.文件形式(以二进制形式保存在SD 卡)
2.流的形式(以二进制形式存在于内存)
3.Bitmap 形式  

一般来说,手机 SD 卡上的文件形式图片,与内存中流形式的图片,大小相同。不过,当图片以 Bitmap 形式存在时,其占用的内存会变得很大。

查看大小:

文件形式: file.length()

流的形式: 查看流的 byte 个数

Bitmap:bitmap.getByteCount()
  1. 图片压缩

由上面可以知道,图片可能存在 SD 卡里,或者存在于内存,对于这两种存在形式都可以进行压缩,分别称为文件压缩和内存压缩

文件压缩: 为了减小图片文件的大小,比如上传图片到服务器,减少本地图片存储空间。

内存压缩: 这块主要是对 Bitmap 所占内存进行压缩,减少内存占用。

二、文件压缩

文件压缩有三种:

质量压缩	
尺寸压缩	
格式选择:JPEG/WEBP (4.0以上)

1.质量压缩

质量压缩是一种有损压缩,在压缩过程中,图片会丢失一些图片信息,变得不清晰。

代码:

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

        Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.timg);

        compress(bitmap, Bitmap.CompressFormat.JPEG, 100);
        compress(bitmap, Bitmap.CompressFormat.JPEG, 70);
        compress(bitmap, Bitmap.CompressFormat.JPEG, 50);
        compress(bitmap, Bitmap.CompressFormat.JPEG, 30);
        compress(bitmap, Bitmap.CompressFormat.JPEG, 0);
    }


    /**
     * 压缩图片到指定文件
     * @param bitmap 待压缩图片
     * @param format 压缩的格式
     * @param quality   质量
     */
    private boolean compress(Bitmap bitmap, Bitmap.CompressFormat format, int quality){

        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();

        bitmap.compress(format, quality, outputStream);

        byte[] bytes = outputStream.toByteArray();
        bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
        Log.i("MainActivity", " 111 zx 压缩后图片大小:" + bitmap.getByteCount()
                + " 宽度:" + bitmap.getWidth() + " 高度:" + bitmap.getHeight()
                + " bytes.length= " + (bytes.length / 1024) + "KB"
                + " quality=" + quality);
        
        if(outputStream != null){
            try {
                outputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return false;
    }

结果:
这里写图片描述

可以发现,进行压缩后,图片的大小,宽高是没有改变的。这是因为进行质量压缩的时候,并不会减少图片的像素,只是改变图片的位深和透明度。图片的宽、高、每个像素所占字节都没有改变,所以生成的 Bitmap 所占内存是不会改变的。

这边主要用到了 Bitmap 的 compress 方法。

public boolean compress(CompressFormat format, int quality, OutputStream stream)

format: Bitmap 内部类的枚举值,JPEG、PNG、WEBP。
quality: 图片质量,quality 越小,压缩后的图片二进制数据越短,也越容易丢失图片信息。
stream: 压缩后数据流。

注: PNG 图片是无损的,不能进行压缩。如果保存图片格式选择 PNG,那么 quality 失效。

也可以使用下面这段代码,把图片保存在 SD 卡中,然后进行查看,可以发现 quality 越小,图片越模糊。

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

        Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.timg);

        compress(bitmap, Bitmap.CompressFormat.JPEG, 100,Environment.getExternalStorageDirectory()+"/test_scaled100.jpeg");
        compress(bitmap, Bitmap.CompressFormat.JPEG, 70,Environment.getExternalStorageDirectory()+"/test_scaled70.jpeg");
        compress(bitmap, Bitmap.CompressFormat.JPEG, 50,Environment.getExternalStorageDirectory()+"/test_scaled50.jpeg");
        compress(bitmap, Bitmap.CompressFormat.JPEG, 30,Environment.getExternalStorageDirectory()+"/test_scaled30.jpeg");
        compress(bitmap, Bitmap.CompressFormat.JPEG, 0,Environment.getExternalStorageDirectory()+"/test_scaled0.jpeg");
    }

   /**
     * 压缩图片到指定文件
     * @param bitmap 待压缩图片
     * @param format 压缩的格式
     * @param quality   质量
     * @param path  文件地址
     */
    private void compress(Bitmap bitmap, Bitmap.CompressFormat format, int quality, String path){

        FileOutputStream outputStream = null;

        try {
            outputStream = new FileOutputStream(path);

            bitmap.compress(format, quality, outputStream);

        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } finally {
            if(outputStream != null){
                try {
                    outputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

小结:
进行质量压缩不会改变图片的大小以及图片编码格式,所以图片生成的 Bitmap 所占内存不会改变,无法达到内存压缩。
quality 越小,图片文件内存越小, 图片信息丢失越严重,失真越明显。

2.尺寸压缩

很明显就是减小图片的分辨率,从而达到压缩的目的。

Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.timg);

/**
 * 尺寸压缩
 */
Matrix matrix = new Matrix();
float scale =  0.5f;

//scale = 缩放大小 / 原大小
matrix.setScale(scale,scale);

bitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);

这是对图片进行缩放,压缩图片的宽、高,可以把这个 Bitmap 保存成图片文件。对于图片文件来说,宽、高变小了,所占 SD 卡空间自然也变小了。而且宽、高是 Bitmap 所占内存的两大因素,所以压缩后图片生成的 Bitmap 所占内存也变小了。

3.格式选择 JPEG/WEBP

在调用 Bitmap 的 compress 方法时,可以进行格式设置。

JPEG 格式的图片文件比 PNG 格式的图片文件小,而 WEBP 格式的图片文件比 JPEG 格式的图片文件更小。但是,一般上传到服务器的图片采用 JPEG,WEBP 格式目前应用还没有特别广。

对比相同 quality 下,JPEG 和 WEBP。

compress(bitmap, Bitmap.CompressFormat.JPEG, 100);
compress(bitmap, Bitmap.CompressFormat.JPEG, 70);
compress(bitmap, Bitmap.CompressFormat.JPEG, 50);
compress(bitmap, Bitmap.CompressFormat.JPEG, 30);
compress(bitmap, Bitmap.CompressFormat.JPEG, 0);

compress(bitmap, Bitmap.CompressFormat.WEBP, 100);
compress(bitmap, Bitmap.CompressFormat.WEBP, 70);
compress(bitmap, Bitmap.CompressFormat.WEBP, 50);
compress(bitmap, Bitmap.CompressFormat.WEBP, 30);
compress(bitmap, Bitmap.CompressFormat.WEBP, 0);

结果:
这里写图片描述

可以发现,不同格式的图片跟不同 quality 一样,图片文件大小不变,但是图片所生成的 Bitmap 占用的内存减小。

三、压缩原理

1.Bitmap 的 compress

compress:

public boolean compress(CompressFormat format, int quality, OutputStream stream) {
    checkRecycled("Can't compress a recycled bitmap");
    // do explicit check before calling the native method
    if (stream == null) {
        throw new NullPointerException();
    }
    if (quality < 0 || quality > 100) {
        throw new IllegalArgumentException("quality must be 0..100");
    }
    StrictMode.noteSlowCall("Compression of a bitmap is slow");
    Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, "Bitmap.compress");
    boolean result = nativeCompress(mNativePtr, format.nativeInt,
            quality, stream, new byte[WORKING_COMPRESS_STORAGE]);
    Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
    return result;
}
    
private static native boolean nativeCompress(long nativeBitmap, int format,
     int quality, OutputStream stream, byte[] tempStorage);

compress 方法调用到原生的 nativeCompress 方法,这个方法的具体实现是在安卓源码中:

/frameworks/base/core/jni/android/graphics/Bitmap.cpp

nativeCompress:

//nativeCompress 采用动态注册
{"nativeCompress", "(IIILjava/io/OutputStream;[B)Z", (void*)Bitmap_compress },

static bool Bitmap_compress(JNIEnv* env, jobject clazz, SkBitmap* bitmap,
                            int format, int quality,
                            jobject jstream, jbyteArray jstorage) {
    SkImageEncoder::Type fm;

	//判断图片格式	
    switch (format) {
    case kJPEG_JavaEncodeFormat:
        fm = SkImageEncoder::kJPEG_Type;
        break;
    case kPNG_JavaEncodeFormat:
        fm = SkImageEncoder::kPNG_Type;
        break;
    case kWEBP_JavaEncodeFormat:
        fm = SkImageEncoder::kWEBP_Type;
        break;
    default:
        return false;
    }

    bool success = false;
    if (NULL != bitmap) {
        SkAutoLockPixels alp(*bitmap);

        if (NULL == bitmap->getPixels()) {
            return false;
        }

        SkWStream* strm = CreateJavaOutputStreamAdaptor(env, jstream, jstorage);
        if (NULL == strm) {
            return false;
        }
		//开始压缩
        SkImageEncoder* encoder = SkImageEncoder::Create(fm);
        if (NULL != encoder) {
            success = encoder->encodeStream(strm, *bitmap, quality);
            delete encoder;
        }
        delete strm;
    }
    return success;
}

SkImageEncoder 的具体类是 externalskiasrcimages 下的 SkImageDecoder_libpng.cpp、SkImageDecoder_libjpeg.cpp、SkImageDecoder_libwebp.cpp 等。

2.Skia引擎

Skia 官网

上面使用的其实就是 Skia 引擎,这是一款 Google 研发、开源的 C++ 二维图形库 。

在安卓中使用的是阉割的 Skia 版本,对 JPEG 的处理是基于 libjpeg,对 PNG 则是基于 libpng。在安卓早期,由于 CPU 吃紧,将 libjpeg 中的最优哈夫曼编码关闭了。

在安卓 7.0 的 externalskiasrcimagesSkImageDecoder_libjpeg.cpp 中有进行设置,7.0 之前的源码是没有这个设置。

cinfo.optimize_coding = TRUE;

这是 Android Studio 在创建项目时候,提供的版本选择帮助,可以看见目前市面上各版本的支持。很明显,只有少部分手机达到 Android 7.0 以上,也就是说,只有少部分手机可以使用 libjpeg 中的最优哈夫曼编码。
这里写代码片

3.哈夫曼编码

主要思想: 采取可变长编码方式,对文件中出现次数多的字符采取比较短的编码,对于出现次数少的字符采取比较长的编码,可以有效地减小总的编码长度。

这里写图片描述

想了解跟多,具体可自行百度学习。

4.总结

libjpeg-turbo github地址

在 Android 7.0 之前,如果对图片文件大小有要求,自行下载 libjpeg-turbo 进行编译生成 so 库,然后编写 C 代码,使用 JNI 进行调用。这块属于 NDK 相关,内容较多,不在这里进行记录。

四、内存压缩

1.内存大小

在图片文件压缩中已经用到了图片生成的 Bitmap 所占内存使用方法 bitmap.getByteCount() 进行获取。

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

        Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.timg);

        compress(bitmap, Bitmap.CompressFormat.JPEG, 100);

    }

    /**
     * 压缩图片到制定文件
     * @param bitmap 待压缩图片
     * @param format 压缩的格式
     * @param quality   质量
     */
    private boolean compress(Bitmap bitmap, Bitmap.CompressFormat format, int quality){

        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();

        bitmap.compress(format, quality, outputStream);

        byte[] bytes = outputStream.toByteArray();
        bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
        Log.i("MainActivity", " 111 zx 压缩后图片大小:" + bitmap.getByteCount()
                + " 宽度:" + bitmap.getWidth() + " 高度:" + bitmap.getHeight()
                + " bytes.length= " + (bytes.length / 1024) + "KB"
                + " quality=" + quality);

        if(outputStream != null){
            try {
                outputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return false;
    }

打印结果:

这里写图片描述

Bitmap 占用内存的计算方式:

图片长上的像素点 * 图片宽上的像素点 * 一个像素点占用的字节数

Android 中 RGB 编码格式

RGB888(int):R、G、B分量各占8位
RGB565(short):R、G、B分量分别占5、6、5位
RGB555(short):RGB分量都用5位表示(剩下的1位不用)
ARGB8888(int):A、R、G、B分量各占8位
ARGB4444(short):A、R、G、B分量各占4位

在截图中,图片内存大小为 1093928,宽为 682, 高为 401。 1093928 = 682 * 401 * 4。这是因为采用的是 ARGB8888 编码格式,每个像素占用 4 个字节。

2.不同 drawer 文件夹

同样的代码,同一张图片,原先处于 drawable-xhdpi 文件夹下,现在我们移到 drawable 文件夹下。

这里写图片描述

可以发现,图片的宽、高、所占内存全部改变了。

我们获取 Bitmap 的方法是采用了 BitmapFactory 的 decodeResource,而这个会调用到 BitmapFactory 的 decodeResourceStream 方法。

BitmapFactory 的 decodeResourceStream:

    public static Bitmap decodeResourceStream(Resources res, TypedValue value,
            InputStream is, Rect pad, Options opts) {
        validate(opts);
        if (opts == null) {
            opts = new Options();
        }

        if (opts.inDensity == 0 && value != null) {
            final int density = value.density;
            if (density == TypedValue.DENSITY_DEFAULT) {
                opts.inDensity = DisplayMetrics.DENSITY_DEFAULT;
            } else if (density != TypedValue.DENSITY_NONE) {
                opts.inDensity = density;
            }
        }
        
        if (opts.inTargetDensity == 0 && res != null) {
            opts.inTargetDensity = res.getDisplayMetrics().densityDpi;
        }
        
        return decodeStream(is, pad, opts);
    }

在这里会有个 Options 参数,如果为空,进行赋默认值。

BitmapFactory.Options 是控制解码图片参数,有两个参数。

inDensity: 表示这个 Bitmap 的像素密度,根据 drawable 目录
inTargetDensity: 表示要被画出来时的目标(屏幕)像素密度, getResources().getDisplayMetrics().densityDpi。

Bitmap 在创建的时候,会根据这两个参数进行图片的缩放。

注: 简单对比上面两次打印结果,可以发现,第二次内存是第一次的 4 倍多,这是非常可怕的事情。也就是说,如果我们图片放对了 drawer 文件夹,可以节省大量的内存。

3.inBitmap 复用

我们知道,在安卓中, Bitmap 是内存大户,在不使用的时候,经常需要对 Bitmap 进行手动的回收释放。由于每一个 Bitmap 内存通常都很大,如果说对每一个新的 Bitmap 都进行一次内存申请,使用后释放,容易引起较大的内存抖动。严重的话,释放不及时,频繁的触发 GC 进行会后,甚至内存溢出。

在 Android3.0 开始,系统在 BitmapFactory.Options 里引入了 inBitmap 机制来配合缓存机制(在后面)。如果在载入图片时传入了inBitmap 那么载入的图片就会使用 inBitmap 的内存,而不会去新申请一块内存。

看一个简单的例子:

        BitmapFactory.Options options = new BitmapFactory.Options();
        Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.timg, options);
        for (int i = 0; i < 100; i ++){
            bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.timg, options);
        }

这是直接连续创建 100 个临时的 Bitmap,app 不停的申请内存。

占用内存:
这里写图片描述

        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inMutable = true;
        Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.timg, options);
        for (int i = 0; i < 100; i ++){
            options.inBitmap = bitmap;
            bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.timg, options);
        }

在这里,使用了 inBitmap 复用,新建的 Bitmap 使用旧的 Bitmap 内存。

占用内存:
这里写图片描述

合理的利用 inBitmap ,可以减少 app 申请的 Bitmap 内存,以及减少多次申请内存,是否内存造成的抖动。

注意点:
1.可被复用的 Bitmap 必须设置 inMutable 为 true。

2.Android4.4(API 19) 之前只有格式为 jpg、png,同等宽高(要求苛刻),inSampleSize 为 1 的 Bitmap 才可以复用;
Android4.4(API 19) 之前被复用的 Bitmap 的 inPreferredConfig 会覆盖待分配内存的 Bitmap 设置的 inPreferredConfig;
Android4.4(API 19) 之后被复用的 Bitmap 的内存必须大于等于需要申请内存的 Bitmap的 内存;

3.通过bitmap复用,减少频繁申请内存带来的性能问题。

4.API-19 的 getAllocationByteCount
一般情况下 getByteCount() 与 getAllocationByteCount() 是相等的;
通过复用 Bitmap 来解码图片,如果被复用的 Bitmap 的内存比待分配内存的 Bitmap 大,那么 getByteCount() 表示新解码图片占用内存的大小(并非实际内存大小,实际大小是复用的那个 Bitmap 的大小),getAllocationByteCount() 表示被复用 Bitmap 占用的内存大小。所以可能 allocation 比 bytecount 大。

4.LruCache

LruCache 是 Android 提供的一个缓存工具类,使用 LRU 缓存淘汰算法。根据数据的历史访问记录来进行淘汰数据, “如果数据最近被访问过,那么将来被访问的几率也更高”。

这里写图片描述

1.新数据插入到链表头部;
2.每当缓存命中(即缓存数据被访问),则将数据移到链表头部;
3.当链表满的时候,将链表尾部的数据丢弃。

五、手写图片缓存

1.核心思路

1.图片有二级缓存,内存缓存和磁盘缓存。
2.使用 inBitmap 复用已经不用的 Bitmap 内存,避免多次申请释放内存。
3.使用类似 LruCache 的 DiskLruCache 进行磁盘缓存的实现(二者基本一样)。

流程:
1.加载图片时,先从内存缓存读取图片。存在的话直接读取。
2.内存缓存不存在的时候,从磁盘缓存进行读取。存在的话直接读取,并添加到内存缓存。
3.磁盘缓存也不存在的时候,开始加载图片,同时保存到内存缓存和磁盘缓存中。
4.内存缓存中图片释放时候,添加到复用队列中,复用队列使用软引用。
5.添加到内存缓存的时候,先判断复用队列中是否有可复用的内存,有则进行复用。

2.核心代码

ImageCache:

public class ImageCache {

    private static ImageCache instance;
    private LruCache<String, Bitmap> memoryCache;

    // private DiskLruCache diskLruCache;
    BitmapFactory.Options options = new BitmapFactory.Options();

    //使用一个 Bitmap 复用池 来保存可被复用的Bitmap
    private Set<WeakReference<Bitmap>> reusablePool;
    /**
     * 使用一个引用队列,来接收 Bitmap 复用池 reusablePool 中弱引用回收事件
     * 主要是 Bitmap 在安卓 8.0 之后内存在 native,需要释放
     */
    private Thread clearReferenceQueue;
    private boolean shutDown;

    //磁盘缓存
    private DiskLruCache diskLruCache;

    //单例模式
    public static ImageCache getInstance() {
        if (null == instance) {
            synchronized (ImageCache.class) {
                if (null == instance) {
                    instance = new ImageCache();
                }
            }
        }
        return instance;
    }

    private ReferenceQueue referenceQueue;
    public ReferenceQueue<Bitmap> getReferenceQueue(){

        if(referenceQueue == null){
            referenceQueue = new ReferenceQueue<Bitmap>();

            clearReferenceQueue = new Thread(new Runnable() {
                @Override
                public void run() {
                    while (!shutDown){
                        try {
                            Reference<Bitmap> reference = referenceQueue.remove();
                            Bitmap bitmap = reference.get();

                            //对 bitmap 进行 recycle
                            if(bitmap != null && !bitmap.isRecycled()){
                                bitmap.recycle();
                            }
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            });
            clearReferenceQueue.start();
        }

        return referenceQueue;
    }

    public void init(String path){

        reusablePool = Collections.synchronizedSet(new HashSet<WeakReference<Bitmap>>());

        ActivityManager am = (ActivityManager) App.getInstance().getSystemService(Context.ACTIVITY_SERVICE);
        //获得程序可用最大内存,单位 M
        int memoryClass = am.getMemoryClass();

        //需要设置缓存最大的内存,一般根据 app 可用内存进行计算
        memoryCache = new LruCache<String, Bitmap>(memoryClass / 8 * 1024 * 1024){

            /**
             *
             * @param key
             * @param value
             * @return value占用的内存
             */
            @Override
            protected int sizeOf(String key, Bitmap value) {

                // 由于使用过了复用,
                // 使用 getAllocationByteCount 替代 getByteCount 进行内存获取
                return value.getAllocationByteCount();
            }

            /**
             * 当 Bitmap 从 Lru 中移除时 回调
             * @param evicted
             * @param key
             * @param oldValue
             * @param newValue
             */
            @Override
            protected void entryRemoved(boolean evicted, String key, Bitmap oldValue, Bitmap newValue) {

                //在回收的时候,判断其是否可复用
                if(oldValue.isMutable()){
                    /**
                     * Bitmap 在安卓中内存位置:
                     *      3.0 以下 native
                     *      3.0 Java
                     *      8.0 native
                     *  3.0 以下可以不管,
                     *  3.0 之后,内存由虚拟机管理
                     *  8.0 之后,在 oldValue 回收的时候,需要通知释放 native 内存。
                     */
                    Log.i("Adapter","加入复用池");
                    reusablePool.add(new WeakReference<Bitmap>(oldValue, getReferenceQueue()));
                } else {
                    oldValue.recycle();
                }

            }
        };

        try {
            // 第一个参数 directory:缓存文件
            // 第二个参数 appVersion:app 版本
            // 第三个参数 valueCount:表示一个 key 对应 valueCount 个文件
            // 第四个参数 maxSize:初始化磁盘缓存,默认设置 10 M
            diskLruCache = DiskLruCache.open(new File(path), BuildConfig.VERSION_CODE,
                    1, 10 * 1024 * 1024);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 保存 Bitmap 到内存
     * @param key
     * @param bitmap
     */
    public void putBitmap2Memory(String key, Bitmap bitmap) {
        memoryCache.put(key, bitmap);
    }

    /**
     * 从内存获取 Bitmap
     * @param key
     * @return
     */
    public Bitmap getBitmapFromMemory(String key) {
        return memoryCache.get(key);
    }

    /**
     * 清除内存 Bitmap 缓存
     * @return
     */
    public void clearMemory() {
        memoryCache.evictAll();
    }

    /**
     * 加入磁盘缓存
     * @param key
     * @param bitmap
     */
    public void putBitMap2Disk(String key,Bitmap bitmap){
        DiskLruCache.Snapshot snapshot = null;
        OutputStream os = null;
        try {
            snapshot = diskLruCache.get(key);
            // 如果缓存有对应 key 的文件,那么不管(也可以替换)
            if (null == snapshot){
                DiskLruCache.Editor edit = diskLruCache.edit(key);
                if (null != edit){
                    os = edit.newOutputStream(0);
                    bitmap.compress(Bitmap.CompressFormat.JPEG,50,os);
                    edit.commit();
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (null != snapshot){
                snapshot.close();
            }
            if (null != os){
                try {
                    os.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * 从磁盘缓存获取 对应key缓存的图片
     * @param key
     * @return
     */
    public Bitmap getBitmapFromDisk(String key,Bitmap reusable){
        DiskLruCache.Snapshot snapshot = null;
        Bitmap bitmap = null;
        try {
            snapshot = diskLruCache.get(key);
            if (null == snapshot){
                return null;
            }
            //获得文件输入流 读取 bitmap
            InputStream is = snapshot.getInputStream(0);
            //为了能够被复用内存
            options.inMutable = true;
            options.inBitmap = reusable;
            Log.i("Adapter","使用复用内存:"+reusable);
            bitmap = BitmapFactory.decodeStream(is,null,options);
            if (null != bitmap){
                memoryCache.put(key,bitmap);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (null != snapshot){
                snapshot.close();
            }
        }
        return bitmap;
    }

    /**
     * 从复用池获取可复用的内存
     *
     * 可被复用的 Bitmap 必须设置 inMutable 为 true;
     *      Android4.4(API 19) 之前只有格式为 jpg、png,同等宽高(要求苛刻),
     *          inSampleSize 为 1 的 Bitmap 才可以复用;
     *      Android4.4(API 19) 之前被复用的 Bitmap 的 inPreferredConfig
     *          会覆盖待分配内存的 Bitmap 设置的 inPreferredConfig;
     *      Android4.4(API 19) 之后被复用的 Bitmap 的内存
     *          必须大于等于需要申请内存的 Bitmap 的内存;
     *
     * @param w 新 Bitmap 的宽度
     * @param h 新 Bitmap 的高度
     * @param inSampleSize 缩放系数
     * @return 可复用的内存
     */
    public Bitmap getReusable(int w,int h,int inSampleSize){
        //忽略 15 之前的,现在比较少手机使用这个系统
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB){
            return null;
        }

        Bitmap reusable = null;
        Iterator<WeakReference<Bitmap>> iterator = reusablePool.iterator();

        //迭代查找符合复用条件的Bitmap
        while (iterator.hasNext()){
            Bitmap bitmap = iterator.next().get();
            if (null != bitmap){
                //可以被复用
                if (checkInBitmap(bitmap, w, h, inSampleSize)){
                    reusable = bitmap;
                    //移出复用池
                    iterator.remove();
                    break;
                }
            }else{
                iterator.remove();
            }
        }
        return reusable;
    }

    /**
     * 判断 bitmap 所在内存是否满足新 Bitmap 复用
     * @param bitmap
     * @param w
     * @param h
     * @param inSampleSize
     * @return
     */
    boolean checkInBitmap(Bitmap bitmap,int w,int h,int inSampleSize){
        //API 19 之前的判断
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT){
            return bitmap.getWidth() == w && bitmap.getHeight() == h
                    && inSampleSize == 1;
        }
        //如果缩放系数大于1 获得缩放后的宽与高
        if (inSampleSize > 1){
            w /= inSampleSize;
            h /= inSampleSize;
        }
        int byteCout = w * h * getPixelsCout(bitmap.getConfig());
        return byteCout <= bitmap.getAllocationByteCount();
    }

    /**
     * 根据 Bitmap.Config 获取每个像素占几个字节
     * @param config Bitmap 的格式
     * @return 占用字节数
     */
    private int getPixelsCout(Bitmap.Config config){
        if (config == Bitmap.Config.ARGB_8888){
            return 4;
        }
        //除了 ARGB_8888,其他基本都是 2 字节,有特殊的自行添加
        return 2;
    }
}

3.效果

未使用缓存:
这里写图片描述

使用缓存:
这里写图片描述

明显使用缓存的效果比没有使用缓存的效果流畅。

4.总结

这边的代码主要是学习是按思路进行编写的,大体上没有问题。实际项目中可以直接使用现在网络上的图片加载框架:Picasso、Glide 等。

六、附

代码链接

最后

以上就是体贴飞鸟为你收集整理的(五)图片压缩 —— 优化图片文件、内存一、图片压缩二、文件压缩三、压缩原理四、内存压缩五、手写图片缓存六、附的全部内容,希望文章能够帮你解决(五)图片压缩 —— 优化图片文件、内存一、图片压缩二、文件压缩三、压缩原理四、内存压缩五、手写图片缓存六、附所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(42)

评论列表共有 0 条评论

立即
投稿
返回
顶部