Android中的图片优化全解析

图片,每个App都会有,而它,又是最容易引起OOM的。所以,有必要在Android开发中对图片做一些处理。

在Android中,对图片的处理,可以从两个方面着手。

1.减少图片在磁盘上所占空间的大小。

减少图片在磁盘上所占空间的大小,分为两步:

  1. 去掉图片的Alpha通道。
  2. 使用哈夫曼算法对图片进行压缩。
2.减少图片在内存中的占用大小。
  1. 图片在Android程序中所占的内存大小,与图片大小无关,与图片像素点*图片格式有关。
  2. 设置BitmapFactory.Options中的inSampleSize属性来进行位图的缩放。
  3. 设置BitmapFactory.Options中的inBitmap属性,来实现图片的复用。
  4. 加载图片时,使用4级缓存。

1. 减少图片在磁盘上所占空间的大小。

Android6.0之前,并没有使用哈夫曼压缩,因此,Android自身提供的图片压缩方法,在6.0之前的压缩效果很差。我们需要自己编写一个方法,解决这个问题。编写这个方法时,就需要用到第三方库libjpeg,实际上,Android本身也是使用的这个库做的图片的压缩,但Android将其放入Skia图形引擎中,我们无法直接使用,我们要使用这个库,就需要自己编译好了,放在项目中使用。

使用NDK编译第三方库libjpeg。
libjpeg这个库非常有名,大多数的第三方图片压缩框架所使用的底层库都是它,它在Github的下载地址:https://github.com/libjpeg-turbo/libjpeg-turbo

1.在Linux中下载这个库。

在Github上复制release版本中的最新版本的Linux版本的链接:https://github.com/libjpeg-turbo/libjpeg-turbo/archive/2.0.3.tar.gz ,在Linux上使用命令


wget  https://github.com/libjpeg-turbo/libjpeg-turbo/archive/2.0.3.tar.gz

2.Linux中解压libjpeg

tag -xvf  2.0.3.tar.gz

-xvf命令之后跟着的是文件名和后缀

3.编译下载的libjpeg库

3.1 使用CMake

查看编译文档https://github.com/libjpeg-turbo/libjpeg-turbo/blob/master/BUILDING.md,发现必须要在CMake环境下编译,因此需要下载CMake。

3.1.1 下载CMake

进入CMake官网,点击下载Latest Release 的Linux版本,右键复制其下载链接:https://github.com/Kitware/CMake/releases/download/v3.16.0/cmake-3.16.0.tar.gz 在Linux中下载并解压,下载与解压的命令与1、2相同。

3.1.2 安装CMake

进入到Cmake文件夹中,Linux中使用命令 cd 文件夹名称 使用ls命令查看其中的文件: 其中绿色的是可执行文件,比如bootstrap文件就是可执行文件。使用如下命令来运行bootstrap

./bootstrap

3.1.3 安装完成后,依次执行gmake、make install命令,将CMake安装完成。

gmake
make install

3.1.4 使用cmake - -version 查看安装的cmake版本,如果可以看到cmake的版本 说明安装成功

cmake - -version

3.2 如果要编译x86的库,还需要下载NASM,下载解压运行的过程和CMake一致。运行configure文件。

3.3 编译libjpeg库。

编译后的文件是.a文件或者是我们熟悉的.so文件,这些文件我们可以直接放在项目中使用的。

3.3.1 编写编译命令

使用cd命令,进入解压后的libjpeg目录,在该目录中新建记事本文件build.sh,使用vim命令创建文本文件。

 vim build.sh

我们查看编译文档,发现其中给出的编译命令模板如下:

NDK_PATH={full path to the NDK directory-- for example, # NDK路径 在网上下载 如:\..\android-ndk-r19c
  /opt/android/android-ndk-r16b}
TOOLCHAIN={"gcc" or "clang"-- "gcc" must be used with NDK r14b and earlier,
  and "clang" must be used with NDK r17c and later} #c语言的编译器 : gcc或者clang
ANDROID_VERSION={the minimum version of Android to support.  "21" or later
  is required for a 64-bit build.}

cd {build_directory}
cmake -G"Unix Makefiles" \
  -DANDROID_ABI=arm64-v8a \  # 根据库的类型,这里可以写四种:armeabi-v7a、arm64-v8a、x86、x86_64
  -DANDROID_ARM_MODE=arm \ # 如果是x86类型,这句不用写
  -DANDROID_PLATFORM=android-${ANDROID_VERSION} \
  -DANDROID_TOOLCHAIN=${TOOLCHAIN} \
  -DCMAKE_ASM_FLAGS="--target=aarch64-linux-android${ANDROID_VERSION}" \
  -DCMAKE_TOOLCHAIN_FILE=${NDK_PATH}/build/cmake/android.toolchain.cmake \
  [additional CMake flags] {source_directory}
make

其中NDK_PATH是NDK路径,NDK需要自己在网上下载,下载地址是:https://developer.android.google.cn/ndk/downloads/older_releases.html
TOOLCHAIN 是C语言编译器的类型,一般有两种gcc和clang。ANDROID_VERSION是指,Android的版本号。DANDROID_ABI 是所要编译的库的类型,有四种,分别是:armeabi-v7a、arm64-v8a、x86、x86_64。这几个选项都是根据自己的情况填写的。填写之后的编译命令如下所示:

#!/bin/bash
# Set these variables to suit your needs
NDK_PATH=../android-ndk-r17c
TOOLCHAIN=gcc
ANDROID_VERSION=21

cmake -G"Unix Makefiles" \
  -DANDROID_ABI=x86_64 \
  -DANDROID_PLATFORM=android-${ANDROID_VERSION} \
  -DANDROID_TOOLCHAIN=${TOOLCHAIN} \
  -DCMAKE_TOOLCHAIN_FILE=${NDK_PATH}/build/cmake/android.toolchain.cmake \
make

这里需要注意的是,在编写时,需要添加一个抬头,在文件最前面写上:

#!/bin/bash

编写完成之后保存: esc —> 写入命令:wq(写入退出) 编译文件就写好了

3.3.2 执行build.sh文件

首先需要给build.sh添加执行权限,使用以下命令:

 chmod +x build.sh

添加完执行权限之后,build.sh文件变成了绿色,也就是Linux中的可执行文件。然后使用./build.sh命令来执行build.sh文件

./build.sh

编译好之后,回到之前的文件夹,使用ls查看其中的文件,会发现其中多了一个libturbojpeg.a的静态库

4 使用静态库

4.1 创建项目

创建我们要编写native方法的项目,在创建项目时,需要勾选include c++ support ,勾选完成后,会发现项目中多了个cpp文件夹,这个文件夹就是用来装一些NDK相关的文件等。然后将libjpeg-turbo文件夹中的 jconfig.h、jerror.h 、jmorecfg.h 、jpeglib.h 、turbojpeg.h 、静态库 libturbojpeg.a 拷贝进项目里。如下图所示:


Android中的图片优化全解析_第1张图片
Cpp文件夹

4.2 准备工作 :

在build.gradle文件中写入:

cmake {
    cppFlags ""
    abiFilers "x86_64"
    //指定 android编译器
    arguments '-DANDROID_TOOLCHAIN=gcc'
}

编辑 cMakeLists.txt 文件

cmake_minimum_required(VERSION 3.4.1)

add_library(
        native-lib
        SHARED
        src/main/cpp/native-lib.cpp)

add_library(libjpeg STATIC IMPORTED)
set_target_properties(libjpeg PROPERTIES IMPORTED_LOCATION ${CMAKE_SOURCE_DIR}/src/main/cpp/libs/libturbojpeg.a)

#引入头文件    import
include_directories(src/main/cpp/include)

target_link_libraries(
        native-lib
        libjpeg
        #jnigraphics是安卓NDK目录中直接有的
        jnigraphics
        log)

写好之后,编译,编译通过之后,我们在项目中写的native方法,都会在navite-lib.cpp文件中生成对应的c语言方法。

4.3 编写Native方法,进行图片的压缩

前面可以说都是准备工作,到这一步,终于到正题了,开始撸代码了。
在Java文件中编写native方法,会在项目中的native-lib.cpp文件中生成相应的C语言方法。

//Java方法 第一个参数是要压缩的Bitmap,第二个参数是压缩质量,第三个参数是压缩后的图片放置的位置
public native void nativeCompress(Bitmap bitmap,int q,String path);

对应生成的c语言方法,首先我们需要引入几个头文件,C语言里的头文件就相当于Java中的库,引入之后就可以使用其中的方法。

#include  //操作内存区需要的头文件
#include  // 操作Bitmap对象需要的头文件
#include  
4.3.1 去掉图片的Alpha通道。

什么叫做去掉图片的Alpha通道呢? 我们知道,图片都是由一个个的像素点构成的,每个像素点都是由argb组成,a就是透明度,rgb分别是红、绿、蓝三种颜色。去掉Alpha通道,就是把每个像素点的透明度去掉。

extern "C"
JNIEXPORT void JNICALL
Java_com_example_administrator_lsn_15_1demo_MainActivity_nativeCompress(JNIEnv *env,
                                                                        jobject instance,
                                                                        jobject bitmap, jint q,
                                                                        jstring path_) {
    const char *path = env->GetStringUTFChars(path_, 0);
    //从bitmap获取argb数据
    AndroidBitmapInfo info;//info=new 对象();
    //获取里面的信息
    AndroidBitmap_getInfo(env, bitmap, &info);//  void method(list)
    //得到图片中的像素信息
    uint8_t *pixels;//uint8_t char    java   byte     *pixels可以当byte[]
    AndroidBitmap_lockPixels(env, bitmap, (void **) &pixels);
    //jpeg argb中去掉他的a ===>rgb
    int w = info.width;
    int h = info.height;
    int color;
    //开一块内存用来存入rgb信息
    uint8_t *data = (uint8_t *) malloc(w * h * 3);//data中可以存放图片的所有内容
    uint8_t *temp = data;
    uint8_t r, g, b;//byte
    //循环取图片的每一个像素
    for (int i = 0; i < h; i++) {
        for (int j = 0; j < w; j++) {
            color = *(int *) pixels;//0-3字节  color4 个字节  一个点
            //取出rgb
            r = (color >> 16) & 0xFF;//    #00rrggbb  16  0000rr   8  00rrgg
            g = (color >> 8) & 0xFF;
            b = color & 0xFF;
            //存放,以前的主流格式jpeg    bgr
            *data = b;
            *(data + 1) = g;
            *(data + 2) = r;
            data += 3;
            //指针跳过4个字节
            pixels += 4;
        }
    }
    //把得到的新的图片的信息存入一个新文件中,这个方法中,使用哈夫曼压缩,将图片进一步的压缩
    write_JPEG_file(temp, w, h, q, path);


    //释放内存
    free(temp);
    AndroidBitmap_unlockPixels(env, bitmap);
    env->ReleaseStringUTFChars(path_, path);

}

Android中的图片优化全解析_第2张图片
内存示意图

在代码注释中,每一步已经写的很清楚。辅以图片的解释,大致的思路就是,取得图片的每一个像素点,使用位运算取出rgb,开一块新的内存,将取出的每一个rgb存入新内存中。这个新内存显示的图片,就是去掉了Alpha通道的图片了。

4.4 使用哈夫曼算法进行图片的压缩

首先来了解一下哈夫曼算法。哈夫曼算法,也叫做最优二叉树,原本是哈夫曼当年为了解决远距离通信的数据传输最优化的问题。这里被我们用来做图片的压缩。
我们知道,图片是由一个个的像素点组成的,我们取到这个图片每种像素点的个数,假如,一张图片只有6种颜色,我们将其排序。


Android中的图片优化全解析_第3张图片
哈夫曼算法一

Android中的图片优化全解析_第4张图片
哈夫曼算法二

Android中的图片优化全解析_第5张图片
哈夫曼算法三

一开始,图片的每个像素是由RGB组成,是3 * 8byte=24b,
这张图片所占的字节就是 5 * 24+8 * 24+15 * 24+···+30 * 24,使用哈夫曼算法得到的结果就是:
4b+4b+3b+2b+1b+2b,可以看到,使用哈夫曼算法压缩后的图片,压缩率可达到90%

那么怎么去写哈夫曼算法呢? 在去掉Alpha通道那一步里,我们留了一个方法write_JPEG_file(temp, w, h, q, path)还没写,就在这个方法中,来写哈夫曼压缩。

使用哈夫曼压缩一共要经历7步,这7步是固定的,只需调用第三方库的一些方法即可。
1、创建jpeg压缩对象
2、指定存储文件
3、设置压缩参数
4、开始压缩
5、循环写入每一行数据
6、压缩完成
7、释放jpeg对象

void write_JPEG_file(uint8_t *data, int w, int h, jint q, const char *path) {
//    3.1、创建jpeg压缩对象
    jpeg_compress_struct jcs;
    //错误回调
    jpeg_error_mgr error;
    jcs.err = jpeg_std_error(&error);
    //创建压缩对象
    jpeg_create_compress(&jcs);
//    3.2、指定存储文件  write binary
    FILE *f = fopen(path, "wb");
    jpeg_stdio_dest(&jcs, f);
//    3.3、设置压缩参数
    jcs.image_width = w;
    jcs.image_height = h;
    //bgr
    jcs.input_components = 3;
    jcs.in_color_space = JCS_RGB;
    jpeg_set_defaults(&jcs);
    //开启哈夫曼功能
    jcs.optimize_coding = true;
    jpeg_set_quality(&jcs, q, 1);
//    3.4、开始压缩
    jpeg_start_compress(&jcs, 1);
//    3.5、循环写入每一行数据
    int row_stride = w * 3;//一行的字节数
    JSAMPROW row[1];
    //jcs.next_scanline:行数指针 jcs.image_height:整个图片的高度
    while (jcs.next_scanline < jcs.image_height) {
        //取一行数据 row_stride:一行的大小
        uint8_t *pixels = data + jcs.next_scanline * row_stride;
        row[0]=pixels;
        //写一行数据
        jpeg_write_scanlines(&jcs,row,1);
    }
//    3.6、压缩完成
    jpeg_finish_compress(&jcs);
//    3.7、释放jpeg对象
    fclose(f);
    jpeg_destroy_compress(&jcs);
}

到这里磁盘压缩就写完了,要使用这两个方法,调用在Java代码中写的native方法即可。

public native void nativeCompress(Bitmap bitmap,int q,String path);

2.减少图片在内存中的占用大小

1. 图片在Android程序中所占的内存大小,与图片大小无关,与图片像素点*图片格式有关。

图片在Android程序中所占内存的大小和图片在磁盘上的大小其实没有多大的关系,真正有关系的是Android程序中的图片所在的Drawable文件夹。
为什么这样说呢,Android系统,会根据不同的Drawable文件夹来缩放图片,而图片在内存中的大小,是由图片长* 图片宽* 4来决定的(图片长* 图片宽 = 整张图片的像素点个数,在RGB_8888格式下每个像素点占4字节(argb各占一字节))。

因此,我们要减少图片在Android程序中所占用的内存,首先需要注意将图片放在哪个文件夹下;然后要注意使用哪一种图片格式。
Android中的图片格式有以下几种:

位图规格 每像素所占字节数
ARGB_8888 32
RGB_565 16
ARGB_4444 16
ALPHA_8 8

ARGB_8888是系统默认的位图格式。其他几种都减小了位图通道,可以减少内存开销并提升内存显示的性能。
如果不需要Alpha通道,除了大图模式,一般都可以使用RGB_565,几乎看不出差别。
如果需要更小的格式,但需要透明通道,就可以使用ARGB_4444,减少了一半的数据,但保留了透明通道,视觉上差异较大,一般可以用于用户头像。
Aplha_8主要是用于染色,图片不常用。
如何设置图片的位图规格呢? 在BitmapFactory.Options中设置,代码如下:

BitmapFactory.Options options = new BitmapFactory.Options();
options.inPreferredConfig = Bitmap.Config.RGB_565;
BitmapFactory.decodeStream(is, null,options);

2.使用inSampleSize属性。

BitmapFactory.Options 中的inSampleSize 属性实现了位图的缩放功能。将这个属性设置为1时,可以在不加载完整大小图片的前提下,生成一张只有原始图片部分大小的新图片。设置为2时,获得只有1/2大小的图片。设置为4,获得1/4大小的图片,以此类推。

BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = 4;
BitmapFactory.decodeStream(is, null,options);

3.使用inBitmap属性,内存复用。

如果设置了这个属性,那么,当需要使用decode方法显示一张图片时,decode方法会尝试重用一个已经存在的内存块,从而改善性能,不会再重新分配和释放内存。这里需要注意的是,在Android4.4之前,只能重用相同大小的Bitmap内存区域,而4.4之后,可以重用任何Bitmap的内存区域。

BitmapFactory.Options options=new BitmapFactory.Options();
//如果要复用,需要设计成易变
options.inMutable=true;
Bitmap bitmap=BitmapFactory.decodeResource(getResources(),R.mipmap.wyz_p,options);
// 将inBitmap设置成异变内存块bitmap,那么下次再显示图片时,就会使用内存块bitmap 
options.inBitmap=bitmap;//需要指定复用的内存块
bitmap=BitmapFactory.decodeResource(getResources(),R.mipmap.wyz_p,options);

前面几步了解之后,我们可以将其整理封装成一个方法,放入工具类中,每次加载图片时,都使用这个工具类加载。

public class ImageResize {

    /**
     *  缩放bitmap
     * @param context
     * @param id
     * @param maxW
     * @param maxH
     * @return
     */
    public static Bitmap resizeBitmap(Context context,int id,int maxW,int maxH,boolean hasAlpha,Bitmap reusable){
        Resources resources = context.getResources();
        BitmapFactory.Options options = new BitmapFactory.Options();
        // 只解码出 outxxx参数 比如 宽、高
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeResource(resources,id,options);
        //根据宽、高进行缩放
        int w = options.outWidth;
        int h = options.outHeight;
        //设置缩放系数
        options.inSampleSize = calcuteInSampleSize(w,h,maxW,maxH);
        if (!hasAlpha){
            options.inPreferredConfig = Bitmap.Config.RGB_565;
        }
        options.inJustDecodeBounds = false;
        //设置成能复用
        options.inMutable=true;
        options.inBitmap=reusable;
        return BitmapFactory.decodeResource(resources,id,options);
    }

    /**
     * 计算缩放系数
     * @param w
     * @param h
     * @param maxW
     * @param maxH
     * @return 缩放的系数
     */
    private static int calcuteInSampleSize(int w,int h,int maxW,int maxH) {
        int inSampleSize = 1;
        if (w > maxW && h > maxH){
            inSampleSize = 2;
            //循环 使宽、高小于 最大的宽、高
            while (w /inSampleSize > maxW && h / inSampleSize > maxH){
                inSampleSize *= 2;
            }
        }
        return inSampleSize;
    }
}

4.加载图片时,使用4级缓存。

4级缓存,就是在三级缓存的基础上,再多添加一层缓存。实际上,是在内存缓存中,再做一级缓存。
首先,了解一下三级缓存。
目前比较流行的图片框架,如Fresco,Glide等,都使用了“内存-磁盘-网络”三级缓存策略。首先,App访问网络拉取图片,分别将加载的图片保存在本地SD卡和内存中。当App再一次需要加载图片时,先判断内存中是否有缓存,有则直接从内存中拉取,没有则查看本地缓存目录,看其中是否有缓存,本地磁盘中如果存在缓存,则从本地缓存卡中拉取,否则从网络加载图片。

1.内存缓存。

内存缓存通常使用的是LruCache。LruCache在android.util包下,可以翻译为最近最少使用缓存,它用强引用保存需要缓存的对象,内部维护一个队列,LinkedHashMap内部的双向链表,添加了线程安全操作。原理就是:当其中的一个值被访问时,它被放到队列的尾部,当缓存将满时,队列头部的值会被丢弃,之后被垃圾回收。
LruCache的用法,也很简单,只需要调用Android提供的Api即可。

        //获取程序最大可用内存 单位是M
        ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
        int memoryClass=am.getMemoryClass();
        //   内存缓存的大小,一般是程序最大可用内存的1/8  单位是byte 
        memoryCache=new LruCache(memoryClass/8*1024*1024){
            /**
             * @return 每张图片value所占内存的大小
               这个方法的作用,就是通过LruCache的缓存大小,
               和返回的bitmap占用的内存大小,来计算LruCache中能放多少张图片。
             */
            @Override
            protected int sizeOf(String key, Bitmap value) {
               /* LruCache中的所有图片都是设置成可用复用的,
                   value.getByteCount():取到的是图片的大小,而不是内存的大小
                   如果一个内存块被复用后,新的图片大小<内存块大小 ,
                   那么得到的是图片大小,而不是内存块的大小  */
                if(Build.VERSION.SDK_INT > Build.VERSION_CODES.KITKAT){
                    return value.getAllocationByteCount();//19之后,获取内存块大小
                }
              /*取到的是图片的大小,而不是内存的大小,
                但在19之前,必须是相同大小的Bitmap才可以复用,
                因此可以直接使用这个方法获取内存大小。*/
                return value.getByteCount();
            }
            /**
             * 当lru满了,bitmap从lru中移除对象时,会回调 
             * oldValue就是要从LruCache中移除的对象
             */
            @Override
            protected void entryRemoved(boolean evicted, String key, Bitmap oldValue, Bitmap newValue) {
                oldValue.recycle();
            }
        };
在内存缓存的基础上,再添加一级缓存,也就是,被丢弃的Bitmap,不急着回收,而是放入一个复用池中。 再在复用池中开启一个引用队列,如果复用池中的Bitmap被GC扫到过,那么就将该Bitmap放入到引用队列中,将其释放。这里要注意的是,复用池中不是图片,而是一个个可以复用的内存块。

第四级缓存的代码如下:

  1. 定义一个复用池
   public static Set> reuseablePool;

2.定义一个引用队列

ReferenceQueue referenceQueue;

3.单开一个线程,死循环,一直轮询引用队列中是否已有Bitmap。如果没有,一直阻塞,如果有就移除。

 Thread clearReferenceQueue;
    boolean shutDown;

    private ReferenceQueue getReferenceQueue(){
        if(null==referenceQueue){
            //当弱用引需要被回收的时候,会进到这个队列中
            referenceQueue=new ReferenceQueue();
            //单开一个线程,去扫描引用队列中GC扫到的内容,交到native层去释放
            clearReferenceQueue=new Thread(new Runnable() {
                @Override
                public void run() {
                    while(!shutDown){
                        try {
                            //remove 获取并移除头结点,是阻塞式的 ,如果队列中没有数据,就会一直阻塞在这里。
                            Reference reference=referenceQueue.remove();
                            Bitmap bitmap=reference.get();
                            if(null!=bitmap && !bitmap.isRecycled()){
                                bitmap.recycle();
                            }
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            });
            clearReferenceQueue.start();
        }
        return referenceQueue;
    }

  1. 在内存缓存中,把队伍头的Bitmap,也就是要回收的Bitmap放入引用队列中。也就是在LruCache的entryRemoved中,把oldValue放入复用池中。
            @Override
            protected void entryRemoved(boolean evicted, String key, Bitmap oldValue, Bitmap newValue) {
                if(oldValue.isMutable()){//如果是设置成能复用的内存块,拉到java层来管理
                    //3.0以下 Bitmap放在 native层,3.0--8.0 Bitmap放在java层 8.0之后 Bitmap又放回Navite层
                    //把这些图片放到一个复用沲中
                    reuseablePool.add(new WeakReference(oldValue,referenceQueue));
                }else{
                    //oldValue就是移出来的对象
                    oldValue.recycle();
                }
            }
2.磁盘缓存。

磁盘缓存使用的是第三方的DiskLruCache。DiskLruCache是Jake Wharton写的一个第三方库,它的地址是https://github.com/JakeWharton/DiskLruCache,下载下来之后,发现其中只用三个类,拷贝进自己的项目中进行使用即可。

  1. 开启磁盘缓存,使用open方法
diskLruCache = DiskLruCache.open(new File(dir), BuildConfig.VERSION_CODE, 1, 10 * 1024 * 1024);

2.写入磁盘缓存,使用DiskLruCache.Editor

/**
     * 加入磁盘缓存
     */
    public void putBitMapToDisk(String key,Bitmap bitmap){
        DiskLruCache.Snapshot snapshot=null;
        OutputStream os=null;
        try {
            snapshot=diskLruCache.get(key);
            //如果缓存中已经有这个文件  不理他
            if(null==snapshot){
                //如果没有这个文件,就生成这个文件
                DiskLruCache.Editor editor=diskLruCache.edit(key);
                if(null!=editor){
                    os=editor.newOutputStream(0);
                    bitmap.compress(Bitmap.CompressFormat.JPEG,50,os);
                    editor.commit();
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            if(null!=snapshot){
                snapshot.close();
            }
            if(null!=os){
                try {
                    os.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

3.从磁盘缓存中取

 /**
     * 从磁盘缓存中取
     */
    public Bitmap getBitmapFromDisk(String key,Bitmap reuseable){
        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=reuseable;
            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;
    }

整理成一个工具类,就是这样:

public class ImageCache {

    private static ImageCache instance;
    private Context context;
    private LruCache memoryCache;
    private DiskLruCache diskLruCache;

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

    /**
     * 定义一个复用池
     */
    public static Set> reuseablePool;


    public static ImageCache getInstance(){
        if(null==instance){
            synchronized (ImageCache.class){
                if(null==instance){
                    instance=new ImageCache();
                }
            }
        }
        return instance;
    }

    //引用队列
    ReferenceQueue referenceQueue;
    Thread clearReferenceQueue;
    boolean shutDown;

    private ReferenceQueue getReferenceQueue(){
        if(null==referenceQueue){
            //当弱用引需要被回收的时候,会进到这个队列中
            referenceQueue=new ReferenceQueue();
            //单开一个线程,去扫描引用队列中GC扫到的内容,交到native层去释放
            clearReferenceQueue=new Thread(new Runnable() {
                @Override
                public void run() {
                    while(!shutDown){
                        try {
                            //remove 获取并移除头结点,是阻塞式的
                            Reference reference=referenceQueue.remove();
                            Bitmap bitmap=reference.get();
                            if(null!=bitmap && !bitmap.isRecycled()){
                                bitmap.recycle();
                            }
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            });
            clearReferenceQueue.start();
        }
        return referenceQueue;
    }

    //dir是用来存放图片文件的路径
    public void init(Context context,String dir){
        this.context=context.getApplicationContext();

        //复用池 使用一个带锁的set集合
        reuseablePool=Collections.synchronizedSet(new HashSet>());

        ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
        //获取程序最大可用内存 单位是M
        int memoryClass=am.getMemoryClass();
        // 内存缓存的大小,一般是程序最大可用内存的1/8 单位是byte
        memoryCache=new LruCache(memoryClass/8*1024*1024){
            /**
             *
             * @return bitmap占用的内存大小 
             */
            @Override
            protected int sizeOf(String key, Bitmap bitmap) {
                //19之前   必需同等大小,才能复用  inSampleSize=1
                if(Build.VERSION.SDK_INT > Build.VERSION_CODES.KITKAT){
                    return bitmap.getAllocationByteCount();
                }
                return bitmap.getByteCount();
            }
            /**
             * 当lru满了,bitmap从lru中移除对象时,会回调
             */
            @Override
            protected void entryRemoved(boolean evicted, String key, Bitmap oldValue, Bitmap newValue) {
                if(oldValue.isMutable()){//如果是设置成能复用的内存块,拉到java层来管理
                    //3.0以下   Bitmap   native
                    //3.0以后---8.0之前  java
                    //8。0开始      native
                    //把这些图片放到一个复用沲中
                    reuseablePool.add(new WeakReference(oldValue,referenceQueue));
                }else{
                    //oldValue就是移出来的对象
                    oldValue.recycle();
                }


            }
        };
        // 开启磁盘缓存,第三个参数 valueCount:表示一个key对应valueCount个文件  第四个参数 磁盘缓存的最大缓存
       try {
           diskLruCache = DiskLruCache.open(new File(dir), BuildConfig.VERSION_CODE, 1, 10 * 1024 * 1024);
       }catch(Exception e){
           e.printStackTrace();
       }

       getReferenceQueue();
    }





    public void putBitmapToMemeory(String key,Bitmap bitmap){
        memoryCache.put(key,bitmap);
    }
    public Bitmap getBitmapFromMemory(String key){
        return memoryCache.get(key);
    }
    public void clearMemoryCache(){
        memoryCache.evictAll();
    }



    /*
    从复用池中取得Bitmap 
    这个方法的使用时机:是在内存缓存中没有某张图片时,调用该方法看缓存池中有没有这张图片
    */
    public Bitmap getReuseable(int w,int h,int inSampleSize){
        if(Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB){
            //3.0之前 
            return null;
        }
        Bitmap reuseable=null;
        Iterator> iterator = reuseablePool.iterator();
        while(iterator.hasNext()){
            Bitmap bitmap=iterator.next().get();
            if(null!=bitmap){
                //看是否可以复用,bitmap必须<=复用的内存块大小才可以复用
                if(checkInBitmap(bitmap,w,h,inSampleSize)){
                    //从复用池中拿一个,就删掉一个
                    reuseable=bitmap;
                    iterator.remove();
                    break;
                }else{
                    iterator.remove();
                }
            }
        }
        return reuseable;
    }



    private boolean checkInBitmap(Bitmap bitmap, int w, int h, int inSampleSize) {
        if(Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT){
            //19以前,必须宽高相同,才可以复用
            return bitmap.getWidth()==w && bitmap.getHeight()==h && inSampleSize==1;
        }
        if(inSampleSize>=1){
            //做过缩放
            w/=inSampleSize;
            h/=inSampleSize;
        }
        int byteCount=w*h*getPixelsCount(bitmap.getConfig());//图片所占的字节数
        return byteCount<=bitmap.getAllocationByteCount();//图片的大小<复用内存块的大小时,可以复用
    }

    private int getPixelsCount(Bitmap.Config config) {
        if(config==Bitmap.Config.ARGB_8888){
            return 4;
        }
        return 2;
    }


    //磁盘缓存的处理
    /**
     * 加入磁盘缓存
     */
    public void putBitMapToDisk(String key,Bitmap bitmap){
        DiskLruCache.Snapshot snapshot=null;
        OutputStream os=null;
        try {
            snapshot=diskLruCache.get(key);
            //如果缓存中已经有这个文件  不理他
            if(null==snapshot){
                //如果没有这个文件,就生成这个文件
                DiskLruCache.Editor editor=diskLruCache.edit(key);
                if(null!=editor){
                    os=editor.newOutputStream(0);
                    bitmap.compress(Bitmap.CompressFormat.JPEG,50,os);
                    editor.commit();
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            if(null!=snapshot){
                snapshot.close();
            }
            if(null!=os){
                try {
                    os.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    /**
     * 从磁盘缓存中取
     */
    public Bitmap getBitmapFromDisk(String key,Bitmap reuseable){
        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=reuseable;
            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 bitmap=ImageCache.getInstance().getBitmapFromMemory(String.valueOf(position));
        if(null==bitmap){
            //如果内存没数据,就去复用池找,找的是可以复用的内存块,这个内存块在磁盘缓存中可以使用
            Bitmap reuseable=ImageCache.getInstance().getReuseable(60,60,1);
            //从磁盘找
            bitmap = ImageCache.getInstance().getBitmapFromDisk(String.valueOf(position),reuseable);
            //如果磁盘中也没缓存,就从网络下载
            if(null==bitmap){
                bitmap=ImageResize.resizeBitmap(context,R.mipmap.wyz_p,80,80,false,reuseable);
                ImageCache.getInstance().putBitmapToMemeory(String.valueOf(position),bitmap);
                ImageCache.getInstance().putBitMapToDisk(String.valueOf(position),bitmap);
                Log.i("jett","从网络加载了数据");
            }else{
                Log.i("jett","从磁盘中加载了数据");
            }

        }else{
            Log.i("jett","从内存中加载了数据");
        }

到这里,基本上就总结了全部Android中的图片优化的方式。使用这一套全家桶下来,图片是可以减少50%以上的占用内存的。

你可能感兴趣的:(Android中的图片优化全解析)